snowflake-sqlalchemy 1.7.2__tar.gz → 1.7.4__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 (127) hide show
  1. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/DESCRIPTION.md +15 -0
  2. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/PKG-INFO +24 -2
  3. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/README.md +23 -1
  4. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/pyproject.toml +6 -0
  5. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/src/snowflake/sqlalchemy/base.py +41 -1
  6. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/src/snowflake/sqlalchemy/custom_types.py +1 -1
  7. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/src/snowflake/sqlalchemy/snowdialect.py +19 -27
  8. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/src/snowflake/sqlalchemy/sql/custom_schema/hybrid_table.py +1 -0
  9. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/src/snowflake/sqlalchemy/version.py +1 -1
  10. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/__snapshots__/test_structured_datatypes.ambr +3 -0
  11. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/conftest.py +26 -0
  12. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/custom_tables/__snapshots__/test_compile_hybrid_table.ambr +3 -0
  13. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/custom_tables/__snapshots__/test_reflect_snowflake_table.ambr +3 -0
  14. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/custom_tables/test_compile_hybrid_table.py +21 -4
  15. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/custom_tables/test_create_iceberg_table.py +6 -3
  16. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/custom_tables/test_reflect_snowflake_table.py +30 -0
  17. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/test_compiler.py +94 -0
  18. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/test_core.py +87 -1
  19. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/test_structured_datatypes.py +15 -0
  20. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/.gitignore +0 -0
  21. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/.gitmodules +0 -0
  22. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/.pre-commit-config.yaml +0 -0
  23. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/LICENSE.txt +0 -0
  24. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/MANIFEST.in +0 -0
  25. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/ci/build.sh +0 -0
  26. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/ci/build_docker.sh +0 -0
  27. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/ci/docker/sqlalchemy_build/Dockerfile +0 -0
  28. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/ci/docker/sqlalchemy_build/scripts/entrypoint.sh +0 -0
  29. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/ci/set_base_image.sh +0 -0
  30. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/ci/test.sh +0 -0
  31. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/ci/test_docker.sh +0 -0
  32. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/ci/test_linux.sh +0 -0
  33. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/license_header.txt +0 -0
  34. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/setup.cfg +0 -0
  35. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/snyk/requirements.txt +0 -0
  36. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/snyk/requiremtnts.txt +0 -0
  37. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/snyk/update_requirements.py +0 -0
  38. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/src/snowflake/sqlalchemy/__init__.py +0 -0
  39. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/src/snowflake/sqlalchemy/_constants.py +0 -0
  40. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/src/snowflake/sqlalchemy/compat.py +0 -0
  41. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/src/snowflake/sqlalchemy/custom_commands.py +0 -0
  42. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/src/snowflake/sqlalchemy/exc.py +0 -0
  43. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/src/snowflake/sqlalchemy/functions.py +0 -0
  44. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/src/snowflake/sqlalchemy/parser/custom_type_parser.py +0 -0
  45. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/src/snowflake/sqlalchemy/provision.py +0 -0
  46. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/src/snowflake/sqlalchemy/requirements.py +0 -0
  47. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/src/snowflake/sqlalchemy/sql/__init__.py +0 -0
  48. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/src/snowflake/sqlalchemy/sql/custom_schema/__init__.py +0 -0
  49. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/src/snowflake/sqlalchemy/sql/custom_schema/clustered_table.py +0 -0
  50. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/src/snowflake/sqlalchemy/sql/custom_schema/custom_table_base.py +0 -0
  51. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/src/snowflake/sqlalchemy/sql/custom_schema/custom_table_prefix.py +0 -0
  52. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/src/snowflake/sqlalchemy/sql/custom_schema/dynamic_table.py +0 -0
  53. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/src/snowflake/sqlalchemy/sql/custom_schema/iceberg_table.py +0 -0
  54. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/src/snowflake/sqlalchemy/sql/custom_schema/options/__init__.py +0 -0
  55. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/src/snowflake/sqlalchemy/sql/custom_schema/options/as_query_option.py +0 -0
  56. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/src/snowflake/sqlalchemy/sql/custom_schema/options/cluster_by_option.py +0 -0
  57. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/src/snowflake/sqlalchemy/sql/custom_schema/options/identifier_option.py +0 -0
  58. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/src/snowflake/sqlalchemy/sql/custom_schema/options/invalid_table_option.py +0 -0
  59. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/src/snowflake/sqlalchemy/sql/custom_schema/options/keyword_option.py +0 -0
  60. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/src/snowflake/sqlalchemy/sql/custom_schema/options/keywords.py +0 -0
  61. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/src/snowflake/sqlalchemy/sql/custom_schema/options/literal_option.py +0 -0
  62. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/src/snowflake/sqlalchemy/sql/custom_schema/options/table_option.py +0 -0
  63. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/src/snowflake/sqlalchemy/sql/custom_schema/options/target_lag_option.py +0 -0
  64. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/src/snowflake/sqlalchemy/sql/custom_schema/snowflake_table.py +0 -0
  65. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/src/snowflake/sqlalchemy/sql/custom_schema/table_from_query.py +0 -0
  66. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/src/snowflake/sqlalchemy/util.py +0 -0
  67. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tested_requirements/requirements_310.reqs +0 -0
  68. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tested_requirements/requirements_37.reqs +0 -0
  69. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tested_requirements/requirements_38.reqs +0 -0
  70. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tested_requirements/requirements_39.reqs +0 -0
  71. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/README.rst +0 -0
  72. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/__init__.py +0 -0
  73. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/__snapshots__/test_compile_dynamic_table.ambr +0 -0
  74. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/__snapshots__/test_core.ambr +0 -0
  75. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/__snapshots__/test_orm.ambr +0 -0
  76. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/__snapshots__/test_reflect_dynamic_table.ambr +0 -0
  77. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/__snapshots__/test_unit_structured_types.ambr +0 -0
  78. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/custom_tables/__init__.py +0 -0
  79. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/custom_tables/__snapshots__/test_compile_dynamic_table.ambr +0 -0
  80. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/custom_tables/__snapshots__/test_compile_iceberg_table.ambr +0 -0
  81. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/custom_tables/__snapshots__/test_compile_snowflake_table.ambr +0 -0
  82. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/custom_tables/__snapshots__/test_create_dynamic_table.ambr +0 -0
  83. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/custom_tables/__snapshots__/test_create_hybrid_table.ambr +0 -0
  84. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/custom_tables/__snapshots__/test_create_iceberg_table.ambr +0 -0
  85. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/custom_tables/__snapshots__/test_create_snowflake_table.ambr +0 -0
  86. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/custom_tables/__snapshots__/test_generic_options.ambr +0 -0
  87. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/custom_tables/__snapshots__/test_reflect_hybrid_table.ambr +0 -0
  88. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/custom_tables/test_compile_dynamic_table.py +0 -0
  89. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/custom_tables/test_compile_iceberg_table.py +0 -0
  90. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/custom_tables/test_compile_snowflake_table.py +0 -0
  91. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/custom_tables/test_create_dynamic_table.py +0 -0
  92. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/custom_tables/test_create_hybrid_table.py +0 -0
  93. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/custom_tables/test_create_snowflake_table.py +0 -0
  94. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/custom_tables/test_generic_options.py +0 -0
  95. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/custom_tables/test_reflect_dynamic_table.py +0 -0
  96. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/custom_tables/test_reflect_hybrid_table.py +0 -0
  97. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/data/users.txt +0 -0
  98. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/sqlalchemy_test_suite/README.md +0 -0
  99. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/sqlalchemy_test_suite/__init__.py +0 -0
  100. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/sqlalchemy_test_suite/conftest.py +0 -0
  101. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/sqlalchemy_test_suite/test_suite.py +0 -0
  102. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/sqlalchemy_test_suite/test_suite_20.py +0 -0
  103. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/test_copy.py +0 -0
  104. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/test_create.py +0 -0
  105. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/test_custom_functions.py +0 -0
  106. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/test_custom_types.py +0 -0
  107. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/test_geography.py +0 -0
  108. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/test_geometry.py +0 -0
  109. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/test_imports.py +0 -0
  110. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/test_index_reflection.py +0 -0
  111. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/test_multivalues_insert.py +0 -0
  112. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/test_orm.py +0 -0
  113. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/test_pandas.py +0 -0
  114. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/test_qmark.py +0 -0
  115. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/test_quote.py +0 -0
  116. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/test_quote_identifiers.py +0 -0
  117. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/test_semi_structured_datatypes.py +0 -0
  118. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/test_sequence.py +0 -0
  119. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/test_timestamp.py +0 -0
  120. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/test_transactions.py +0 -0
  121. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/test_unit_core.py +0 -0
  122. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/test_unit_cte.py +0 -0
  123. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/test_unit_structured_types.py +0 -0
  124. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/test_unit_types.py +0 -0
  125. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/test_unit_url.py +0 -0
  126. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tests/util.py +0 -0
  127. {snowflake_sqlalchemy-1.7.2 → snowflake_sqlalchemy-1.7.4}/tox.ini +0 -0
