snowflake-sqlalchemy 1.6.1__tar.gz → 1.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/.pre-commit-config.yaml +1 -0
  2. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/DESCRIPTION.md +13 -2
  3. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/PKG-INFO +4 -4
  4. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/README.md +1 -1
  5. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/pyproject.toml +5 -0
  6. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/src/snowflake/sqlalchemy/__init__.py +52 -6
  7. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/src/snowflake/sqlalchemy/_constants.py +2 -0
  8. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/src/snowflake/sqlalchemy/base.py +58 -9
  9. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/src/snowflake/sqlalchemy/custom_types.py +20 -0
  10. snowflake_sqlalchemy-1.7.0/src/snowflake/sqlalchemy/exc.py +82 -0
  11. snowflake_sqlalchemy-1.7.0/src/snowflake/sqlalchemy/parser/custom_type_parser.py +190 -0
  12. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/src/snowflake/sqlalchemy/snowdialect.py +212 -142
  13. snowflake_sqlalchemy-1.7.0/src/snowflake/sqlalchemy/sql/custom_schema/__init__.py +9 -0
  14. snowflake_sqlalchemy-1.7.0/src/snowflake/sqlalchemy/sql/custom_schema/clustered_table.py +37 -0
  15. snowflake_sqlalchemy-1.7.0/src/snowflake/sqlalchemy/sql/custom_schema/custom_table_base.py +127 -0
  16. snowflake_sqlalchemy-1.7.0/src/snowflake/sqlalchemy/sql/custom_schema/custom_table_prefix.py +13 -0
  17. snowflake_sqlalchemy-1.7.0/src/snowflake/sqlalchemy/sql/custom_schema/dynamic_table.py +117 -0
  18. snowflake_sqlalchemy-1.7.0/src/snowflake/sqlalchemy/sql/custom_schema/hybrid_table.py +62 -0
  19. snowflake_sqlalchemy-1.7.0/src/snowflake/sqlalchemy/sql/custom_schema/iceberg_table.py +102 -0
  20. snowflake_sqlalchemy-1.7.0/src/snowflake/sqlalchemy/sql/custom_schema/options/__init__.py +33 -0
  21. snowflake_sqlalchemy-1.7.0/src/snowflake/sqlalchemy/sql/custom_schema/options/as_query_option.py +63 -0
  22. snowflake_sqlalchemy-1.7.0/src/snowflake/sqlalchemy/sql/custom_schema/options/cluster_by_option.py +58 -0
  23. snowflake_sqlalchemy-1.7.0/src/snowflake/sqlalchemy/sql/custom_schema/options/identifier_option.py +63 -0
  24. snowflake_sqlalchemy-1.7.0/src/snowflake/sqlalchemy/sql/custom_schema/options/invalid_table_option.py +25 -0
  25. snowflake_sqlalchemy-1.7.0/src/snowflake/sqlalchemy/sql/custom_schema/options/keyword_option.py +65 -0
  26. snowflake_sqlalchemy-1.7.0/src/snowflake/sqlalchemy/sql/custom_schema/options/keywords.py +14 -0
  27. snowflake_sqlalchemy-1.7.0/src/snowflake/sqlalchemy/sql/custom_schema/options/literal_option.py +67 -0
  28. snowflake_sqlalchemy-1.7.0/src/snowflake/sqlalchemy/sql/custom_schema/options/table_option.py +84 -0
  29. snowflake_sqlalchemy-1.7.0/src/snowflake/sqlalchemy/sql/custom_schema/options/target_lag_option.py +94 -0
  30. snowflake_sqlalchemy-1.7.0/src/snowflake/sqlalchemy/sql/custom_schema/snowflake_table.py +70 -0
  31. snowflake_sqlalchemy-1.7.0/src/snowflake/sqlalchemy/sql/custom_schema/table_from_query.py +54 -0
  32. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/src/snowflake/sqlalchemy/version.py +1 -1
  33. snowflake_sqlalchemy-1.7.0/tests/__snapshots__/test_compile_dynamic_table.ambr +13 -0
  34. snowflake_sqlalchemy-1.7.0/tests/__snapshots__/test_core.ambr +4 -0
  35. snowflake_sqlalchemy-1.7.0/tests/__snapshots__/test_orm.ambr +4 -0
  36. snowflake_sqlalchemy-1.7.0/tests/__snapshots__/test_reflect_dynamic_table.ambr +4 -0
  37. snowflake_sqlalchemy-1.7.0/tests/__snapshots__/test_structured_datatypes.ambr +90 -0
  38. snowflake_sqlalchemy-1.7.0/tests/__snapshots__/test_unit_structured_types.ambr +4 -0
  39. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/tests/conftest.py +30 -0
  40. snowflake_sqlalchemy-1.7.0/tests/custom_tables/__init__.py +2 -0
  41. snowflake_sqlalchemy-1.7.0/tests/custom_tables/__snapshots__/test_compile_dynamic_table.ambr +40 -0
  42. snowflake_sqlalchemy-1.7.0/tests/custom_tables/__snapshots__/test_compile_hybrid_table.ambr +7 -0
  43. snowflake_sqlalchemy-1.7.0/tests/custom_tables/__snapshots__/test_compile_iceberg_table.ambr +19 -0
  44. snowflake_sqlalchemy-1.7.0/tests/custom_tables/__snapshots__/test_compile_snowflake_table.ambr +35 -0
  45. snowflake_sqlalchemy-1.7.0/tests/custom_tables/__snapshots__/test_create_dynamic_table.ambr +7 -0
  46. snowflake_sqlalchemy-1.7.0/tests/custom_tables/__snapshots__/test_create_hybrid_table.ambr +7 -0
  47. snowflake_sqlalchemy-1.7.0/tests/custom_tables/__snapshots__/test_create_iceberg_table.ambr +14 -0
  48. snowflake_sqlalchemy-1.7.0/tests/custom_tables/__snapshots__/test_create_snowflake_table.ambr +4 -0
  49. snowflake_sqlalchemy-1.7.0/tests/custom_tables/__snapshots__/test_generic_options.ambr +13 -0
  50. snowflake_sqlalchemy-1.7.0/tests/custom_tables/__snapshots__/test_reflect_hybrid_table.ambr +4 -0
  51. snowflake_sqlalchemy-1.7.0/tests/custom_tables/__snapshots__/test_reflect_snowflake_table.ambr +29 -0
  52. snowflake_sqlalchemy-1.7.0/tests/custom_tables/test_compile_dynamic_table.py +271 -0
  53. snowflake_sqlalchemy-1.7.0/tests/custom_tables/test_compile_hybrid_table.py +52 -0
  54. snowflake_sqlalchemy-1.7.0/tests/custom_tables/test_compile_iceberg_table.py +116 -0
  55. snowflake_sqlalchemy-1.7.0/tests/custom_tables/test_compile_snowflake_table.py +180 -0
  56. snowflake_sqlalchemy-1.7.0/tests/custom_tables/test_create_dynamic_table.py +124 -0
  57. snowflake_sqlalchemy-1.7.0/tests/custom_tables/test_create_hybrid_table.py +95 -0
  58. snowflake_sqlalchemy-1.7.0/tests/custom_tables/test_create_iceberg_table.py +43 -0
  59. snowflake_sqlalchemy-1.7.0/tests/custom_tables/test_create_snowflake_table.py +66 -0
  60. snowflake_sqlalchemy-1.7.0/tests/custom_tables/test_generic_options.py +83 -0
  61. snowflake_sqlalchemy-1.7.0/tests/custom_tables/test_reflect_dynamic_table.py +88 -0
  62. snowflake_sqlalchemy-1.7.0/tests/custom_tables/test_reflect_hybrid_table.py +65 -0
  63. snowflake_sqlalchemy-1.7.0/tests/custom_tables/test_reflect_snowflake_table.py +92 -0
  64. snowflake_sqlalchemy-1.7.0/tests/sqlalchemy_test_suite/__init__.py +3 -0
  65. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/tests/test_core.py +89 -89
  66. snowflake_sqlalchemy-1.7.0/tests/test_custom_types.py +67 -0
  67. snowflake_sqlalchemy-1.7.0/tests/test_index_reflection.py +34 -0
  68. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/tests/test_orm.py +178 -9
  69. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/tests/test_pandas.py +1 -1
  70. snowflake_sqlalchemy-1.7.0/tests/test_structured_datatypes.py +271 -0
  71. snowflake_sqlalchemy-1.7.0/tests/test_unit_structured_types.py +73 -0
  72. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/tests/util.py +2 -0
  73. snowflake_sqlalchemy-1.6.1/tests/test_custom_types.py +0 -36
  74. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/.gitignore +0 -0
  75. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/.gitmodules +0 -0
  76. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/LICENSE.txt +0 -0
  77. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/MANIFEST.in +0 -0
  78. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/ci/build.sh +0 -0
  79. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/ci/build_docker.sh +0 -0
  80. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/ci/docker/sqlalchemy_build/Dockerfile +0 -0
  81. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/ci/docker/sqlalchemy_build/scripts/entrypoint.sh +0 -0
  82. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/ci/set_base_image.sh +0 -0
  83. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/ci/test.sh +0 -0
  84. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/ci/test_docker.sh +0 -0
  85. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/ci/test_linux.sh +0 -0
  86. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/license_header.txt +0 -0
  87. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/setup.cfg +0 -0
  88. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/snyk/requirements.txt +0 -0
  89. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/snyk/requiremtnts.txt +0 -0
  90. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/snyk/update_requirements.py +0 -0
  91. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/src/snowflake/sqlalchemy/compat.py +0 -0
  92. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/src/snowflake/sqlalchemy/custom_commands.py +0 -0
  93. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/src/snowflake/sqlalchemy/functions.py +0 -0
  94. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/src/snowflake/sqlalchemy/provision.py +0 -0
  95. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/src/snowflake/sqlalchemy/requirements.py +0 -0
  96. {snowflake_sqlalchemy-1.6.1/tests → snowflake_sqlalchemy-1.7.0/src/snowflake/sqlalchemy/sql}/__init__.py +0 -0
  97. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/src/snowflake/sqlalchemy/util.py +0 -0
  98. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/tested_requirements/requirements_310.reqs +0 -0
  99. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/tested_requirements/requirements_37.reqs +0 -0
  100. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/tested_requirements/requirements_38.reqs +0 -0
  101. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/tested_requirements/requirements_39.reqs +0 -0
  102. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/tests/README.rst +0 -0
  103. {snowflake_sqlalchemy-1.6.1/tests/sqlalchemy_test_suite → snowflake_sqlalchemy-1.7.0/tests}/__init__.py +0 -0
  104. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/tests/data/users.txt +0 -0
  105. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/tests/sqlalchemy_test_suite/README.md +0 -0
  106. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/tests/sqlalchemy_test_suite/conftest.py +0 -0
  107. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/tests/sqlalchemy_test_suite/test_suite.py +0 -0
  108. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/tests/sqlalchemy_test_suite/test_suite_20.py +0 -0
  109. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/tests/test_compiler.py +0 -0
  110. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/tests/test_copy.py +0 -0
  111. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/tests/test_create.py +0 -0
  112. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/tests/test_custom_functions.py +0 -0
  113. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/tests/test_geography.py +0 -0
  114. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/tests/test_geometry.py +0 -0
  115. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/tests/test_multivalues_insert.py +0 -0
  116. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/tests/test_qmark.py +0 -0
  117. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/tests/test_quote.py +0 -0
  118. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/tests/test_semi_structured_datatypes.py +0 -0
  119. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/tests/test_sequence.py +0 -0
  120. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/tests/test_timestamp.py +0 -0
  121. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/tests/test_unit_core.py +0 -0
  122. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/tests/test_unit_cte.py +0 -0
  123. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/tests/test_unit_types.py +0 -0
  124. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/tests/test_unit_url.py +0 -0
  125. {snowflake_sqlalchemy-1.6.1 → snowflake_sqlalchemy-1.7.0}/tox.ini +0 -0
