From 306394ffab49e4351cec70bfe824fd7e386e5132 Mon Sep 17 00:00:00 2001 From: Thomas Schmidt Date: Sun, 7 Jan 2024 15:27:11 +0100 Subject: [PATCH] Add Snowflake support (#29) --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 1 + README.md | 4 + docsource/getting_started/installation.md | 3 + docsource/index.rst | 1 + docsource/usage/snowflake/examples.md | 63 +++++ docsource/usage/snowflake/index.rst | 10 + docsource/usage/snowflake/settings.md | 13 + examples/snowflake/test_example.py | 87 ++++++ poetry.lock | 259 ++++++++++++++++-- pyproject.toml | 4 + src/sql_mock/snowflake/__init__.py | 0 src/sql_mock/snowflake/column_mocks.py | 76 +++++ src/sql_mock/snowflake/settings.py | 8 + src/sql_mock/snowflake/table_mocks.py | 26 ++ tests/sql_mock/snowflake/__init__.py | 0 tests/sql_mock/snowflake/test_column_mocks.py | 47 ++++ tests/sql_mock/snowflake/test_table_mocks.py | 68 +++++ 18 files changed, 649 insertions(+), 23 deletions(-) create mode 100644 docsource/usage/snowflake/examples.md create mode 100644 docsource/usage/snowflake/index.rst create mode 100644 docsource/usage/snowflake/settings.md create mode 100644 examples/snowflake/test_example.py create mode 100644 src/sql_mock/snowflake/__init__.py create mode 100644 src/sql_mock/snowflake/column_mocks.py create mode 100644 src/sql_mock/snowflake/settings.py create mode 100644 src/sql_mock/snowflake/table_mocks.py create mode 100644 tests/sql_mock/snowflake/__init__.py create mode 100644 tests/sql_mock/snowflake/test_column_mocks.py create mode 100644 tests/sql_mock/snowflake/test_table_mocks.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c434482..25441c3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: args: ["--line-length", "119"] stages: [commit] - repo: https://github.com/pycqa/flake8 - rev: 3.9.2 + rev: 7.0.0 hooks: - id: flake8 args: ["--ignore", "E203,E266,E501,W503", "--max-line-length", "119", "--max-complexity", "18", "--select", "B,C,E,F,W,T4,B9"] diff --git a/CHANGELOG.md b/CHANGELOG.md index 80f21f5..7a744ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added * Support for Redshift +* Support for Snowflake ### Changed diff --git a/README.md b/README.md index 38e7a9f..8496aec 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ The library currently supports the following databases. * BigQuery * Clickhouse * Redshift +* Snowflake ## Installation @@ -30,6 +31,9 @@ pip install --upgrade "sql-mock[clickhouse]" # Redshift pip install --upgrade "sql-mock[redshift]" + +# Snowflake +pip install --upgrade "sql-mock[snowflake]" ``` If you need to modify this source code, install the dependencies using poetry: diff --git a/docsource/getting_started/installation.md b/docsource/getting_started/installation.md index d80e152..0a36650 100644 --- a/docsource/getting_started/installation.md +++ b/docsource/getting_started/installation.md @@ -15,6 +15,9 @@ pip install --upgrade "sql-mock[clickhouse]" # Redshift pip install --upgrade "sql-mock[redshift]" + +# Snowflake +pip install --upgrade "sql-mock[snowflake]" ``` If you need to modify this source code, install the dependencies using poetry: diff --git a/docsource/index.rst b/docsource/index.rst index 98e1a65..b78bacb 100644 --- a/docsource/index.rst +++ b/docsource/index.rst @@ -43,6 +43,7 @@ It provides a consistent and convenient way to test the execution of your query usage/bigquery/index usage/clickhouse/index usage/redshift/index + usage/snowflake/index .. toctree:: :maxdepth: 3 diff --git a/docsource/usage/snowflake/examples.md b/docsource/usage/snowflake/examples.md new file mode 100644 index 0000000..645621b --- /dev/null +++ b/docsource/usage/snowflake/examples.md @@ -0,0 +1,63 @@ +```{toctree} +:maxdepth: 2 +``` + +# Example: Testing Subscription Counts in Snowflake + +```python +import datetime +from sql_mock.snowflake import column_mocks as col +from sql_mock.snowflake.table_mocks import SnowflakeMockTable +from sql_mock.table_mocks import table_meta + +# Define mock tables for your data model that inherit from SnowflakeMockTable +@table_meta(table_ref="data.users") +class UserTable(SnowflakeMockTable): + user_id = col.INTEGER(default=1) + user_name = col.STRING(default="Mr. T") + + +@table_meta(table_ref="data.subscriptions") +class SubscriptionTable(SnowflakeMockTable): + subscription_id = col.INTEGER(default=1) + period_start_date = col.DATE(default=datetime.date(2023, 9, 5)) + period_end_date = col.DATE(default=datetime.date(2023, 9, 5)) + user_id = col.INTEGER(default=1) + + +# Define a mock table for your expected results +class SubscriptionCountTable(SnowflakeMockTable): + subscription_count = col.INTEGER(default=1) + user_id = col.INTEGER(default=1) + +# Your original SQL query +query = """ +SELECT + count(*) AS subscription_count, + user_id +FROM data.users +LEFT JOIN data.subscriptions USING(user_id) +GROUP BY user_id +""" + +def test_something(): + # Create mock data for the 'data.users' and 'data.subscriptions' tables + users = UserTable.from_dicts([{'user_id': 1}, {'user_id': 2}]) + subscriptions = SubscriptionTable.from_dicts([ + {'subscription_id': 1, 'user_id': 1}, + {'subscription_id': 2, 'user_id': 1}, + {'subscription_id': 2, 'user_id': 2}, + ]) + + # Define your expected results + expected = [ + {"USER_ID": 1, "SUBSCRIPTION_COUNT": 2}, + {"USER_ID": 2, "SUBSCRIPTION_COUNT": 1}, + ] + + # Simulate the SQL query using SQL Mock + res = SubscriptionCountTable.from_mocks(query=query, input_data=[users, subscriptions]) + + # Assert the results + res.assert_equal(expected) +``` diff --git a/docsource/usage/snowflake/index.rst b/docsource/usage/snowflake/index.rst new file mode 100644 index 0000000..6c143df --- /dev/null +++ b/docsource/usage/snowflake/index.rst @@ -0,0 +1,10 @@ +Snowflake +===================== + +This section documents the specifics on how to use SQL Mock with Snowflake + +.. toctree:: + :maxdepth: 4 + + ./settings.md + ./examples.md diff --git a/docsource/usage/snowflake/settings.md b/docsource/usage/snowflake/settings.md new file mode 100644 index 0000000..2c9143d --- /dev/null +++ b/docsource/usage/snowflake/settings.md @@ -0,0 +1,13 @@ +```{toctree} +:maxdepth: 2 +``` + +# Settings + +In order to use SQL Mock with Snowflake, you need to provide the following environment variables when you run tests: + +* `SQL_MOCK_SNOWFLAKE_ACCOUNT`: The name of your Snowflake account +* `SQL_MOCK_SNOWFLAKE_USER`: The name of your Snowflake user +* `SQL_MOCK_SNOWFLAKE_PASSWORD`: The password for your Snowflake user + +Having those environment variables enables SQL Mock to connect to your Snowflake instance. diff --git a/examples/snowflake/test_example.py b/examples/snowflake/test_example.py new file mode 100644 index 0000000..065c51d --- /dev/null +++ b/examples/snowflake/test_example.py @@ -0,0 +1,87 @@ +import datetime + +from sql_mock.snowflake import column_mocks as col +from sql_mock.snowflake.table_mocks import SnowflakeMockTable +from sql_mock.table_mocks import table_meta + + +@table_meta(table_ref="data.users") +class UserTable(SnowflakeMockTable): + user_id = col.INTEGER(default=1) + user_name = col.STRING(default="Mr. T") + + +@table_meta(table_ref="data.subscriptions") +class SubscriptionTable(SnowflakeMockTable): + subscription_id = col.INTEGER(default=1) + period_start_date = col.DATE(default=datetime.date(2023, 9, 5)) + period_end_date = col.DATE(default=datetime.date(2023, 9, 5)) + user_id = col.INTEGER(default=1) + + +@table_meta( + query_path="./examples/test_query.sql", + default_inputs=[ + UserTable([]), + SubscriptionTable([]), + ], # We can provide defaults for the class if needed. Then we don't always need to provide data for all input tables. +) +class MultipleSubscriptionUsersTable(SnowflakeMockTable): + user_id = col.INTEGER(default=1) + + +def test_something(): + users = UserTable.from_dicts([{"user_id": 1}, {"user_id": 2}]) + subscriptions = SubscriptionTable.from_dicts( + [ + {"subscription_id": 1, "user_id": 1}, + {"subscription_id": 2, "user_id": 1}, + {"subscription_id": 2, "user_id": 2}, + ] + ) + + subscriptions_per_user__expected = [ + {"USER_ID": 1, "SUBSCRIPTION_COUNT": 2}, + {"USER_ID": 2, "SUBSCRIPTION_COUNT": 1}, + ] + users_with_multiple_subs__expected = [{"USER_ID": 1, "SUBSCRIPTION_COUNT": 2}] + end_result__expected = [{"USER_ID": 1}] + + res = MultipleSubscriptionUsersTable.from_mocks(input_data=[users, subscriptions]) + + # Check the results of the subscriptions_per_user CTE + res.assert_cte_equal("subscriptions_per_user", subscriptions_per_user__expected) + # Check the results of the users_with_multiple_subs CTE + res.assert_cte_equal("users_with_multiple_subs", users_with_multiple_subs__expected) + # Check the end result + res.assert_equal(end_result__expected) + + +def test_with_defaults_for_subscriptions_table(): + """ + In this test case we don't provide a mock for subscriptions + because we use the class default Subscriptions([]) which translates to an empty table. + """ + users = UserTable.from_dicts( + [ + {"user_id": 1}, + {"user_id": 2}, + ] + ) + + subscriptions_per_user__expected = [ + {"USER_ID": 1, "SUBSCRIPTION_COUNT": 0}, + {"USER_ID": 2, "SUBSCRIPTION_COUNT": 0}, + ] + users_with_multiple_subs__expected = [] + end_result__expected = [] + + # We don't provide a mock input for subscriptions + res = MultipleSubscriptionUsersTable.from_mocks(input_data=[users]) + + # Check the results of the subscriptions_per_user CTE + res.assert_cte_equal("subscriptions_per_user", subscriptions_per_user__expected) + # Check the results of the users_with_multiple_subs CTE + res.assert_cte_equal("users_with_multiple_subs", users_with_multiple_subs__expected) + # Check the end result + res.assert_equal(end_result__expected) diff --git a/poetry.lock b/poetry.lock index b97df65..e1ad615 100644 --- a/poetry.lock +++ b/poetry.lock @@ -174,6 +174,70 @@ files = [ {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, ] +[[package]] +name = "cffi" +version = "1.16.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, + {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, + {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, + {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, + {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, + {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, + {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, + {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, + {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, + {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, + {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, + {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, + {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, + {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, +] + +[package.dependencies] +pycparser = "*" + [[package]] name = "cfgv" version = "3.4.0" @@ -428,6 +492,51 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "cryptography" +version = "41.0.7" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:3c78451b78313fa81607fa1b3f1ae0a5ddd8014c38a02d9db0616133987b9cdf"}, + {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:928258ba5d6f8ae644e764d0f996d61a8777559f72dfeb2eea7e2fe0ad6e782d"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a1b41bc97f1ad230a41657d9155113c7521953869ae57ac39ac7f1bb471469a"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:841df4caa01008bad253bce2a6f7b47f86dc9f08df4b433c404def869f590a15"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5429ec739a29df2e29e15d082f1d9ad683701f0ec7709ca479b3ff2708dae65a"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:43f2552a2378b44869fe8827aa19e69512e3245a219104438692385b0ee119d1"}, + {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:af03b32695b24d85a75d40e1ba39ffe7db7ffcb099fe507b39fd41a565f1b157"}, + {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:49f0805fc0b2ac8d4882dd52f4a3b935b210935d500b6b805f321addc8177406"}, + {file = "cryptography-41.0.7-cp37-abi3-win32.whl", hash = "sha256:f983596065a18a2183e7f79ab3fd4c475205b839e02cbc0efbbf9666c4b3083d"}, + {file = "cryptography-41.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:90452ba79b8788fa380dfb587cca692976ef4e757b194b093d845e8d99f612f2"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:079b85658ea2f59c4f43b70f8119a52414cdb7be34da5d019a77bf96d473b960"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:b640981bf64a3e978a56167594a0e97db71c89a479da8e175d8bb5be5178c003"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e3114da6d7f95d2dee7d3f4eec16dacff819740bbab931aff8648cb13c5ff5e7"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d5ec85080cce7b0513cfd233914eb8b7bbd0633f1d1703aa28d1dd5a72f678ec"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7a698cb1dac82c35fcf8fe3417a3aaba97de16a01ac914b89a0889d364d2f6be"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:37a138589b12069efb424220bf78eac59ca68b95696fc622b6ccc1c0a197204a"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:68a2dec79deebc5d26d617bfdf6e8aab065a4f34934b22d3b5010df3ba36612c"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:09616eeaef406f99046553b8a40fbf8b1e70795a91885ba4c96a70793de5504a"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48a0476626da912a44cc078f9893f292f0b3e4c739caf289268168d8f4702a39"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c7f3201ec47d5207841402594f1d7950879ef890c0c495052fa62f58283fde1a"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c5ca78485a255e03c32b513f8c2bc39fedb7f5c5f8535545bdc223a03b24f248"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6c391c021ab1f7a82da5d8d0b3cee2f4b2c455ec86c8aebbc84837a631ff309"}, + {file = "cryptography-41.0.7.tar.gz", hash = "sha256:13f93ce9bea8016c253b34afc6bd6a75993e5c40672ed5405a9c832f0d4a00bc"}, +] + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] +nox = ["nox"] +pep8test = ["black", "check-sdist", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + [[package]] name = "distlib" version = "0.3.8" @@ -1248,13 +1357,13 @@ files = [ [[package]] name = "platformdirs" -version = "4.1.0" +version = "3.11.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false -python-versions = ">=3.8" +python-versions = ">=3.7" files = [ - {file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"}, - {file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"}, + {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"}, + {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"}, ] [package.extras] @@ -1361,6 +1470,17 @@ files = [ {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, ] +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] + [[package]] name = "pydantic" version = "2.5.3" @@ -1538,6 +1658,41 @@ files = [ plugins = ["importlib-metadata"] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pyjwt" +version = "2.8.0" +description = "JSON Web Token implementation in Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"}, + {file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"}, +] + +[package.extras] +crypto = ["cryptography (>=3.4.0)"] +dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] +docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] + +[[package]] +name = "pyopenssl" +version = "23.3.0" +description = "Python wrapper module around the OpenSSL library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyOpenSSL-23.3.0-py3-none-any.whl", hash = "sha256:6756834481d9ed5470f4a9393455154bc92fe7a64b7bc6ee2c804e78c52099b2"}, + {file = "pyOpenSSL-23.3.0.tar.gz", hash = "sha256:6b2cba5cc46e822750ec3e5a81ee12819850b11303630d575e98108a079c2b12"}, +] + +[package.dependencies] +cryptography = ">=41.0.5,<42" + +[package.extras] +docs = ["sphinx (!=5.2.0,!=5.2.0.post0,!=7.2.5)", "sphinx-rtd-theme"] +test = ["flaky", "pretend", "pytest (>=3.0.1)"] + [[package]] name = "pytest" version = "7.4.4" @@ -1819,6 +1974,71 @@ files = [ {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, ] +[[package]] +name = "snowflake-connector-python" +version = "3.6.0" +description = "Snowflake Connector for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "snowflake-connector-python-3.6.0.tar.gz", hash = "sha256:15667a918780d79da755e6a60bbf6918051854951e8f56ccdf5692283e9a8479"}, + {file = "snowflake_connector_python-3.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4093b38cf9abf95c38119f0b23b07e23dc7a8689b956cd5d34975e1875741f20"}, + {file = "snowflake_connector_python-3.6.0-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:cf5a964fe01b177063f8c44d14df3a72715580bcd195788ec2822090f37330a5"}, + {file = "snowflake_connector_python-3.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55a6418cec585b050e6f05404f25e62b075a3bbea587dc1f903de15640565c58"}, + {file = "snowflake_connector_python-3.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7c76aea92b87f6ecd604e9c934aac8a779f2e20f3be1d990d53bb5b6d87b009"}, + {file = "snowflake_connector_python-3.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:9dfcf178271e892e64e4092b9e011239a066ce5de848afd2efe3f13197a9f8b3"}, + {file = "snowflake_connector_python-3.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4916f9b4a0efd7c96d1fa50a157e05907b6935f91492cca7f200b43cc178a25e"}, + {file = "snowflake_connector_python-3.6.0-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:f15024c66db5e87d359216ec733a2974d7562aa38f3f18c8b6e65489839e00d7"}, + {file = "snowflake_connector_python-3.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcbd3102f807ebbbae52b1b5683d45cd7b3dcb0eaec131233ba6b156e8d70fa4"}, + {file = "snowflake_connector_python-3.6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7662e2de25b885abe08ab866cf7c7b026ad1af9faa39c25e2c25015ef807abe3"}, + {file = "snowflake_connector_python-3.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:d1fa102f55ee166cc766aeee3f9333b17b4bede6fb088eee1e1f022df15b6d81"}, + {file = "snowflake_connector_python-3.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fde1e0727e2f23c2a07b49b30e1bc0f49977f965d08ddfda10015b24a2beeb76"}, + {file = "snowflake_connector_python-3.6.0-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:1b51fe000c8cf6372d30b73c7136275e52788e6af47010cd1984c9fb03378e86"}, + {file = "snowflake_connector_python-3.6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7a11699689a19916e65794ce58dca72b8a40fe6a7eea06764931ede10b47bcc"}, + {file = "snowflake_connector_python-3.6.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d810be5b180c6f47ce9b6f989fe64b9984383e4b77e30b284a83e33f229a3a82"}, + {file = "snowflake_connector_python-3.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:b5db47d4164d6b7a07c413a46f9edc4a1d687e3df44fd9d5fa89a89aecb94a8e"}, + {file = "snowflake_connector_python-3.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bf8c1ad5aab5304fefa2a4178061a24c96da45e3e3db9d901621e9953e005402"}, + {file = "snowflake_connector_python-3.6.0-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:1058ab5c98cc62fde8b3f021f0a5076cb7865b5cdab8a9bccde0df88b9e91334"}, + {file = "snowflake_connector_python-3.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b93f55989f80d69278e0f40a7a1c0e737806b7c0ddb0351513a752b837243e8"}, + {file = "snowflake_connector_python-3.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50dd954ea5918d3242ded69225b72f701963cd9c043ee7d9ab35dc22211611c8"}, + {file = "snowflake_connector_python-3.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:4ad42613b87f31441d07a8ea242f4c28ed5eb7b6e05986f9e94a7e44b96d3d1e"}, +] + +[package.dependencies] +asn1crypto = ">0.24.0,<2.0.0" +certifi = ">=2017.4.17" +cffi = ">=1.9,<2.0.0" +charset-normalizer = ">=2,<4" +cryptography = ">=3.1.0,<42.0.0" +filelock = ">=3.5,<4" +idna = ">=2.5,<4" +packaging = "*" +platformdirs = ">=2.6.0,<4.0.0" +pyjwt = "<3.0.0" +pyOpenSSL = ">=16.2.0,<24.0.0" +pytz = "*" +requests = "<3.0.0" +sortedcontainers = ">=2.4.0" +tomlkit = "*" +typing-extensions = ">=4.3,<5" +urllib3 = {version = ">=1.21.1,<2.0.0", markers = "python_version < \"3.10\""} + +[package.extras] +development = ["Cython", "coverage", "more-itertools", "numpy (<1.27.0)", "pendulum (!=2.1.1)", "pexpect", "pytest (<7.5.0)", "pytest-cov", "pytest-rerunfailures", "pytest-timeout", "pytest-xdist", "pytzdata"] +pandas = ["pandas (>=1.0.0,<2.2.0)", "pyarrow"] +secure-local-storage = ["keyring (!=16.1.0,<25.0.0)"] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +optional = false +python-versions = "*" +files = [ + {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, + {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, +] + [[package]] name = "soupsieve" version = "2.5" @@ -2042,6 +2262,17 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +[[package]] +name = "tomlkit" +version = "0.12.3" +description = "Style preserving TOML library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomlkit-0.12.3-py3-none-any.whl", hash = "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba"}, + {file = "tomlkit-0.12.3.tar.gz", hash = "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4"}, +] + [[package]] name = "typing-extensions" version = "4.9.0" @@ -2097,23 +2328,6 @@ brotli = ["brotli (==1.0.9)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotl secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] -[[package]] -name = "urllib3" -version = "2.0.7" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.7" -files = [ - {file = "urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"}, - {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"}, -] - -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] - [[package]] name = "virtualenv" version = "20.25.0" @@ -2153,8 +2367,9 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p bigquery = ["google-cloud-bigquery"] clickhouse = ["clickhouse-driver"] redshift = ["boto3", "redshift-connector"] +snowflake = ["snowflake-connector-python"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "738e53e7c931645583460e68f4a369952c5d5b29c80773bc6298d354fefdba0d" +content-hash = "fd148fa065ad96cae78f4e3253e16a0884c084379fdadfd91f6a6c014ee5e3a2" diff --git a/pyproject.toml b/pyproject.toml index f640d11..29deebd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,10 +52,14 @@ google-cloud-bigquery = {version ="^3.11.4", optional = true} boto3 = "1.34.14" # Pin version to resolve dependencies faster redshift-connector = "^2.0.918" +# Snowflake specific +snowflake-connector-python = "^3.6.0" + [tool.poetry.extras] bigquery = ["google-cloud-bigquery"] clickhouse = ["clickhouse-driver"] redshift = ["redshift-connector", "boto3"] +snowflake = ["snowflake-connector-python"] [tool.poetry.dev-dependencies] pytest = "^7.2" diff --git a/src/sql_mock/snowflake/__init__.py b/src/sql_mock/snowflake/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/sql_mock/snowflake/column_mocks.py b/src/sql_mock/snowflake/column_mocks.py new file mode 100644 index 0000000..6aaca0d --- /dev/null +++ b/src/sql_mock/snowflake/column_mocks.py @@ -0,0 +1,76 @@ +from sql_mock.column_mocks import ColumnMock + + +class SnowflakeColumnMock(ColumnMock): + pass + + +class INTEGER(SnowflakeColumnMock): + dtype = "INTEGER" + + +class FLOAT(SnowflakeColumnMock): + dtype = "FLOAT" + + +class BOOLEAN(SnowflakeColumnMock): + dtype = "BOOLEAN" + + +class STRING(SnowflakeColumnMock): + dtype = "STRING" + + +class TEXT(SnowflakeColumnMock): + dtype = "TEXT" + + +class BINARY(SnowflakeColumnMock): + dtype = "BINARY" + + +class DATE(SnowflakeColumnMock): + dtype = "DATE" + + +class TIME(SnowflakeColumnMock): + dtype = "TIME" + + +class TIMESTAMP(SnowflakeColumnMock): + dtype = "TIMESTAMP" + + +class TIMESTAMP_LTZ(SnowflakeColumnMock): + dtype = "TIMESTAMP_LTZ" + + +class TIMESTAMP_NTZ(SnowflakeColumnMock): + dtype = "TIMESTAMP_NTZ" + + +class TIMESTAMP_TZ(SnowflakeColumnMock): + dtype = "TIMESTAMP_TZ" + + +class VARIANT(SnowflakeColumnMock): + dtype = "VARIANT" + + +class OBJECT(SnowflakeColumnMock): + dtype = "OBJECT" + + +class ARRAY(SnowflakeColumnMock): + dtype = "ARRAY" + use_quotes_for_casting = False + + +class GEOGRAPHY(SnowflakeColumnMock): + dtype = "GEOGRAPHY" + + +class DECIMAL(SnowflakeColumnMock): + def __init__(self, default, precision, scale, nullable=False) -> None: + self.dtype = f"DECIMAL({precision}, {scale})" + super().__init__(default, nullable) diff --git a/src/sql_mock/snowflake/settings.py b/src/sql_mock/snowflake/settings.py new file mode 100644 index 0000000..06a13c8 --- /dev/null +++ b/src/sql_mock/snowflake/settings.py @@ -0,0 +1,8 @@ +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class SnowflakeSettings(BaseSettings): + model_config = SettingsConfigDict(env_prefix="SQL_MOCK_SNOWFLAKE_") + account: str + user: str + password: str diff --git a/src/sql_mock/snowflake/table_mocks.py b/src/sql_mock/snowflake/table_mocks.py new file mode 100644 index 0000000..12cc29c --- /dev/null +++ b/src/sql_mock/snowflake/table_mocks.py @@ -0,0 +1,26 @@ +from snowflake.connector import DictCursor, connect + +from sql_mock.snowflake.settings import SnowflakeSettings +from sql_mock.table_mocks import BaseMockTable + + +class SnowflakeMockTable(BaseMockTable): + _sql_dialect = "snowflake" + + def __init__( + self, + *args, + **kwargs, + ): + self.settings = SnowflakeSettings() + super().__init__(*args, **kwargs) + + def _get_results(self, query: str) -> list[dict]: + with connect( + user=self.settings.user, + password=self.settings.password, + account=self.settings.account, + ) as conn: + with conn.cursor(DictCursor) as cur: + cur.execute(query) + return cur.fetchall() diff --git a/tests/sql_mock/snowflake/__init__.py b/tests/sql_mock/snowflake/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/sql_mock/snowflake/test_column_mocks.py b/tests/sql_mock/snowflake/test_column_mocks.py new file mode 100644 index 0000000..23e69dd --- /dev/null +++ b/tests/sql_mock/snowflake/test_column_mocks.py @@ -0,0 +1,47 @@ +from sql_mock.snowflake.column_mocks import DECIMAL, SnowflakeColumnMock + + +def test_init_not_nullable(): + """ + ...then nullable should be False and dtype be the same as passed. + """ + + class ColMock(SnowflakeColumnMock): + dtype = "INTEGER" + + column = ColMock(default=42) + + assert column.default == 42 + assert column.dtype == "INTEGER" + assert not column.nullable + + +def test_init_nullable(): + """ + ...then nullable should be True" + """ + + class ColMock(SnowflakeColumnMock): + dtype = "INTEGER" + + column = ColMock(default=42, nullable=True) + + assert column.default == 42 + assert column.dtype == "INTEGER" + assert column.nullable + + +class TestDecimalColumn: + def test_decimal_initialization_not_nullable(self): + """Ensure that the Decimal object is initialized correctly.""" + decimal_col = DECIMAL(default=0.0, precision=10, scale=2, nullable=False) + assert decimal_col.dtype == "DECIMAL(10, 2)" + assert decimal_col.default == 0.0 + assert not decimal_col.nullable + + def test_decimal_initialization_nullable(self): + """Ensure that the Decimal object is initialized correctly.""" + decimal_col = DECIMAL(default=None, precision=10, scale=2, nullable=True) + assert decimal_col.dtype == "DECIMAL(10, 2)" + assert decimal_col.default is None + assert decimal_col.nullable diff --git a/tests/sql_mock/snowflake/test_table_mocks.py b/tests/sql_mock/snowflake/test_table_mocks.py new file mode 100644 index 0000000..dededd9 --- /dev/null +++ b/tests/sql_mock/snowflake/test_table_mocks.py @@ -0,0 +1,68 @@ +import os + +import pytest +from pydantic import ValidationError + +from sql_mock.snowflake.column_mocks import INTEGER +from sql_mock.snowflake.table_mocks import SnowflakeMockTable +from sql_mock.table_mocks import table_meta + + +@table_meta(table_ref="mock_test_table") +class MockTestTable(SnowflakeMockTable): + id = INTEGER(default=1) + + +@pytest.fixture(autouse=True) +def patch_os_environment_variables(mocker): + mocker.patch.dict( + os.environ, + { + "SQL_MOCK_SNOWFLAKE_ACCOUNT": "account", + "SQL_MOCK_SNOWFLAKE_USER": "user", + "SQL_MOCK_SNOWFLAKE_PASSWORD": "password", + }, + clear=True, + ) + + +def test_init_with_environment_variables(mocker): + """...then the env vars should be used to set the attributes""" + table = MockTestTable() + assert table.settings.account == "account" + assert table.settings.user == "user" + assert table.settings.password == "password" + + +def test_init_with_missing_configs(mocker): + """...then it should raise an error""" + mocker.patch.dict( + os.environ, + {}, + clear=True, + ) + with pytest.raises(ValidationError): + MockTestTable() + + +def test_get_results(mocker): + """Test the _get_results method.""" + # Create a mock query job result + mock_query_job_result = [ + {"column1": "value1", "column2": "value2"}, + {"column1": "value3", "column2": "value4"} + # Add more rows as needed + ] + query = "SELECT 1, 2" + # Mock the Snowflake connector + mock_connect = mocker.patch("sql_mock.snowflake.table_mocks.connect") + mock_cursor = mock_connect.return_value.__enter__.return_value.cursor + mock_execute = mock_cursor.return_value.__enter__.return_value.execute + mock_fetchall = mock_cursor.return_value.__enter__.return_value.fetchall + mock_fetchall.return_value = mock_query_job_result + + instance = SnowflakeMockTable() + result = instance._get_results(query=query) + + assert result == mock_query_job_result + mock_execute.assert_called_once_with(query)