@@ -6,8 +6,23 @@ Snowflake Documentation is available at:
6
6
 
7
7
  Source code is also available at:
8
8
  <https://github.com/snowflakedb/snowflake-sqlalchemy>
9
+ # Unreleased Notes
9
10
 
10
11
  # Release Notes
12
+ - v1.7.4(June 10, 2025)
13
+ - Fix dependency on DESCRIBE TABLE columns quantity (differences in columns caused by Snowflake parameters).
14
+ - Fix unnecessary condition was causing issues when parsing StructuredTypes columns.
15
+ - Update README.md to include instructions on how to verify package signatures using cosign.
16
+
17
+ - v1.7.3(January 15, 2025)
18
+ - Fix support for SqlAlchemy ARRAY.
19
+ - Fix return value of snowflake get_table_names.
20
+ - Fix incorrect quoting of identifiers with `_` as initial character.
21
+ - Fix ARRAY type not supported in HYBRID tables.
22
+ - Add `force_div_is_floordiv` flag to override `div_is_floordiv` new default value `False` in `SnowflakeDialect`.
23
+ - With the flag in `False`, the `/` division operator will be treated as a float division and `//` as a floor division.
24
+ - This flag is added to maintain backward compatibility with the previous behavior of Snowflake Dialect division.
25
+ - This flag will be removed in the future and Snowflake Dialect will use `div_is_floor_div` as `False`.
11
26
 
12
27
  - v1.7.2(December 18, 2024)