@@ -4,6 +4,7 @@ repos:
4
4
  rev: v4.5.0
5
5
  hooks:
6
6
  - id: trailing-whitespace
7
+ exclude: '\.ambr$'
7
8
  - id: end-of-file-fixer
8
9
  - id: check-yaml
9
10
  exclude: .github/repo_meta.yaml
@@ -9,6 +9,17 @@ Source code is also available at:
9
9
 
10
10
  # Release Notes
11
11
 
12
+ - v1.7.0(November 22, 2024)
13
+
14
+ - Add support for dynamic tables and required options
15
+ - Add support for hybrid tables
16
+ - Fixed SAWarning when registering functions with existing name in default namespace
17
+ - Update options to be defined in key arguments instead of arguments.
18
+ - Add support for refresh_mode option in DynamicTable
19
+ - Add support for iceberg table with Snowflake Catalog
20
+ - Fix cluster by option to support explicit expressions
21
+ - Add support for MAP datatype
22
+
12
23
  - v1.6.1(July 9, 2024)
13
24
 
14
25
  - Update internal project workflow with pypi publishing
@@ -24,7 +35,7 @@ Source code is also available at:
24
35
 
25
36
  - v1.5.3(April 16, 2024)
26
37
 
27
- - Limit SQLAlchemy to < 2.0.0 before releasing version compatible with 2.0
38
+ - Limit SQLAlchemy to < 2.0.0 before releasing version compatible with 2.0
28
39
 
