Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SNOW-1449798: Adding E2E sample for NA + Hybrid Tables #10

Merged
merged 7 commits into from
Jul 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ jobs:
python -m pip install pytest
- name: Run tests
run: |
args=${{ steps.tests_to_run.outputs.pytestArgs }}
pythonpath=${{ steps.tests_to_run.outputs.pytestPaths }}
args="${{ steps.tests_to_run.outputs.pytestArgs }}"
pythonpath="${{ steps.tests_to_run.outputs.pytestPaths }}"
if [ -z "${args}" ] || [ -z "${pythonpath}" ]; then
echo “Nothing to test”
else
Expand Down
2 changes: 2 additions & 0 deletions hybrid-tables/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
snowflake.local.yml
output/**
70 changes: 70 additions & 0 deletions hybrid-tables/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Hybrid Tables

This Snowflake Native Application sample demonstrates how to use hybrid tables when user needs to enforce a primary key constraint.

The `internal.dictionary` table simulates the behavior of a `Dictionary/Hash Table` data structure by allowing users add just key/value pairs that don't exist yet in the table.

```sql
CREATE HYBRID TABLE IF NOT EXISTS internal.dictionary
(
key VARCHAR,
value VARCHAR,
CONSTRAINT pkey PRIMARY KEY (key)
);
```

To add new values in the `dictionary` table, there is a Stored Procedure that returns an `OK` status if the columns where added successfully or a json with the error if the new row could not be added.

```sql
CREATE OR REPLACE PROCEDURE core.add_key_value(KEY VARCHAR, VALUE VARCHAR)
RETURNS VARIANT
LANGUAGE SQL
AS
$$
BEGIN
INSERT INTO internal.dictionary VALUES (:KEY, :VALUE);
RETURN OBJECT_CONSTRUCT('STATUS', 'OK');
EXCEPTION
WHEN STATEMENT_ERROR THEN
RETURN OBJECT_CONSTRUCT('STATUS', 'FAILED',
'SQLCODE', SQLCODE,
'SQLERRM', SQLERRM,
'SQLSTATE', SQLSTATE);
END;
$$;
```

## Development

### Setting up / Updating the Environment

Run the following command to create or update Conda environment. This includes tools like Snowflake CLI and testing packages:

```sh
conda env update -f local_test_env.yml
```
To activate the environment, run the following command:

```sh
conda activate hybrid-tables-testing
```

### Automated Testing

With the conda environment activated, you can test the app as follows:

```sh
pytest
```

### Manual Testing / Deployment to Snowflake

You can deploy the application in dev mode as follows:

```sh
snow app run
```

## Additional Resources

- [Hybrid Tables](https://docs.snowflake.com/en/user-guide/tables-hybrid)
20 changes: 20 additions & 0 deletions hybrid-tables/app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
## Welcome to Hybrid Tables + Native Apps!

In this Snowflake Native App, you will be able to explore the usage of `Hybrid Tables` within Native Apps.

For more information about a Snowflake Native App, please read the [official Snowflake documentation](https://docs.snowflake.com/en/developer-guide/native-apps/native-apps-about) which goes in depth about many additional functionalities of this framework.

## Using the application after installation
To interact with the application after it has successfully installed in your account, switch to the application owner role first.

### Calling a stored procedure

```
CALL <your_application_name>.<schema_name>.<stored_procedure_name_with_args>;
```

### Calling a function

```
SELECT <your_application_name>.<schema_name>.<udf_with_args>;
```
11 changes: 11 additions & 0 deletions hybrid-tables/app/manifest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# This is a manifest.yml file, a required component of creating a Snowflake Native App.
# This file defines properties required by the application package, including the location of the setup script and version definitions.
# Refer to https://docs.snowflake.com/en/developer-guide/native-apps/creating-manifest for a detailed understanding of this file.

manifest_version: 1

artifacts:
setup_script: setup_script.sql
default_streamlit: core.ui
extension_code: true
readme: README.md
43 changes: 43 additions & 0 deletions hybrid-tables/app/setup_script.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
-- This is the setup script that runs while installing a Snowflake Native App in a consumer account.
-- To write this script, you can familiarize yourself with some of the following concepts:
-- Application Roles
-- Versioned Schemas
-- UDFs/Procs
-- Extension Code
-- Refer to https://docs.snowflake.com/en/developer-guide/native-apps/creating-setup-script for a detailed understanding of this file.

CREATE APPLICATION ROLE IF NOT EXISTS app_public;
CREATE OR ALTER VERSIONED SCHEMA core;
CREATE SCHEMA IF NOT EXISTS internal;
GRANT USAGE ON SCHEMA core TO APPLICATION ROLE app_public;
GRANT USAGE ON SCHEMA internal TO APPLICATION ROLE app_public;

CREATE HYBRID TABLE IF NOT EXISTS internal.dictionary
(
key VARCHAR,
value VARCHAR,
CONSTRAINT pkey PRIMARY KEY (key)
);

CREATE OR REPLACE PROCEDURE core.add_key_value(KEY VARCHAR, VALUE VARCHAR)
RETURNS VARIANT
LANGUAGE SQL
AS
$$
BEGIN
INSERT INTO internal.dictionary VALUES (:KEY, :VALUE);
RETURN OBJECT_CONSTRUCT('STATUS', 'OK');
EXCEPTION
WHEN STATEMENT_ERROR THEN
RETURN OBJECT_CONSTRUCT('STATUS', 'FAILED',
'SQLCODE', SQLCODE,
'SQLERRM', SQLERRM,
'SQLSTATE', SQLSTATE);
END;
$$;

CREATE OR REPLACE STREAMLIT core.ui
FROM '/streamlit/'
MAIN_FILE = 'ui.py';

GRANT USAGE ON STREAMLIT core.ui TO APPLICATION ROLE app_public;
12 changes: 12 additions & 0 deletions hybrid-tables/local_test_env.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# This file is used to install packages for local testing
name: hybrid-tables-testing
channels:
- snowflake
dependencies:
- python=3.8
- pip
- pip:
- snowflake-snowpark-python>=1.15.0
- pytest
- streamlit>=1.26.0

2 changes: 2 additions & 0 deletions hybrid-tables/pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[pytest]
pythonpath=python/src
8 changes: 8 additions & 0 deletions hybrid-tables/python/src/environment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# This file is used to install packages used by the Streamlit App.
# For more details, refer to https://docs.snowflake.com/en/developer-guide/streamlit/create-streamlit-sql#label-streamlit-install-packages-manual

channels:
- snowflake
dependencies:
- streamlit=1.26.0
- snowflake-native-apps-permission
30 changes: 30 additions & 0 deletions hybrid-tables/python/src/ui.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from snowflake.snowpark import Session
import json
import streamlit as st

class UI:
def __init__(self, session: Session) -> None:
self.session = session

def run(self):
with st.form('key_value_form', clear_on_submit=True):
col1, col2 = st.columns(2)
key = col1.text_input('Add a key', key='Key')
value = col2.text_input('Add a value for the key', key='Value')
if st.form_submit_button('Add'):
if key == '' or value == '':
st.error("Key-value pairs should not be empty")
else:
self.add(key, value)

st.dataframe(self.session.table('internal.dictionary').to_pandas(), use_container_width=True)

def add(self, key: str, value: str):
result = json.loads(self.session.call('core.add_key_value', key, value))
if "SQLCODE" in result:
error = f"Primary key violation on Hybrid Table. **{key}** already exists." if result["SQLCODE"] == 200001 else result["SQLERRM"]
st.error(error, icon="🚨")

if __name__ == '__main__':
ui = UI(Session.builder.getOrCreate())
ui.run()
40 changes: 40 additions & 0 deletions hybrid-tables/python/test/test_ui.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import pytest
from unittest.mock import patch, MagicMock
from snowflake.snowpark import Session
from streamlit.testing.v1 import AppTest
from ui import UI

@pytest.fixture()
def session():
session = Session.builder.config('local_testing', True).create()
yield session
session.close()

@patch('snowflake.snowpark.session.Session.table')
def test_run(table: MagicMock, session):
# arrange
def script(session):
from ui import UI
sut = UI(session)
return sut.run()

table.return_value = session.create_dataframe([{"key": "mykey", "value": "myvalue"}])

# act
at = AppTest.from_function(script, kwargs={ "session": session }).run()

# assert
assert len(at.dataframe[0].value.index) == 1

@patch('snowflake.snowpark.session.Session.call')
def test_add(call: MagicMock, session):
# arrange
call.return_value = '{ "STATUS": "OK" }'
sut = UI(session)

# act
sut.add('', '')

# assert
call.assert_called_once()

11 changes: 11 additions & 0 deletions hybrid-tables/snowflake.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# This is a project definition file, a required component if you intend to use Snowflake CLI in a project directory such as this template.

definition_version: 1
native_app:
name: hybrid_tables
source_stage: app_src.stage
artifacts:
- src: app/*
dest: ./
- src: python/src/*
dest: streamlit/
Loading