13
28
  - Fix quoting of `_` as column name
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: snowflake-sqlalchemy
3
- Version: 1.7.2
3
+ Version: 1.7.4
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
@@ -63,7 +63,7 @@ Description-Content-Type: text/markdown
63
63
  Snowflake SQLAlchemy runs on the top of the Snowflake Connector for Python as a [dialect](http://docs.sqlalchemy.org/en/latest/dialects/) to bridge a Snowflake database and SQLAlchemy applications.
64
64
 
65
65
 
66
- | :exclamation: | For production-affecting or urgent issues related to the connector, please [create a case with Snowflake Support](https://community.snowflake.com/s/article/How-To-Submit-a-Support-Case-in-Snowflake-Lodge). |
66
+ | :exclamation: | Effective May 8th, 2025, Snowflake SQLAlchemy will transition to maintenance mode and will cease active development. Support will be limited to addressing critical bugs and security vulnerabilities. To report such issues, please [create a case with Snowflake Support](https://community.snowflake.com/s/article/How-To-Submit-a-Support-Case-in-Snowflake-Lodge). for individual evaluation. Please note that pull requests from external contributors may not receive action from Snowflake. |
67
67
  |---------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
68
68
 
69
69
 
@@ -731,6 +731,28 @@ dynamic_test_table_1 = DynamicTable(
731
731
  - Direct data insertion into Dynamic Tables is not supported.
732
732
 
733
733
 
734
+ ## Verifying Package Signatures
735
+
736
+ To ensure the authenticity and integrity of the Python package, follow the steps below to verify the package signature using `cosign`.
737
+
738
+ **Steps to verify the signature:**
739
+ - Install cosign:
740
+ - This example is using golang installation: [installing-cosign-with-go](https://edu.chainguard.dev/open-source/sigstore/cosign/how-to-install-cosign/#installing-cosign-with-go)
741
+ - Download the file from the repository like pypi:
742
+ - https://pypi.org/project/snowflake-sqlalchemy/#files
743
+ - Download the signature files from the release tag, replace the version number with the version you are verifying:
744
+ - https://github.com/snowflakedb/snowflake-sqlalchemy/releases/tag/v1.7.3
745
+ - Verify signature:
746
+ ````bash
747
+ # replace the version number with the version you are verifying
748
+ ./cosign verify-blob snowflake_sqlalchemy-1.7.3-py3-none-any.whl \
749
+ --certificate snowflake_sqlalchemy-1.7.3-py3-none-any.whl.crt \
750
+ --certificate-identity https://github.com/snowflakedb/snowflake-sqlalchemy/.github/workflows/python-publish.yml@refs/tags/v1.7.3 \
751
+ --certificate-oidc-issuer https://token.actions.githubusercontent.com \
752
+ --signature snowflake_sqlalchemy-1.7.3-py3-none-any.whl.sig
753
+ Verified OK
754
+ ````
755
+
734
756
  ## Support
735
757
 
736
758
  Feel free to file an issue or submit a PR here for general cases. For official support, contact Snowflake support at:
@@ -9,7 +9,7 @@
9
9
  Snowflake SQLAlchemy runs on the top of the Snowflake Connector for Python as a [dialect](http://docs.sqlalchemy.org/en/latest/dialects/) to bridge a Snowflake database and SQLAlchemy applications.
10
10
 
11
11
 
12
- | :exclamation: | For production-affecting or urgent issues related to the connector, please [create a case with Snowflake Support](https://community.snowflake.com/s/article/How-To-Submit-a-Support-Case-in-Snowflake-Lodge). |
12
+ | :exclamation: | Effective May 8th, 2025, Snowflake SQLAlchemy will transition to maintenance mode and will cease active development. Support will be limited to addressing critical bugs and security vulnerabilities. To report such issues, please [create a case with Snowflake Support](https://community.snowflake.com/s/article/How-To-Submit-a-Support-Case-in-Snowflake-Lodge). for individual evaluation. Please note that pull requests from external contributors may not receive action from Snowflake. |
13
13
  |---------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
14
14
 
15
15
 
@@ -677,6 +677,28 @@ dynamic_test_table_1 = DynamicTable(
677
677
  - Direct data insertion into Dynamic Tables is not supported.
678
678
 
679
679
 
680
+ ## Verifying Package Signatures
681
+
682
+ To ensure the authenticity and integrity of the Python package, follow the steps below to verify the package signature using `cosign`.
683
+
684
+ **Steps to verify the signature:**
685
+ - Install cosign:
686
+ - This example is using golang installation: [installing-cosign-with-go](https://edu.chainguard.dev/open-source/sigstore/cosign/how-to-install-cosign/#installing-cosign-with-go)
687
+ - Download the file from the repository like pypi:
688
+ - https://pypi.org/project/snowflake-sqlalchemy/#files
689
+ - Download the signature files from the release tag, replace the version number with the version you are verifying:
690
+ - https://github.com/snowflakedb/snowflake-sqlalchemy/releases/tag/v1.7.3
691
+ - Verify signature:
692
+ ````bash
693
+ # replace the version number with the version you are verifying
694
+ ./cosign verify-blob snowflake_sqlalchemy-1.7.3-py3-none-any.whl \
695
+ --certificate snowflake_sqlalchemy-1.7.3-py3-none-any.whl.crt \
696
+ --certificate-identity https://github.com/snowflakedb/snowflake-sqlalchemy/.github/workflows/python-publish.yml@refs/tags/v1.7.3 \
697
+ --certificate-oidc-issuer https://token.actions.githubusercontent.com \
698
+ --signature snowflake_sqlalchemy-1.7.3-py3-none-any.whl.sig
699
+ Verified OK
700
+ ````
701
+
680
702
  ## Support
681
703
 
682
704
  Feel free to file an issue or submit a PR here for general cases. For official support, contact Snowflake support at:
@@ -87,6 +87,11 @@ extra-dependencies = ["SQLAlchemy>=1.4.19,<2.0.0"]
87
87
  features = ["development", "pandas"]
88
88
  python = "3.8"
89
89
 
90
+ [tool.hatch.envs.sa14.scripts]
91
+ test-dialect = "pytest --ignore_v20_test -ra -vvv --tb=short --cov snowflake.sqlalchemy --cov-append --junitxml ./junit.xml --ignore=tests/sqlalchemy_test_suite tests/"
92
+ test-dialect-compatibility = "pytest --ignore_v20_test -ra -vvv --tb=short --cov snowflake.sqlalchemy --cov-append --junitxml ./junit.xml tests/sqlalchemy_test_suite"
93
+ test-dialect-aws = "pytest --ignore_v20_test -m \"aws\" -ra -vvv --tb=short --cov snowflake.sqlalchemy --cov-append --junitxml ./junit.xml --ignore=tests/sqlalchemy_test_suite tests/"
94
+
90
95
  [tool.hatch.envs.default.env-vars]
91
96
  COVERAGE_FILE = "coverage.xml"
92
97
  SQLACHEMY_WARN_20 = "1"
@@ -134,4 +139,5 @@ markers = [
134
139
  "requires_external_volume: tests that needs a external volume to be executed",
135
140
  "external: tests that could but should only run on our external CI",
136
141
  "feature_max_lob_size: tests that could but should only run on our external CI",
142
+ "feature_v20: tests that could but should only run on SqlAlchemy v20",
137
143
  ]
@@ -6,6 +6,7 @@ import itertools
6
6
  import operator
7
7
  import re
8
8
  import string
9
+ import warnings
9
10
  from typing import List
10
11
 
11
12
  from sqlalchemy import exc as sa_exc
@@ -116,7 +117,11 @@ AUTOCOMMIT_REGEXP = re.compile(
116
117
  r"\s*(?:UPDATE|INSERT|DELETE|MERGE|COPY)", re.I | re.UNICODE
117
118
  )
118
119
  # used for quoting identifiers ie. table names, column names, etc.
119
- ILLEGAL_INITIAL_CHARACTERS = frozenset({d for d in string.digits}.union({"_", "$"}))
120
+ ILLEGAL_INITIAL_CHARACTERS = frozenset({d for d in string.digits}.union({"$"}))
121
+
122
+
123
+ # used for quoting identifiers ie. table names, column names, etc.
124
+ ILLEGAL_IDENTIFIERS = frozenset({d for d in string.digits}.union({"_"}))
120
125
 
121
126
  """
122
127
  Overwrite methods to handle Snowflake BCR change:
@@ -442,6 +447,7 @@ class SnowflakeORMSelectCompileState(context.ORMSelectCompileState):
442
447
  class SnowflakeIdentifierPreparer(compiler.IdentifierPreparer):
443
448
  reserved_words = {x.lower() for x in RESERVED_WORDS}
444
449
  illegal_initial_characters = ILLEGAL_INITIAL_CHARACTERS
450
+ illegal_identifiers = ILLEGAL_IDENTIFIERS
445
451
 
446
452
  def __init__(self, dialect, **kw):
447
453
  quote = '"'
@@ -470,6 +476,17 @@ class SnowflakeIdentifierPreparer(compiler.IdentifierPreparer):
470
476
 
471
477
  return self.quote_identifier(s) if n.quote else s
472
478
 
479
+ def _requires_quotes(self, value: str) -> bool:
480
+ """Return True if the given identifier requires quoting."""
481
+ lc_value = value.lower()
482
+ return (
483
+ lc_value in self.reserved_words
484
+ or lc_value in self.illegal_identifiers
485
+ or value[0] in self.illegal_initial_characters
486
+ or not self.legal_characters.match(str(value))
487
+ or (lc_value != value)
488
+ )
489
+
473
490
  def _split_schema_by_dot(self, schema):
474
491
  ret = []
475
492
  idx = 0
@@ -802,6 +819,26 @@ class SnowflakeCompiler(compiler.SQLCompiler):
802
819
  + join.onclause._compiler_dispatch(self, from_linter=from_linter, **kwargs)
803
820
  )
804
821
 
822
+ def visit_truediv_binary(self, binary, operator, **kw):
823
+ if self.dialect.div_is_floordiv:
824
+ warnings.warn(
825
+ "div_is_floordiv value will be changed to False in a future release. This will generate a behavior change on true and floor division. Please review https://docs.sqlalchemy.org/en/20/changelog/whatsnew_20.html#python-division-operator-performs-true-division-for-all-backends-added-floor-division",
826
+ PendingDeprecationWarning,
827
+ stacklevel=2,
828
+ )
829
+ return (
830
+ self.process(binary.left, **kw) + " / " + self.process(binary.right, **kw)
831
+ )
832
+
833
+ def visit_floordiv_binary(self, binary, operator, **kw):
834
+ if self.dialect.div_is_floordiv and IS_VERSION_20:
835
+ warnings.warn(
836
+ "div_is_floordiv value will be changed to False in a future release. This will generate a behavior change on true and floor division. Please review https://docs.sqlalchemy.org/en/20/changelog/whatsnew_20.html#python-division-operator-performs-true-division-for-all-backends-added-floor-division",
837
+ PendingDeprecationWarning,
838
+ stacklevel=2,
839
+ )
840
+ return super().visit_floordiv_binary(binary, operator, **kw)
841
+
805
842
  def render_literal_value(self, value, type_):
806
843
  # escape backslash
807
844
  return super().render_literal_value(value, type_).replace("\\", "\\\\")
@@ -1096,6 +1133,9 @@ class SnowflakeTypeCompiler(compiler.GenericTypeCompiler):
1096
1133
  )
1097
1134
 
1098
1135
  def visit_ARRAY(self, type_, **kw):
1136
+ return "ARRAY"
1137
+
1138
+ def visit_SNOWFLAKE_ARRAY(self, type_, **kw):
1099
1139
  if type_.is_semi_structured:
1100
1140
  return "ARRAY"
1101
1141
  not_null = f" {NOT_NULL}" if type_.not_null else ""
@@ -83,7 +83,7 @@ class OBJECT(StructuredType):
83
83
 
84
84
 
85
85
  class ARRAY(StructuredType):
86
- __visit_name__ = "ARRAY"
86
+ __visit_name__ = "SNOWFLAKE_ARRAY"
87
87
 
88
88
  def __init__(
89
89
  self,
@@ -79,6 +79,9 @@ class SnowflakeDialect(default.DefaultDialect):
79
79
  colspecs = colspecs
80
80
  ischema_names = ischema_names
81
81
 
82
+ # target database treats the / division operator as “floor division”
83
+ div_is_floordiv = False
84
+
82
85
  # all str types must be converted in Unicode
83
86
  convert_unicode = True
84
87
 
@@ -147,10 +150,17 @@ class SnowflakeDialect(default.DefaultDialect):
147
150
 
148
151
  def __init__(
149
152
  self,
153
+ force_div_is_floordiv: bool = True,
150
154
  isolation_level: Optional[str] = SnowflakeIsolationLevel.READ_COMMITTED.value,
151
155
  **kwargs: Any,
152
156
  ):
153
157
  super().__init__(isolation_level=isolation_level, **kwargs)
158
+ self.force_div_is_floordiv = force_div_is_floordiv
159
+ self.div_is_floordiv = force_div_is_floordiv
160
+
161
+ def initialize(self, connection):
162
+ super().initialize(connection)
163
+ self.div_is_floordiv = self.force_div_is_floordiv
154
164
 
155
165
  @classmethod
156
166
  def dbapi(cls):
@@ -501,13 +511,6 @@ class SnowflakeDialect(default.DefaultDialect):
501
511
  )
502
512
  schema_name = self.denormalize_name(schema)
503
513
 
504
- iceberg_table_names = self.get_table_names_with_prefix(
505
- connection,
506
- schema=schema_name,
507
- prefix=CustomTablePrefix.ICEBERG.name,
508
- info_cache=kw.get("info_cache", None),
509
- )
510
-
511
514
  result = connection.execute(
512
515
  text(
513
516
  """
@@ -568,10 +571,7 @@ class SnowflakeDialect(default.DefaultDialect):
568
571
  col_type_kw["scale"] = numeric_scale
569
572
  elif issubclass(col_type, (sqltypes.String, sqltypes.BINARY)):
570
573
  col_type_kw["length"] = character_maximum_length
571
- elif (
572
- issubclass(col_type, StructuredType)
573
- and table_name in iceberg_table_names
574
- ):
574
+ elif issubclass(col_type, StructuredType):
575
575
  if (schema_name, table_name) not in full_columns_descriptions:
576
576
  full_columns_descriptions[(schema_name, table_name)] = (
577
577
  self.table_columns_as_dict(
@@ -644,21 +644,13 @@ class SnowflakeDialect(default.DefaultDialect):
644
644
  f" TABLE {table_schema}.{table_name} TYPE = COLUMNS"
645
645
  )
646
646
  )
647
- for (
648
- column_name,
649
- coltype,
650
- _kind,
651
- is_nullable,
652
- column_default,
653
- primary_key,
654
- _unique_key,
655
- _check,
656
- _expression,
657
- comment,
658
- _policy_name,
659
- _privacy_domain,
660
- _name_mapping,
661
- ) in result:
647
+ for desc_data in result:
648
+ column_name = desc_data[0]
649
+ coltype = desc_data[1]
650
+ is_nullable = desc_data[3]
651
+ column_default = desc_data[4]
652
+ primary_key = desc_data[5]
653
+ comment = desc_data[9]
662
654
 
663
655
  column_name = self.normalize_name(column_name)
664
656
  if column_name.startswith("sys_clustering_column"):
@@ -756,7 +748,7 @@ class SnowflakeDialect(default.DefaultDialect):
756
748
  ret = self._get_schema_tables_info(
757
749
  connection, schema, info_cache=kw.get("info_cache", None)
758
750
  ).keys()
759
- return ret
751
+ return list(ret)
760
752
 
761
753
  @reflection.cache
762
754
  def get_view_names(self, connection, schema=None, **kw):
@@ -32,6 +32,7 @@ class HybridTable(CustomTableBase):
32
32
 
33
33
  __table_prefixes__ = [CustomTablePrefix.HYBRID]
34
34
  _enforce_primary_keys: bool = True
35
+ _support_structured_types = True
35
36
 
36
37
  def __init__(
37
38
  self,
@@ -3,4 +3,4 @@
3
3
  #
4
4
  # Update this for the versions
5
5
  # Don't change the forth version number from None
6
- VERSION = "1.7.2"
6
+ VERSION = "1.7.4"
@@ -5,6 +5,9 @@
5
5
  # name: test_compile_table_with_double_map
6
6
  'CREATE TABLE clustered_user (\t"Id" INTEGER NOT NULL AUTOINCREMENT, \tname MAP(DECIMAL, MAP(DECIMAL, VARCHAR)), \tPRIMARY KEY ("Id"))'
7
7
  # ---
8
+ # name: test_compile_table_with_sqlalchemy_array
9
+ 'CREATE TABLE clustered_user (\t"Id" INTEGER NOT NULL AUTOINCREMENT, \tname ARRAY, \tPRIMARY KEY ("Id"))'
10
+ # ---
8
11
  # name: test_compile_table_with_structured_data_type[structured_type0]
9
12
  'CREATE TABLE clustered_user (\t"Id" INTEGER NOT NULL AUTOINCREMENT, \tname MAP(DECIMAL(10, 0), MAP(DECIMAL(10, 0), VARCHAR(16777216))), \tPRIMARY KEY ("Id"))'
10
13
  # ---
@@ -47,6 +47,26 @@ snowflake.connector.connection.DEFAULT_CONFIGURATION[
47
47
  TEST_SCHEMA = f"sqlalchemy_tests_{str(uuid.uuid4()).replace('-', '_')}"
48
48
 
49
49
 
50
+ def pytest_addoption(parser):
51
+ parser.addoption(
52
+ "--ignore_v20_test",
53
+ action="store_true",
54
+ default=False,
55
+ help="skip sqlalchemy 2.0 exclusive tests",
56
+ )
57
+
58
+
59
+ def pytest_collection_modifyitems(config, items):
60
+ if config.getoption("--ignore_v20_test"):
61
+ # --ignore_v20_test given in cli: skip sqlalchemy 2.0 tests
62
+ skip_feature_v2 = pytest.mark.skip(
63
+ reason="need remove --ignore_v20_test option to run"
64
+ )
65
+ for item in items:
66
+ if "feature_v20" in item.keywords:
67
+ item.add_marker(skip_feature_v2)
68
+
69
+
50
70
  @pytest.fixture(scope="session")
51
71
  def on_travis():
52
72
  return os.getenv("TRAVIS", "").lower() == "true"
@@ -183,6 +203,12 @@ def get_engine(url: URL, **engine_kwargs):
183
203
  "echo": True,
184
204
  }
185
205
  engine_params.update(engine_kwargs)
206
+
207
+ connect_args = engine_params.get("connect_args", {}).copy()
208
+ connect_args["disable_ocsp_checks"] = True
209
+ connect_args["insecure_mode"] = True
210
+ engine_params["connect_args"] = connect_args
211
+
186
212
  engine = create_engine(url, **engine_params)
187
213
  return engine
188
214
 
@@ -5,3 +5,6 @@
5
5
  # name: test_compile_hybrid_table_orm
6
6
  'CREATE HYBRID TABLE test_hybrid_table_orm (\tid INTEGER NOT NULL AUTOINCREMENT, \tname VARCHAR, \tPRIMARY KEY (id))'
7
7
  # ---
8
+ # name: test_compile_hybrid_table_with_array
9
+ 'CREATE HYBRID TABLE test_hybrid_table (\tid INTEGER NOT NULL AUTOINCREMENT, \tname VARCHAR, \tgeom GEOMETRY, \tarray ARRAY, \tPRIMARY KEY (id))'
10
+ # ---
@@ -21,6 +21,9 @@
21
21
  }),
22
22
  ])
23
23
  # ---
24
+ # name: test_reflection_of_table_with_object_data_type
25
+ 'CREATE TABLE test_snowflake_table_reflection (\tid DECIMAL(38, 0) NOT NULL, \tname OBJECT, \tCONSTRAINT demo_name PRIMARY KEY (id))'
26
+ # ---
24
27
  # name: test_simple_reflection_of_table_as_snowflake_table
25
28
  'CREATE TABLE test_snowflake_table_reflection (\tid DECIMAL(38, 0) NOT NULL, \tname VARCHAR(16777216), \tCONSTRAINT demo_name PRIMARY KEY (id))'
26
29
  # ---
@@ -1,15 +1,14 @@
1
1
  #
2
2
  # Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved.
3
3
  #
4
- import pytest
4
+
5
5
  from sqlalchemy import Column, Integer, MetaData, String
6
6
  from sqlalchemy.orm import declarative_base
7
7
  from sqlalchemy.sql.ddl import CreateTable
8
8
 
9
- from snowflake.sqlalchemy import GEOMETRY, HybridTable
9
+ from snowflake.sqlalchemy import ARRAY, GEOMETRY, HybridTable
10
10
 
11
11
 
12
- @pytest.mark.aws
13
12
  def test_compile_hybrid_table(sql_compiler, snapshot):
14
13
  metadata = MetaData()
15
14
  table_name = "test_hybrid_table"
@@ -28,7 +27,25 @@ def test_compile_hybrid_table(sql_compiler, snapshot):
28
27
  assert actual == snapshot
29
28
 
30
29
 
31
- @pytest.mark.aws
30
+ def test_compile_hybrid_table_with_array(sql_compiler, snapshot):
31
+ metadata = MetaData()
32
+ table_name = "test_hybrid_table"
33
+ test_geometry = HybridTable(
34
+ table_name,
35
+ metadata,
36
+ Column("id", Integer, primary_key=True),
37
+ Column("name", String),
38
+ Column("geom", GEOMETRY),
39
+ Column("array", ARRAY),
40
+ )
41
+
42
+ value = CreateTable(test_geometry)
43
+
44
+ actual = sql_compiler(value)
45
+
46
+ assert actual == snapshot
47
+
48
+
32
49
  def test_compile_hybrid_table_orm(sql_compiler, snapshot):
33
50
  Base = declarative_base()
34
51
 
@@ -9,7 +9,7 @@ from snowflake.sqlalchemy import IcebergTable
9
9
 
10
10
 
11
11
  @pytest.mark.aws
12
- def test_create_iceberg_table(engine_testaccount, snapshot):
12
+ def test_create_iceberg_table(engine_testaccount):
13
13
  metadata = MetaData()
14
14
  external_volume_name = "exvol"
15
15
  create_external_volume = f"""
@@ -19,7 +19,7 @@ def test_create_iceberg_table(engine_testaccount, snapshot):
19
19
  (
20
20
  NAME = 'my-s3-us-west-2'
21
21
  STORAGE_PROVIDER = 'S3'
22
- STORAGE_BASE_URL = 's3://MY_EXAMPLE_BUCKET/'
22
+ STORAGE_BASE_URL = 's3://myexamplebucket/'
23
23
  STORAGE_AWS_ROLE_ARN = 'arn:aws:iam::123456789012:role/myrole'
24
24
  ENCRYPTION=(TYPE='AWS_SSE_KMS' KMS_KEY_ID='1234abcd-12ab-34cd-56ef-1234567890ab')
25
25
  )
@@ -40,4 +40,7 @@ def test_create_iceberg_table(engine_testaccount, snapshot):
40
40
  metadata.create_all(engine_testaccount)
41
41
 
42
42
  error_str = str(argument_error.value)
43
- assert error_str[: error_str.rfind("\n")] == snapshot
43
+ assert (
44
+ "(snowflake.connector.errors.ProgrammingError)"
45
+ in error_str[: error_str.rfind("\n")]
46
+ )
@@ -7,6 +7,36 @@ from sqlalchemy.sql.ddl import CreateTable
7
7
  from snowflake.sqlalchemy import SnowflakeTable
8
8
 
9
9
 
10
+ def test_reflection_of_table_with_object_data_type(
11
+ engine_testaccount, db_parameters, sql_compiler, snapshot
12
+ ):
13
+ metadata = MetaData()
14
+ table_name = "test_snowflake_table_reflection"
15
+
16
+ create_table_sql = f"""
17
+ CREATE TABLE {table_name} (id INT primary key, name OBJECT);
18
+ """
19
+
20
+ with engine_testaccount.connect() as connection:
21
+ connection.exec_driver_sql(create_table_sql)
22
+
23
+ snowflake_test_table = Table(table_name, metadata, autoload_with=engine_testaccount)
24
+ constraint = snowflake_test_table.constraints.pop()
25
+ constraint.name = "demo_name"
26
+ snowflake_test_table.constraints.add(constraint)
27
+
28
+ try:
29
+ with engine_testaccount.connect():
30
+ value = CreateTable(snowflake_test_table)
31
+
32
+ actual = sql_compiler(value)
33
+
34
+ assert actual == snapshot
35
+
36
+ finally:
37
+ metadata.drop_all(engine_testaccount)
38
+
39
+
10
40
  def test_simple_reflection_of_table_as_sqlalchemy_table(
11
41
  engine_testaccount, db_parameters, sql_compiler, snapshot
12
42
  ):
@@ -2,12 +2,14 @@
2
2
  # Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved.
3
3
  #
4
4
 
5
+ import pytest
5
6
  from sqlalchemy import Integer, String, and_, func, insert, select
6
7
  from sqlalchemy.schema import DropColumnComment, DropTableComment
7
8
  from sqlalchemy.sql import column, quoted_name, table
8
9
  from sqlalchemy.testing.assertions import AssertsCompiledSQL
9
10
 
10
11
  from snowflake.sqlalchemy import snowdialect
12
+ from src.snowflake.sqlalchemy.snowdialect import SnowflakeDialect
11
13
 
12
14
  table1 = table(
13
15
  "table1", column("id", Integer), column("name", String), column("value", Integer)
@@ -48,6 +50,21 @@ class TestSnowflakeCompiler(AssertsCompiledSQL):
48
50
  dialect="snowflake",
49
51
  )
50
52
 
53
+ def test_underscore_as_initial_character_as_non_quoted_identifier(self):
54
+ _table = table(
55
+ "table_1745924",
56
+ column("ca", Integer),
57
+ column("cb", String),
58
+ column("_identifier", String),
59
+ )
60
+
61
+ stmt = insert(_table).values(ca=1, cb="test", _identifier="test_")
62
+ self.assert_compile(
63
+ stmt,
64
+ "INSERT INTO table_1745924 (ca, cb, _identifier) VALUES (%(ca)s, %(cb)s, %(_identifier)s)",
65
+ dialect="snowflake",
66
+ )
67
+
51
68
  def test_multi_table_delete(self):
52
69
  statement = table1.delete().where(table1.c.id == table2.c.id)
53
70
  self.assert_compile(
@@ -135,3 +152,80 @@ def test_outer_lateral_join():
135
152
  str(stmt.compile(dialect=snowdialect.dialect()))
136
153
  == "SELECT colname AS label \nFROM abc JOIN LATERAL flatten(PARSE_JSON(colname2)) AS anon_1 GROUP BY colname"
137
154
  )
155
+
156
+
157
+ @pytest.mark.feature_v20
158
+ def test_division_operator_with_force_div_is_floordiv_false():
159
+ col1 = column("col1", Integer)
160
+ col2 = column("col2", Integer)
161
+ stmt = col1 / col2
162
+ assert (
163
+ str(stmt.compile(dialect=SnowflakeDialect(force_div_is_floordiv=False)))
164
+ == "col1 / col2"
165
+ )
166
+
167
+
168
+ @pytest.mark.feature_v20
169
+ def test_division_operator_with_denominator_expr_force_div_is_floordiv_false():
170
+ col1 = column("col1", Integer)
171
+ col2 = column("col2", Integer)
172
+ stmt = col1 / func.sqrt(col2)
173
+ assert (
174
+ str(stmt.compile(dialect=SnowflakeDialect(force_div_is_floordiv=False)))
175
+ == "col1 / sqrt(col2)"
176
+ )
177
+
178
+
179
+ @pytest.mark.feature_v20
180
+ def test_division_operator_with_force_div_is_floordiv_default_true():
181
+ col1 = column("col1", Integer)
182
+ col2 = column("col2", Integer)
183
+ stmt = col1 / col2
184
+ assert str(stmt.compile(dialect=SnowflakeDialect())) == "col1 / col2"
185
+
186
+
187
+ @pytest.mark.feature_v20
188
+ def test_division_operator_with_denominator_expr_force_div_is_floordiv_default_true():
189
+ col1 = column("col1", Integer)
190
+ col2 = column("col2", Integer)
191
+ stmt = col1 / func.sqrt(col2)
192
+ assert str(stmt.compile(dialect=SnowflakeDialect())) == "col1 / sqrt(col2)"
193
+
194
+
195
+ @pytest.mark.feature_v20
196
+ def test_floor_division_operator_force_div_is_floordiv_false():
197
+ col1 = column("col1", Integer)
198
+ col2 = column("col2", Integer)
199
+ stmt = col1 // col2
200
+ assert (
201
+ str(stmt.compile(dialect=SnowflakeDialect(force_div_is_floordiv=False)))
202
+ == "FLOOR(col1 / col2)"
203
+ )
204
+
205
+
206
+ @pytest.mark.feature_v20
207
+ def test_floor_division_operator_with_denominator_expr_force_div_is_floordiv_false():
208
+ col1 = column("col1", Integer)
209
+ col2 = column("col2", Integer)
210
+ stmt = col1 // func.sqrt(col2)
211
+ assert (
212
+ str(stmt.compile(dialect=SnowflakeDialect(force_div_is_floordiv=False)))
213
+ == "FLOOR(col1 / sqrt(col2))"
214
+ )
215
+
216
+
217
+ @pytest.mark.feature_v20
218
+ def test_floor_division_operator_force_div_is_floordiv_default_true():
219
+ col1 = column("col1", Integer)
220
+ col2 = column("col2", Integer)
221
+ stmt = col1 // col2
222
+ assert str(stmt.compile(dialect=SnowflakeDialect())) == "col1 / col2"
223
+
224
+
225
+ @pytest.mark.feature_v20
226
+ def test_floor_division_operator_with_denominator_expr_force_div_is_floordiv_default_true():
227
+ col1 = column("col1", Integer)
228
+ col2 = column("col2", Integer)
229
+ stmt = col1 // func.sqrt(col2)
230
+ res = stmt.compile(dialect=SnowflakeDialect())
231
+ assert str(res) == "FLOOR(col1 / sqrt(col2))"
@@ -31,13 +31,15 @@ from sqlalchemy import (
31
31
  create_engine,
32
32
  dialects,
33
33
  exc,
34
+ func,
34
35
  insert,
35
36
  inspect,
36
37
  text,
37
38
  )
38
39
  from sqlalchemy.exc import DBAPIError, NoSuchTableError, OperationalError
39
- from sqlalchemy.sql import and_, not_, or_, select
40
+ from sqlalchemy.sql import and_, literal, not_, or_, select
40
41
  from sqlalchemy.sql.ddl import CreateTable
42
+ from sqlalchemy.testing.assertions import eq_
41
43
 
42
44
  import snowflake.connector.errors
43
45
  import snowflake.sqlalchemy.snowdialect
@@ -469,6 +471,7 @@ def test_inspect_column(engine_testaccount):
469
471
  try:
470
472
  inspector = inspect(engine_testaccount)
471
473
  all_table_names = inspector.get_table_names()
474
+ assert isinstance(all_table_names, list)
472
475
  assert "users" in all_table_names
473
476
  assert "addresses" in all_table_names
474
477
 
@@ -1863,3 +1866,86 @@ def test_snowflake_sqlalchemy_as_valid_client_type():
1863
1866
  snowflake.connector.connection.DEFAULT_CONFIGURATION[
1864
1867
  "internal_application_version"
1865
1868
  ] = origin_internal_app_version
1869
+
1870
+
1871
+ @pytest.mark.parametrize(
1872
+ "operation",
1873
+ [
1874
+ [
1875
+ literal(5),
1876
+ literal(10),
1877
+ 0.5,
1878
+ ],
1879
+ [literal(5), func.sqrt(literal(10)), 1.5811388300841895],
1880
+ [literal(4), literal(5), decimal.Decimal("0.800000")],
1881
+ [literal(2), literal(2), 1.0],
1882
+ [literal(3), literal(2), 1.5],
1883
+ [literal(4), literal(1.5), 2.666667],
1884
+ [literal(5.5), literal(10.7), 0.5140187],
1885
+ [literal(5.5), literal(8), 0.6875],
1886
+ ],
1887
+ )
1888
+ def test_true_division_operation(engine_testaccount, operation):
1889
+ # expected_warning = "div_is_floordiv value will be changed to False in a future release. This will generate a behavior change on true and floor division. Please review https://docs.sqlalchemy.org/en/20/changelog/whatsnew_20.html#python-division-operator-performs-true-division-for-all-backends-added-floor-division"
1890
+ # with pytest.warns(PendingDeprecationWarning, match=expected_warning):
1891
+ with engine_testaccount.connect() as conn:
1892
+ eq_(
1893
+ conn.execute(select(operation[0] / operation[1])).fetchall(),
1894
+ [((operation[2]),)],
1895
+ )
1896
+
1897
+
1898
+ @pytest.mark.parametrize(
1899
+ "operation",
1900
+ [
1901
+ [literal(5), literal(10), 0.5, 0.5],
1902
+ [literal(5), func.sqrt(literal(10)), 1.5811388300841895, 1.0],
1903
+ [
1904
+ literal(4),
1905
+ literal(5),
1906
+ decimal.Decimal("0.800000"),
1907
+ decimal.Decimal("0.800000"),
1908
+ ],
1909
+ [literal(2), literal(2), 1.0, 1.0],
1910
+ [literal(3), literal(2), 1.5, 1.5],
1911
+ [literal(4), literal(1.5), 2.666667, 2.0],
1912
+ [literal(5.5), literal(10.7), 0.5140187, 0],
1913
+ [literal(5.5), literal(8), 0.6875, 0.6875],
1914
+ ],
1915
+ )
1916
+ @pytest.mark.feature_v20
1917
+ def test_division_force_div_is_floordiv_default(engine_testaccount, operation):
1918
+ expected_warning = "div_is_floordiv value will be changed to False in a future release. This will generate a behavior change on true and floor division. Please review https://docs.sqlalchemy.org/en/20/changelog/whatsnew_20.html#python-division-operator-performs-true-division-for-all-backends-added-floor-division"
1919
+ with pytest.warns(PendingDeprecationWarning, match=expected_warning):
1920
+ with engine_testaccount.connect() as conn:
1921
+ eq_(
1922
+ conn.execute(
1923
+ select(operation[0] / operation[1], operation[0] // operation[1])
1924
+ ).fetchall(),
1925
+ [(operation[2], operation[3])],
1926
+ )
1927
+
1928
+
1929
+ @pytest.mark.parametrize(
1930
+ "operation",
1931
+ [
1932
+ [literal(5), literal(10), 0.5, 0],
1933
+ [literal(5), func.sqrt(literal(10)), 1.5811388300841895, 1.0],
1934
+ [literal(4), literal(5), decimal.Decimal("0.800000"), 0],
1935
+ [literal(2), literal(2), 1.0, 1.0],
1936
+ [literal(3), literal(2), 1.5, 1],
1937
+ [literal(4), literal(1.5), 2.666667, 2.0],
1938
+ [literal(5.5), literal(10.7), 0.5140187, 0],
1939
+ [literal(5.5), literal(8), 0.6875, 0],
1940
+ ],
1941
+ )
1942
+ @pytest.mark.feature_v20
1943
+ def test_division_force_div_is_floordiv_false(db_parameters, operation):
1944
+ engine = create_engine(URL(**db_parameters), **{"force_div_is_floordiv": False})
1945
+ with engine.connect() as conn:
1946
+ eq_(
1947
+ conn.execute(
1948
+ select(operation[0] / operation[1], operation[0] // operation[1])
1949
+ ).fetchall(),
1950
+ [(operation[2], operation[3])],
1951
+ )
@@ -2,6 +2,7 @@
2
2
  # Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved.
3
3
  #
4
4
  import pytest
5
+ import sqlalchemy as sa
5
6
  from sqlalchemy import (
6
7
  Column,
7
8
  Integer,
@@ -47,6 +48,20 @@ def test_compile_table_with_structured_data_type(
47
48
  assert sql_compiler(create_table) == snapshot
48
49
 
49
50
 
51
+ def test_compile_table_with_sqlalchemy_array(sql_compiler, snapshot):
52
+ metadata = MetaData()
53
+ user_table = Table(
54
+ "clustered_user",
55
+ metadata,
56
+ Column("Id", Integer, primary_key=True),
57
+ Column("name", sa.ARRAY(sa.String)),
58
+ )
59
+
60
+ create_table = CreateTable(user_table)
61
+
62
+ assert sql_compiler(create_table) == snapshot
63
+
64
+
50
65
  @pytest.mark.requires_external_volume
51
66
  def test_insert_map(engine_testaccount, external_volume, base_location, snapshot):
52
67
  metadata = MetaData()