29
40
  - v1.5.2(April 11, 2024)
30
41
 
@@ -33,7 +44,7 @@ Source code is also available at:
33
44
 
34
45
  - v1.5.1(November 03, 2023)
35
46
 
36
- - Fixed a compatibility issue with Snowflake Behavioral Change 1057 on outer lateral join, for more details check https://docs.snowflake.com/en/release-notes/bcr-bundles/2023_04/bcr-1057.
47
+ - Fixed a compatibility issue with Snowflake Behavioral Change 1057 on outer lateral join, for more details check <https://docs.snowflake.com/en/release-notes/bcr-bundles/2023_04/bcr-1057>.
37
48
  - Fixed credentials with `externalbrowser` authentication not caching due to incorrect parsing of boolean query parameters.
38
49
  - This fixes other boolean parameter passing to driver as well.
39
50
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: snowflake-sqlalchemy
3
- Version: 1.6.1
3
+ Version: 1.7.0
4
4
  Summary: Snowflake SQLAlchemy Dialect
5
5
  Project-URL: Changelog, https://github.com/snowflakedb/snowflake-sqlalchemy/blob/main/DESCRIPTION.md
6
6
  Project-URL: Documentation, https://docs.snowflake.com/en/user-guide/sqlalchemy.html
@@ -8,8 +8,7 @@ Project-URL: Homepage, https://www.snowflake.com/
8
8
  Project-URL: Issues, https://github.com/snowflakedb/snowflake-sqlalchemy/issues
9
9
  Project-URL: Source, https://github.com/snowflakedb/snowflake-sqlalchemy
10
10
  Author-email: "Snowflake Inc." <triage-snowpark-python-api-dl@snowflake.com>
11
- License-Expression: Apache-2.0
12
- License-File: LICENSE.txt
11
+ License: Apache-2.0
13
12
  Keywords: Snowflake,analytics,cloud,database,db,warehouse
14
13
  Classifier: Development Status :: 5 - Production/Stable
15
14
  Classifier: Environment :: Console
@@ -46,6 +45,7 @@ Requires-Dist: pytest-cov; extra == 'development'
46
45
  Requires-Dist: pytest-rerunfailures; extra == 'development'
47
46
  Requires-Dist: pytest-timeout; extra == 'development'
48
47
  Requires-Dist: pytz; extra == 'development'
48
+ Requires-Dist: syrupy==4.6.1; extra == 'development'
49
49
  Provides-Extra: pandas
50
50
  Requires-Dist: snowflake-connector-python[pandas]; extra == 'pandas'
51
51
  Description-Content-Type: text/markdown
@@ -392,7 +392,7 @@ This example shows how to create a table with two columns, `id` and `name`, as t
392
392
  t = Table('myuser', metadata,
393
393
  Column('id', Integer, primary_key=True),
394
394
  Column('name', String),
395
- snowflake_clusterby=['id', 'name'], ...
395
+ snowflake_clusterby=['id', 'name', text('id > 5')], ...
396
396
  )
397
397
  metadata.create_all(engine)
398
398
  ```
@@ -340,7 +340,7 @@ This example shows how to create a table with two columns, `id` and `name`, as t
340
340
  t = Table('myuser', metadata,
341
341
  Column('id', Integer, primary_key=True),
342
342
  Column('name', String),
343
- snowflake_clusterby=['id', 'name'], ...
343
+ snowflake_clusterby=['id', 'name', text('id > 5')], ...
344
344
  )
345
345
  metadata.create_all(engine)
346
346
  ```
@@ -53,6 +53,7 @@ development = [
53
53
  "pytz",
54
54
  "numpy",
55
55
  "mock",
56
+ "syrupy==4.6.1",
56
57
  ]
57
58
  pandas = ["snowflake-connector-python[pandas]"]
58
59
 
@@ -91,6 +92,7 @@ SQLACHEMY_WARN_20 = "1"
91
92
  check = "pre-commit run --all-files"
92
93
  test-dialect = "pytest -ra -vvv --tb=short --cov snowflake.sqlalchemy --cov-append --junitxml ./junit.xml --ignore=tests/sqlalchemy_test_suite tests/"
93
94
  test-dialect-compatibility = "pytest -ra -vvv --tb=short --cov snowflake.sqlalchemy --cov-append --junitxml ./junit.xml tests/sqlalchemy_test_suite"
95
+ test-dialect-aws = "pytest -m \"aws\" -ra -vvv --tb=short --cov snowflake.sqlalchemy --cov-append --junitxml ./junit.xml --ignore=tests/sqlalchemy_test_suite tests/"
94
96
  gh-cache-sum = "python -VV | sha256sum | cut -d' ' -f1"
95
97
  check-import = "python -c 'import snowflake.sqlalchemy; print(snowflake.sqlalchemy.__version__)'"
96
98
 
@@ -109,6 +111,7 @@ line-length = 88
109
111
  line-length = 88
110
112
 
111
113
  [tool.pytest.ini_options]
114
+ addopts = "-m 'not feature_max_lob_size and not aws and not requires_external_volume'"
112
115
  markers = [
113
116
  # Optional dependency groups markers
114
117
  "lambda: AWS lambda tests",
@@ -125,5 +128,7 @@ markers = [
125
128
  # Other markers
126
129
  "timeout: tests that need a timeout time",
127
130
  "internal: tests that could but should only run on our internal CI",
131
+ "requires_external_volume: tests that needs a external volume to be executed",
128
132
  "external: tests that could but should only run on our external CI",
133
+ "feature_max_lob_size: tests that could but should only run on our external CI",
129
134
  ]
@@ -9,7 +9,7 @@ if sys.version_info < (3, 8):
9
9
  else:
10
10
  import importlib.metadata as importlib_metadata
11
11
 
12
- from sqlalchemy.types import (
12
+ from sqlalchemy.types import ( # noqa
13
13
  BIGINT,
14
14
  BINARY,
15
15
  BOOLEAN,
@@ -27,8 +27,8 @@ from sqlalchemy.types import (
27
27
  VARCHAR,
28
28
  )
29
29
 
30
- from . import base, snowdialect
31
- from .custom_commands import (
30
+ from . import base, snowdialect # noqa
31
+ from .custom_commands import ( # noqa
32
32
  AWSBucket,
33
33
  AzureContainer,
34
34
  CopyFormatter,
@@ -41,7 +41,7 @@ from .custom_commands import (
41
41
  MergeInto,
42
42
  PARQUETFormatter,
43
43
  )
44
- from .custom_types import (
44
+ from .custom_types import ( # noqa
45
45
  ARRAY,
46
46
  BYTEINT,
47
47
  CHARACTER,
@@ -50,6 +50,7 @@ from .custom_types import (
50
50
  FIXED,
51
51
  GEOGRAPHY,
52
52
  GEOMETRY,
53
+ MAP,
53
54
  NUMBER,
54
55
  OBJECT,
55
56
  STRING,
@@ -61,13 +62,30 @@ from .custom_types import (
61
62
  VARBINARY,
62
63
  VARIANT,
63
64
  )
64
- from .util import _url as URL
65
+ from .sql.custom_schema import ( # noqa
66
+ DynamicTable,
67
+ HybridTable,
68
+ IcebergTable,
69
+ SnowflakeTable,
70
+ )
71
+ from .sql.custom_schema.options import ( # noqa
72
+ AsQueryOption,
73
+ ClusterByOption,
74
+ IdentifierOption,
75
+ KeywordOption,
76
+ LiteralOption,
77
+ SnowflakeKeyword,
78
+ TableOptionKey,
79
+ TargetLagOption,
80
+ TimeUnit,
81
+ )
82
+ from .util import _url as URL # noqa
65
83
 
66
84
  base.dialect = dialect = snowdialect.dialect
67
85
 
68
86
  __version__ = importlib_metadata.version("snowflake-sqlalchemy")
69
87
 
70
- __all__ = (
88
+ _custom_types = (
71
89
  "BIGINT",
72
90
  "BINARY",
73
91
  "BOOLEAN",
@@ -102,6 +120,10 @@ __all__ = (
102
120
  "TINYINT",
103
121
  "VARBINARY",
104
122
  "VARIANT",
123
+ "MAP",
124
+ )
125
+
126
+ _custom_commands = (
105
127
  "MergeInto",
106
128
  "CSVFormatter",
107
129
  "JSONFormatter",
@@ -114,3 +136,27 @@ __all__ = (
114
136
  "CreateStage",
115
137
  "CreateFileFormat",
116
138
  )
139
+
140
+ _custom_tables = ("HybridTable", "DynamicTable", "IcebergTable", "SnowflakeTable")
141
+
142
+ _custom_table_options = (
143
+ "AsQueryOption",
144
+ "TargetLagOption",
145
+ "LiteralOption",
146
+ "IdentifierOption",
147
+ "KeywordOption",
148
+ "ClusterByOption",
149
+ )
150
+
151
+ _enums = (
152
+ "TimeUnit",
153
+ "TableOptionKey",
154
+ "SnowflakeKeyword",
155
+ )
156
+ __all__ = (
157
+ *_custom_types,
158
+ *_custom_commands,
159
+ *_custom_tables,
160
+ *_custom_table_options,
161
+ *_enums,
162
+ )
@@ -10,3 +10,5 @@ PARAM_INTERNAL_APPLICATION_VERSION = "internal_application_version"
10
10
 
11
11
  APPLICATION_NAME = "SnowflakeSQLAlchemy"
12
12
  SNOWFLAKE_SQLALCHEMY_VERSION = VERSION
13
+ DIALECT_NAME = "snowflake"
14
+ NOT_NULL = "NOT NULL"
@@ -5,6 +5,7 @@
5
5
  import itertools
6
6
  import operator
7
7
  import re
8
+ from typing import List
8
9
 
9
10
  from sqlalchemy import exc as sa_exc
10
11
  from sqlalchemy import inspect, sql
@@ -18,9 +19,22 @@ from sqlalchemy.sql.base import CompileState
18
19
  from sqlalchemy.sql.elements import quoted_name
19
20
  from sqlalchemy.sql.selectable import Lateral, SelectState
20
21
 
21
- from .compat import IS_VERSION_20, args_reducer, string_types
22
- from .custom_commands import AWSBucket, AzureContainer, ExternalStage
22
+ from snowflake.sqlalchemy._constants import DIALECT_NAME
23
+ from snowflake.sqlalchemy.compat import IS_VERSION_20, args_reducer, string_types
24
+ from snowflake.sqlalchemy.custom_commands import (
25
+ AWSBucket,
26
+ AzureContainer,
27
+ ExternalStage,
28
+ )
29
+
30
+ from ._constants import NOT_NULL
31
+ from .exc import (
32
+ CustomOptionsAreOnlySupportedOnSnowflakeTables,
33
+ UnexpectedOptionTypeError,
34
+ )
23
35
  from .functions import flatten
36
+ from .sql.custom_schema.custom_table_base import CustomTableBase
37
+ from .sql.custom_schema.options.table_option import TableOption
24
38
  from .util import (
25
39
  _find_left_clause_to_join_from,
26
40
  _set_connection_interpolate_empty_sequences,
@@ -184,7 +198,6 @@ class SnowflakeSelectState(SelectState):
184
198
  [element._from_objects for element in statement._where_criteria]
185
199
  ),
186
200
  ):
187
-
188
201
  potential[from_clause] = ()
189
202
 
190
203
  all_clauses = list(potential.keys())
@@ -879,7 +892,7 @@ class SnowflakeDDLCompiler(compiler.DDLCompiler):
879
892
 
880
893
  return " ".join(colspec)
881
894
 
882
- def post_create_table(self, table):
895
+ def handle_cluster_by(self, table):
883
896
  """
884
897
  Handles snowflake-specific ``CREATE TABLE ... CLUSTER BY`` syntax.
885
898
 
@@ -896,7 +909,7 @@ class SnowflakeDDLCompiler(compiler.DDLCompiler):
896
909
  ... metadata,
897
910
  ... sa.Column('id', sa.Integer, primary_key=True),
898
911
  ... sa.Column('name', sa.String),
899
- ... snowflake_clusterby=['id', 'name']
912
+ ... snowflake_clusterby=['id', 'name', text("id > 5")]
900
913
  ... )
901
914
  >>> print(CreateTable(user).compile(engine))
902
915
  <BLANKLINE>
@@ -904,19 +917,49 @@ class SnowflakeDDLCompiler(compiler.DDLCompiler):
904
917
  id INTEGER NOT NULL AUTOINCREMENT,
905
918
  name VARCHAR,
906
919
  PRIMARY KEY (id)
907
- ) CLUSTER BY (id, name)
920
+ ) CLUSTER BY (id, name, id > 5)
908
921
  <BLANKLINE>
909
922
  <BLANKLINE>
910
923
  """
911
924
  text = ""
912
- info = table.dialect_options["snowflake"]
925
+ info = table.dialect_options[DIALECT_NAME]
913
926
  cluster = info.get("clusterby")
914
927
  if cluster:
915
928
  text += " CLUSTER BY ({})".format(
916
- ", ".join(self.denormalize_column_name(key) for key in cluster)
929
+ ", ".join(
930
+ (
931
+ self.denormalize_column_name(key)
932
+ if isinstance(key, str)
933
+ else str(key)
934
+ )
935
+ for key in cluster
936
+ )
917
937
  )
918
938
  return text
919
939
 
940
+ def post_create_table(self, table):
941
+ text = self.handle_cluster_by(table)
942
+ options = []
943
+ invalid_options: List[str] = []
944
+
945
+ for key, option in table.dialect_options[DIALECT_NAME].items():
946
+ if isinstance(option, TableOption):
947
+ options.append(option)
948
+ elif key not in ["clusterby", "*"]:
949
+ invalid_options.append(key)
950
+
951
+ if len(invalid_options) > 0:
952
+ raise UnexpectedOptionTypeError(sorted(invalid_options))
953
+
954
+ if isinstance(table, CustomTableBase):
955
+ options.sort(key=lambda x: (x.priority.value, x.option_name), reverse=True)
956
+ for option in options:
957
+ text += "\t" + option.render_option(self)
958
+ elif len(options) > 0:
959
+ raise CustomOptionsAreOnlySupportedOnSnowflakeTables()
960
+
961
+ return text
962
+
920
963
  def visit_create_stage(self, create_stage, **kw):
921
964
  """
922
965
  This visitor will create the SQL representation for a CREATE STAGE command.
@@ -1029,6 +1072,12 @@ class SnowflakeTypeCompiler(compiler.GenericTypeCompiler):
1029
1072
  def visit_VARIANT(self, type_, **kw):
1030
1073
  return "VARIANT"
1031
1074
 
1075
+ def visit_MAP(self, type_, **kw):
1076
+ not_null = f" {NOT_NULL}" if type_.not_null else ""
1077
+ return (
1078
+ f"MAP({type_.key_type.compile()}, {type_.value_type.compile()}{not_null})"
1079
+ )
1080
+
1032
1081
  def visit_ARRAY(self, type_, **kw):
1033
1082
  return "ARRAY"
1034
1083
 
@@ -1065,4 +1114,4 @@ class SnowflakeTypeCompiler(compiler.GenericTypeCompiler):
1065
1114
 
1066
1115
  construct_arguments = [(Table, {"clusterby": None})]
1067
1116
 
1068
- functions.register_function("flatten", flatten)
1117
+ functions.register_function("flatten", flatten, "snowflake")
@@ -37,6 +37,26 @@ class VARIANT(SnowflakeType):
37
37
  __visit_name__ = "VARIANT"
38
38
 
39
39
 
40
+ class StructuredType(SnowflakeType):
41
+ def __init__(self):
42
+ super().__init__()
43
+
44
+
45
+ class MAP(StructuredType):
46
+ __visit_name__ = "MAP"
47
+
48
+ def __init__(
49
+ self,
50
+ key_type: sqltypes.TypeEngine,
51
+ value_type: sqltypes.TypeEngine,
52
+ not_null: bool = False,
53
+ ):
54
+ self.key_type = key_type
55
+ self.value_type = value_type
56
+ self.not_null = not_null
57
+ super().__init__()
58
+
59
+
40
60
  class OBJECT(SnowflakeType):
41
61
  __visit_name__ = "OBJECT"
42
62
 
@@ -0,0 +1,82 @@
1
+ #
2
+ # Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved.
3
+
4
+ from typing import List
5
+
6
+ from sqlalchemy.exc import ArgumentError
7
+
8
+
9
+ class NoPrimaryKeyError(ArgumentError):
10
+ def __init__(self, target: str):
11
+ super().__init__(f"Table {target} required primary key.")
12
+
13
+
14
+ class UnsupportedPrimaryKeysAndForeignKeysError(ArgumentError):
15
+ def __init__(self, target: str):
16
+ super().__init__(f"Primary key and foreign keys are not supported in {target}.")
17
+
18
+
19
+ class RequiredParametersNotProvidedError(ArgumentError):
20
+ def __init__(self, target: str, parameters: List[str]):
21
+ super().__init__(
22
+ f"{target} requires the following parameters: %s." % ", ".join(parameters)
23
+ )
24
+
25
+
26
+ class UnexpectedTableOptionKeyError(ArgumentError):
27
+ def __init__(self, expected: str, actual: str):
28
+ super().__init__(f"Expected table option {expected} but got {actual}.")
29
+
30
+
31
+ class OptionKeyNotProvidedError(ArgumentError):
32
+ def __init__(self, target: str):
33
+ super().__init__(
34
+ f"Expected option key in {target} option but got NoneType instead."
35
+ )
36
+
37
+
38
+ class UnexpectedOptionParameterTypeError(ArgumentError):
39
+ def __init__(self, parameter_name: str, target: str, types: List[str]):
40
+ super().__init__(
41
+ f"Parameter {parameter_name} of {target} requires to be one"
42
+ f" of following types: {', '.join(types)}."
43
+ )
44
+
45
+
46
+ class CustomOptionsAreOnlySupportedOnSnowflakeTables(ArgumentError):
47
+ def __init__(self):
48
+ super().__init__(
49
+ "Identifier, Literal, TargetLag and other custom options are only supported on Snowflake tables."
50
+ )
51
+
52
+
53
+ class UnexpectedOptionTypeError(ArgumentError):
54
+ def __init__(self, options: List[str]):
55
+ super().__init__(
56
+ f"The following options are either unsupported or should be defined using a Snowflake table: {', '.join(options)}."
57
+ )
58
+
59
+
60
+ class InvalidTableParameterTypeError(ArgumentError):
61
+ def __init__(self, name: str, input_type: str, expected_types: List[str]):
62
+ expected_types_str = "', '".join(expected_types)
63
+ super().__init__(
64
+ f"Invalid parameter type '{input_type}' provided for '{name}'. "
65
+ f"Expected one of the following types: '{expected_types_str}'.\n"
66
+ )
67
+
68
+
69
+ class MultipleErrors(ArgumentError):
70
+ def __init__(self, errors):
71
+ self.errors = errors
72
+
73
+ def __str__(self):
74
+ return "".join(str(e) for e in self.errors)
75
+
76
+
77
+ class StructuredTypeNotSupportedInTableColumnsError(ArgumentError):
78
+ def __init__(self, table_type: str, table_name: str, column_name: str):
79
+ super().__init__(
80
+ f"Column '{column_name}' is of a structured type, which is only supported on Iceberg tables. "
81
+ f"The table '{table_name}' is of type '{table_type}', not Iceberg."
82
+ )
@@ -0,0 +1,190 @@
1
+ #
2
+ # Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved.
3
+
4
+ import sqlalchemy.types as sqltypes
5
+ from sqlalchemy.sql.type_api import TypeEngine
6
+ from sqlalchemy.types import (
7
+ BIGINT,
8
+ BINARY,
9
+ BOOLEAN,
10
+ CHAR,
11
+ DATE,
12
+ DATETIME,
13
+ DECIMAL,
14
+ FLOAT,
15
+ INTEGER,
16
+ REAL,
17
+ SMALLINT,
18
+ TIME,
19
+ TIMESTAMP,
20
+ VARCHAR,
21
+ NullType,
22
+ )
23
+
24
+ from ..custom_types import (
25
+ _CUSTOM_DECIMAL,
26
+ ARRAY,
27
+ DOUBLE,
28
+ GEOGRAPHY,
29
+ GEOMETRY,
30
+ MAP,
31
+ OBJECT,
32
+ TIMESTAMP_LTZ,
33
+ TIMESTAMP_NTZ,
34
+ TIMESTAMP_TZ,
35
+ VARIANT,
36
+ )
37
+
38
+ ischema_names = {
39
+ "BIGINT": BIGINT,
40
+ "BINARY": BINARY,
41
+ # 'BIT': BIT,
42
+ "BOOLEAN": BOOLEAN,
43
+ "CHAR": CHAR,
44
+ "CHARACTER": CHAR,
45
+ "DATE": DATE,
46
+ "DATETIME": DATETIME,
47
+ "DEC": DECIMAL,
48
+ "DECIMAL": DECIMAL,
49
+ "DOUBLE": DOUBLE,
50
+ "FIXED": DECIMAL,
51
+ "FLOAT": FLOAT, # Snowflake FLOAT datatype doesn't has parameters
52
+ "INT": INTEGER,
53
+ "INTEGER": INTEGER,
54
+ "NUMBER": _CUSTOM_DECIMAL,
55
+ # 'OBJECT': ?
56
+ "REAL": REAL,
57
+ "BYTEINT": SMALLINT,
58
+ "SMALLINT": SMALLINT,
59
+ "STRING": VARCHAR,
60
+ "TEXT": VARCHAR,
61
+ "TIME": TIME,
62
+ "TIMESTAMP": TIMESTAMP,
63
+ "TIMESTAMP_TZ": TIMESTAMP_TZ,
64
+ "TIMESTAMP_LTZ": TIMESTAMP_LTZ,
65
+ "TIMESTAMP_NTZ": TIMESTAMP_NTZ,
66
+ "TINYINT": SMALLINT,
67
+ "VARBINARY": BINARY,
68
+ "VARCHAR": VARCHAR,
69
+ "VARIANT": VARIANT,
70
+ "MAP": MAP,
71
+ "OBJECT": OBJECT,
72
+ "ARRAY": ARRAY,
73
+ "GEOGRAPHY": GEOGRAPHY,
74
+ "GEOMETRY": GEOMETRY,
75
+ }
76
+
77
+
78
+ def extract_parameters(text: str) -> list:
79
+ """
80
+ Extracts parameters from a comma-separated string, handling parentheses.
81
+
82
+ :param text: A string with comma-separated parameters, which may include parentheses.
83
+
84
+ :return: A list of parameters as strings.
85
+
86
+ :example:
87
+ For input `"a, (b, c), d"`, the output is `['a', '(b, c)', 'd']`.
88
+ """
89
+
90
+ output_parameters = []
91
+ parameter = ""
92
+ open_parenthesis = 0
93
+ for c in text:
94
+
95
+ if c == "(":
96
+ open_parenthesis += 1
97
+ elif c == ")":
98
+ open_parenthesis -= 1
99
+
100
+ if open_parenthesis > 0 or c != ",":
101
+ parameter += c
102
+ elif c == ",":
103
+ output_parameters.append(parameter.strip(" "))
104
+ parameter = ""
105
+ if parameter != "":
106
+ output_parameters.append(parameter.strip(" "))
107
+ return output_parameters
108
+
109
+
110
+ def parse_type(type_text: str) -> TypeEngine:
111
+ """
112
+ Parses a type definition string and returns the corresponding SQLAlchemy type.
113
+
114
+ The function handles types with or without parameters, such as `VARCHAR(255)` or `INTEGER`.
115
+
116
+ :param type_text: A string representing a SQLAlchemy type, which may include parameters
117
+ in parentheses (e.g., "VARCHAR(255)" or "DECIMAL(10, 2)").
118
+ :return: An instance of the corresponding SQLAlchemy type class (e.g., `String`, `Integer`),
119
+ or `NullType` if the type is not recognized.
120
+
121
+ :example:
122
+ parse_type("VARCHAR(255)")
123
+ String(length=255)
124
+ """
125
+ index = type_text.find("(")
126
+ type_name = type_text[:index] if index != -1 else type_text
127
+ parameters = (
128
+ extract_parameters(type_text[index + 1 : -1]) if type_name != type_text else []
129
+ )
130
+
131
+ col_type_class = ischema_names.get(type_name, None)
132
+ col_type_kw = {}
133
+ if col_type_class is None:
134
+ col_type_class = NullType
135
+ else:
136
+ if issubclass(col_type_class, sqltypes.Numeric):
137
+ col_type_kw = __parse_numeric_type_parameters(parameters)
138
+ elif issubclass(col_type_class, (sqltypes.String, sqltypes.BINARY)):
139
+ col_type_kw = __parse_type_with_length_parameters(parameters)
140
+ elif issubclass(col_type_class, MAP):
141
+ col_type_kw = __parse_map_type_parameters(parameters)
142
+ if col_type_kw is None:
143
+ col_type_class = NullType
144
+ col_type_kw = {}
145
+
146
+ return col_type_class(**col_type_kw)
147
+
148
+
149
+ def __parse_map_type_parameters(parameters):
150
+ if len(parameters) != 2:
151
+ return None
152
+
153
+ key_type_str = parameters[0]
154
+ value_type_str = parameters[1]
155
+ not_null_str = "NOT NULL"
156
+ not_null = False
157
+ if (
158
+ len(value_type_str) >= len(not_null_str)
159
+ and value_type_str[-len(not_null_str) :] == not_null_str
160
+ ):
161
+ not_null = True
162
+ value_type_str = value_type_str[: -len(not_null_str) - 1]
163
+
164
+ key_type: TypeEngine = parse_type(key_type_str)
165
+ value_type: TypeEngine = parse_type(value_type_str)
166
+ if isinstance(key_type, NullType) or isinstance(value_type, NullType):
167
+ return None
168
+
169
+ return {
170
+ "key_type": key_type,
171
+ "value_type": value_type,
172
+ "not_null": not_null,
173
+ }
174
+
175
+
176
+ def __parse_type_with_length_parameters(parameters):
177
+ return (
178
+ {"length": int(parameters[0])}
179
+ if len(parameters) == 1 and str.isdigit(parameters[0])
180
+ else {}
181
+ )
182
+
183
+
184
+ def __parse_numeric_type_parameters(parameters):
185
+ result = {}
186
+ if len(parameters) >= 1 and str.isdigit(parameters[0]):
187
+ result["precision"] = int(parameters[0])
188
+ if len(parameters) == 2 and str.isdigit(parameters[1]):
189
+ result["scale"] = int(parameters[1])
190
+ return result