snowflake-sqlalchemy 1.8.2__tar.gz → 1.9.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 (137) hide show
  1. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/DESCRIPTION.md +17 -0
  2. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/PKG-INFO +190 -3
  3. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/README.md +189 -1
  4. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/ci/build.sh +2 -2
  5. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/ci/test_linux.sh +9 -2
  6. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/pyproject.toml +6 -2
  7. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/src/snowflake/sqlalchemy/__init__.py +4 -0
  8. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/src/snowflake/sqlalchemy/base.py +38 -6
  9. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/src/snowflake/sqlalchemy/compat.py +1 -1
  10. snowflake_sqlalchemy-1.9.0/src/snowflake/sqlalchemy/custom_types.py +295 -0
  11. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/src/snowflake/sqlalchemy/parser/custom_type_parser.py +21 -0
  12. snowflake_sqlalchemy-1.9.0/src/snowflake/sqlalchemy/provision.py +37 -0
  13. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/src/snowflake/sqlalchemy/snowdialect.py +334 -121
  14. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/src/snowflake/sqlalchemy/structured_type_info_manager.py +12 -1
  15. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/src/snowflake/sqlalchemy/version.py +1 -1
  16. snowflake_sqlalchemy-1.9.0/tests/README.rst +99 -0
  17. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/conftest.py +11 -0
  18. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/custom_tables/__snapshots__/test_reflect_snowflake_table.ambr +3 -3
  19. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/custom_tables/test_create_dynamic_table.py +6 -9
  20. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/custom_tables/test_reflect_dynamic_table.py +8 -6
  21. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/custom_tables/test_reflect_snowflake_table.py +3 -3
  22. snowflake_sqlalchemy-1.9.0/tests/test_compat.py +39 -0
  23. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/test_compiler.py +102 -2
  24. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/test_core.py +270 -19
  25. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/test_custom_types.py +11 -2
  26. snowflake_sqlalchemy-1.9.0/tests/test_decfloat.py +366 -0
  27. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/test_dialect_connect.py +9 -10
  28. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/test_index_reflection.py +2 -1
  29. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/test_pandas.py +38 -0
  30. snowflake_sqlalchemy-1.9.0/tests/test_timestamp.py +298 -0
  31. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/test_unit_core.py +50 -0
  32. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/test_unit_structured_types.py +31 -1
  33. snowflake_sqlalchemy-1.9.0/tests/test_vector.py +198 -0
  34. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/util.py +18 -0
  35. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tox.ini +21 -13
  36. snowflake_sqlalchemy-1.8.2/src/snowflake/sqlalchemy/custom_types.py +0 -155
  37. snowflake_sqlalchemy-1.8.2/src/snowflake/sqlalchemy/provision.py +0 -12
  38. snowflake_sqlalchemy-1.8.2/tests/README.rst +0 -51
  39. snowflake_sqlalchemy-1.8.2/tests/test_timestamp.py +0 -83
  40. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/.gitignore +0 -0
  41. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/.gitmodules +0 -0
  42. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/.pre-commit-config.yaml +0 -0
  43. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/LICENSE.txt +0 -0
  44. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/MANIFEST.in +0 -0
  45. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/ci/build_docker.sh +0 -0
  46. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/ci/docker/sqlalchemy_build/Dockerfile +0 -0
  47. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/ci/docker/sqlalchemy_build/scripts/entrypoint.sh +0 -0
  48. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/ci/set_base_image.sh +0 -0
  49. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/ci/test.sh +0 -0
  50. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/ci/test_docker.sh +0 -0
  51. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/license_header.txt +0 -0
  52. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/setup.cfg +0 -0
  53. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/snyk/requirements.txt +0 -0
  54. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/snyk/requiremtnts.txt +0 -0
  55. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/snyk/update_requirements.py +0 -0
  56. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/src/snowflake/sqlalchemy/_constants.py +0 -0
  57. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/src/snowflake/sqlalchemy/custom_commands.py +0 -0
  58. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/src/snowflake/sqlalchemy/exc.py +0 -0
  59. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/src/snowflake/sqlalchemy/functions.py +0 -0
  60. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/src/snowflake/sqlalchemy/name_utils.py +0 -0
  61. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/src/snowflake/sqlalchemy/requirements.py +0 -0
  62. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/src/snowflake/sqlalchemy/sql/__init__.py +0 -0
  63. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/src/snowflake/sqlalchemy/sql/custom_schema/__init__.py +0 -0
  64. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/src/snowflake/sqlalchemy/sql/custom_schema/clustered_table.py +0 -0
  65. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/src/snowflake/sqlalchemy/sql/custom_schema/custom_table_base.py +0 -0
  66. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/src/snowflake/sqlalchemy/sql/custom_schema/custom_table_prefix.py +0 -0
  67. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/src/snowflake/sqlalchemy/sql/custom_schema/dynamic_table.py +0 -0
  68. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/src/snowflake/sqlalchemy/sql/custom_schema/hybrid_table.py +0 -0
  69. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/src/snowflake/sqlalchemy/sql/custom_schema/iceberg_table.py +0 -0
  70. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/src/snowflake/sqlalchemy/sql/custom_schema/options/__init__.py +0 -0
  71. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/src/snowflake/sqlalchemy/sql/custom_schema/options/as_query_option.py +0 -0
  72. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/src/snowflake/sqlalchemy/sql/custom_schema/options/cluster_by_option.py +0 -0
  73. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/src/snowflake/sqlalchemy/sql/custom_schema/options/identifier_option.py +0 -0
  74. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/src/snowflake/sqlalchemy/sql/custom_schema/options/invalid_table_option.py +0 -0
  75. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/src/snowflake/sqlalchemy/sql/custom_schema/options/keyword_option.py +0 -0
  76. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/src/snowflake/sqlalchemy/sql/custom_schema/options/keywords.py +0 -0
  77. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/src/snowflake/sqlalchemy/sql/custom_schema/options/literal_option.py +0 -0
  78. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/src/snowflake/sqlalchemy/sql/custom_schema/options/table_option.py +0 -0
  79. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/src/snowflake/sqlalchemy/sql/custom_schema/options/target_lag_option.py +0 -0
  80. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/src/snowflake/sqlalchemy/sql/custom_schema/snowflake_table.py +0 -0
  81. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/src/snowflake/sqlalchemy/sql/custom_schema/table_from_query.py +0 -0
  82. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/src/snowflake/sqlalchemy/util.py +0 -0
  83. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tested_requirements/requirements_310.reqs +0 -0
  84. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tested_requirements/requirements_37.reqs +0 -0
  85. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tested_requirements/requirements_38.reqs +0 -0
  86. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tested_requirements/requirements_39.reqs +0 -0
  87. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/__init__.py +0 -0
  88. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/__snapshots__/test_compile_dynamic_table.ambr +0 -0
  89. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/__snapshots__/test_core.ambr +0 -0
  90. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/__snapshots__/test_orm.ambr +0 -0
  91. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/__snapshots__/test_reflect_dynamic_table.ambr +0 -0
  92. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/__snapshots__/test_structured_datatypes.ambr +0 -0
  93. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/__snapshots__/test_unit_structured_types.ambr +0 -0
  94. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/custom_tables/__init__.py +0 -0
  95. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/custom_tables/__snapshots__/test_compile_dynamic_table.ambr +0 -0
  96. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/custom_tables/__snapshots__/test_compile_hybrid_table.ambr +0 -0
  97. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/custom_tables/__snapshots__/test_compile_iceberg_table.ambr +0 -0
  98. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/custom_tables/__snapshots__/test_compile_snowflake_table.ambr +0 -0
  99. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/custom_tables/__snapshots__/test_create_dynamic_table.ambr +0 -0
  100. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/custom_tables/__snapshots__/test_create_hybrid_table.ambr +0 -0
  101. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/custom_tables/__snapshots__/test_create_iceberg_table.ambr +0 -0
  102. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/custom_tables/__snapshots__/test_create_snowflake_table.ambr +0 -0
  103. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/custom_tables/__snapshots__/test_generic_options.ambr +0 -0
  104. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/custom_tables/__snapshots__/test_reflect_hybrid_table.ambr +0 -0
  105. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/custom_tables/test_compile_dynamic_table.py +0 -0
  106. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/custom_tables/test_compile_hybrid_table.py +0 -0
  107. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/custom_tables/test_compile_iceberg_table.py +0 -0
  108. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/custom_tables/test_compile_snowflake_table.py +0 -0
  109. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/custom_tables/test_create_hybrid_table.py +0 -0
  110. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/custom_tables/test_create_iceberg_table.py +0 -0
  111. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/custom_tables/test_create_snowflake_table.py +0 -0
  112. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/custom_tables/test_generic_options.py +0 -0
  113. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/custom_tables/test_reflect_hybrid_table.py +0 -0
  114. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/data/users.txt +0 -0
  115. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/sqlalchemy_test_suite/README.md +0 -0
  116. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/sqlalchemy_test_suite/__init__.py +0 -0
  117. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/sqlalchemy_test_suite/conftest.py +0 -0
  118. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/sqlalchemy_test_suite/test_suite.py +0 -0
  119. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/sqlalchemy_test_suite/test_suite_20.py +0 -0
  120. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/test_copy.py +0 -0
  121. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/test_create.py +0 -0
  122. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/test_custom_functions.py +0 -0
  123. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/test_geography.py +0 -0
  124. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/test_geometry.py +0 -0
  125. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/test_imports.py +0 -0
  126. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/test_multivalues_insert.py +0 -0
  127. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/test_orm.py +0 -0
  128. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/test_qmark.py +0 -0
  129. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/test_quote.py +0 -0
  130. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/test_quote_identifiers.py +0 -0
  131. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/test_semi_structured_datatypes.py +0 -0
  132. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/test_sequence.py +0 -0
  133. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/test_structured_datatypes.py +0 -0
  134. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/test_transactions.py +0 -0
  135. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/test_unit_cte.py +0 -0
  136. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/test_unit_types.py +0 -0
  137. {snowflake_sqlalchemy-1.8.2 → snowflake_sqlalchemy-1.9.0}/tests/test_unit_url.py +0 -0
@@ -11,6 +11,23 @@ Source code is also available at:
11
11
 
12
12
  # Release Notes
13
13
 
14
+ - v1.9.0 (March 4, 2026)
15
+ - Add support for `DECFLOAT` and `VECTOR` data types
16
+ - Add server_version_info support
17
+ - Add support for `ILIKE` in queries
18
+ - Fix `SYSDATE()` rendering
19
+ - Fix and improve schema reflection (SNOW-593204, SNOW-2331576, SNOW-2852779)
20
+ - Fix crash when reflecting without specifying a schema, caused by `None` arguments in internal schema resolution ([#623](https://github.com/snowflakedb/snowflake-sqlalchemy/issues/623)).
21
+ - Fix crash when `SHOW TABLES` returns empty string table names, causing `IndexError` during reflection ([#296](https://github.com/snowflakedb/snowflake-sqlalchemy/issues/296)).
22
+ - Fix incomplete identity column reflection metadata, now includes all fields required by SQLAlchemy 2.0+ (`always`, `cycle`, `order`, etc.).
23
+ - Introduce shared helper for fully-qualified schema name resolution, replacing inconsistent ad-hoc patterns across reflection methods.
24
+ - Refactor column reflection internals into dedicated helpers to reduce complexity without changing behavior.
25
+ - Add `pytest-xdist` parallel test support via per-worker schema provisioning hooks.
26
+ - Bump `pandas` lower bound in `sa14` test environment from `<2.1` to `>=2.1.1,<2.2` to ensure pre-built wheels are available for Python 3.12
27
+ - Fix SQLAlchemy version parsing (SNOW-3066571)
28
+ - Document support for session parameters (like [QUERY_TAG](https://docs.snowflake.com/en/sql-reference/parameters#query-tag)), references: [#644](https://github.com/snowflakedb/snowflake-sqlalchemy/issues/495)
29
+ - Support timezone in timestamp and datetime types ([#199](https://github.com/snowflakedb/snowflake-sqlalchemy/issues/199))
30
+
14
31
  - v1.8.2 (December 9, 2025)
15
32
  - Updated supported max python version to 3.13
16
33
  - Version 1.8.1 yanked due to max python version supported by `snowflake-connector-python`
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: snowflake-sqlalchemy
3
- Version: 1.8.2
3
+ Version: 1.9.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
@@ -39,7 +39,6 @@ Requires-Python: >=3.8
39
39
  Requires-Dist: snowflake-connector-python<5.0.0
40
40
  Requires-Dist: sqlalchemy>=1.4.19
41
41
  Provides-Extra: development
42
- Requires-Dist: mock; extra == 'development'
43
42
  Requires-Dist: numpy; extra == 'development'
44
43
  Requires-Dist: pre-commit; extra == 'development'
45
44
  Requires-Dist: pytest; extra == 'development'
@@ -64,7 +63,47 @@ Description-Content-Type: text/markdown
64
63
 
65
64
  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.
66
65
 
67
-
66
+ Table of contents:
67
+ <!-- TOC -->
68
+ * [Snowflake SQLAlchemy](#snowflake-sqlalchemy)
69
+ * [Prerequisites](#prerequisites)
70
+ * [Snowflake Connector for Python](#snowflake-connector-for-python)
71
+ * [Data Analytics and Web Application Frameworks (Optional)](#data-analytics-and-web-application-frameworks-optional)
72
+ * [Installing Snowflake SQLAlchemy](#installing-snowflake-sqlalchemy)
73
+ * [Verifying Your Installation](#verifying-your-installation)
74
+ * [Parameters and Behavior](#parameters-and-behavior)
75
+ * [Connection Parameters](#connection-parameters)
76
+ * [Escaping Special Characters such as `%, @` signs in Passwords](#escaping-special-characters-such-as---signs-in-passwords)
77
+ * [Using a proxy server](#using-a-proxy-server)
78
+ * [Using session parameters](#using-session-parameters)
79
+ * [Opening and Closing Connection](#opening-and-closing-connection)
80
+ * [Auto-increment Behavior](#auto-increment-behavior)
81
+ * [Object Name Case Handling](#object-name-case-handling)
82
+ * [Index Support](#index-support)
83
+ * [Single Column Index](#single-column-index)
84
+ * [Multi-Column Index](#multi-column-index)
85
+ * [Numpy Data Type Support](#numpy-data-type-support)
86
+ * [DECFLOAT Data Type Support](#decfloat-data-type-support)
87
+ * [DECFLOAT Precision](#decfloat-precision)
88
+ * [VECTOR Data Type Support](#vector-data-type-support)
89
+ * [Cache Column Metadata](#cache-column-metadata)
90
+ * [VARIANT, ARRAY and OBJECT Support](#variant-array-and-object-support)
91
+ * [Structured Data Types Support](#structured-data-types-support)
92
+ * [MAP](#map)
93
+ * [OBJECT](#object)
94
+ * [ARRAY](#array)
95
+ * [CLUSTER BY Support](#cluster-by-support)
96
+ * [Alembic Support](#alembic-support)
97
+ * [Key Pair Authentication Support](#key-pair-authentication-support)
98
+ * [Merge Command Support](#merge-command-support)
99
+ * [CopyIntoStorage Support](#copyintostorage-support)
100
+ * [Iceberg Table with Snowflake Catalog support](#iceberg-table-with-snowflake-catalog-support)
101
+ * [Hybrid Table support](#hybrid-table-support)
102
+ * [Dynamic Tables support](#dynamic-tables-support)
103
+ * [Notes](#notes)
104
+ * [Verifying Package Signatures](#verifying-package-signatures)
105
+ * [Support](#support)
106
+ <!-- TOC -->
68
107
 
69
108
  ## Prerequisites
70
109
 
@@ -240,6 +279,44 @@ engine = create_engine(URL(
240
279
 
241
280
  Use the supported environment variables, `HTTPS_PROXY`, `HTTP_PROXY` and `NO_PROXY` to configure a proxy server.
242
281
 
282
+ #### Using session parameters
283
+
284
+ Snowflake [session parameters](https://docs.snowflake.com/en/sql-reference/parameters#session-parameters) (such as [`QUERY_TAG`](https://docs.snowflake.com/en/sql-reference/parameters#query-tag)) cannot be set directly through the `URL` helper.
285
+ Instead, pass them via the `connect_args` parameter of `create_engine`, using the `session_parameters` dict — the same way you would [through the Python connector](https://docs.snowflake.com/en/developer-guide/python-connector/python-connector-connect#setting-session-parameters):
286
+
287
+ ```python
288
+ from snowflake.sqlalchemy import URL
289
+ from sqlalchemy import create_engine
290
+
291
+ engine = create_engine(
292
+ URL(
293
+ # CONNECTION_PARAMETERS
294
+ ),
295
+ connect_args={
296
+ "session_parameters": {
297
+ "QUERY_TAG": "SOME_QUERY_TAGS",
298
+ }
299
+ },
300
+ )
301
+ ```
302
+
303
+ Session parameters set this way apply to all queries executed within the session.
304
+ To change a session parameter for specific queries mid-session, use `ALTER SESSION`:
305
+
306
+ ```python
307
+ from sqlalchemy import text
308
+
309
+ with engine.connect() as conn:
310
+ conn.execute(text("ALTER SESSION SET QUERY_TAG = 'batch_job_1'"))
311
+ conn.execute(text("...")) # Uses 'batch_job_1'
312
+
313
+ conn.execute(text("ALTER SESSION SET QUERY_TAG = 'batch_job_2'"))
314
+ conn.execute(text("...")) # Uses 'batch_job_2'
315
+
316
+ conn.execute(text("ALTER SESSION UNSET QUERY_TAG"))
317
+ conn.execute(text("...")) # No tag
318
+ ```
319
+
243
320
  ### Opening and Closing Connection
244
321
 
245
322
  Open a connection by executing `engine.connect()`; avoid using `engine.execute()`. Make certain to close the connection by executing `connection.close()` before
@@ -359,6 +436,116 @@ The following `NumPy` data types are supported:
359
436
  - numpy.float64
360
437
  - numpy.datatime64
361
438
 
439
+ ### DECFLOAT Data Type Support
440
+
441
+ Snowflake SQLAlchemy supports the `DECFLOAT` data type, which provides decimal floating-point with up to 38 significant digits. For more information, see the [Snowflake DECFLOAT documentation](https://docs.snowflake.com/en/sql-reference/data-types-numeric#decfloat).
442
+
443
+ ```python
444
+ from sqlalchemy import Column, Integer, MetaData, Table
445
+ from snowflake.sqlalchemy import DECFLOAT
446
+
447
+ metadata = MetaData()
448
+ t = Table('my_table', metadata,
449
+ Column('id', Integer, primary_key=True),
450
+ Column('value', DECFLOAT()),
451
+ )
452
+ metadata.create_all(engine)
453
+ ```
454
+
455
+ #### DECFLOAT Precision
456
+
457
+ The Snowflake Python connector uses Python's `decimal` module context when converting `DECFLOAT` values to Python `Decimal` objects. Python's default decimal context precision is 28 digits, which can truncate `DECFLOAT` values that use up to 38 digits.
458
+
459
+ To preserve full 38-digit precision, add `enable_decfloat=True` to the connection URL:
460
+
461
+ ```python
462
+ from sqlalchemy import create_engine
463
+
464
+ engine = create_engine(
465
+ 'snowflake://testuser1:0123456@abc123/testdb/public?warehouse=testwh&enable_decfloat=True'
466
+ )
467
+ ```
468
+
469
+ Or using the `snowflake.sqlalchemy.URL` helper:
470
+
471
+ ```python
472
+ from snowflake.sqlalchemy import URL
473
+ from sqlalchemy import create_engine
474
+
475
+ engine = create_engine(URL(
476
+ account = 'abc123',
477
+ user = 'testuser1',
478
+ password = '0123456',
479
+ database = 'testdb',
480
+ schema = 'public',
481
+ warehouse = 'testwh',
482
+ enable_decfloat = True,
483
+ ))
484
+ ```
485
+
486
+ **Note**: `DECFLOAT` does not support special values (`inf`, `-inf`, `NaN`) unlike `FLOAT`.
487
+
488
+ **Why is `enable_decfloat` not enabled by default?** Enabling it sets `decimal.getcontext().prec = 38`, which modifies Python's thread-local decimal context and affects all `Decimal` operations in that thread, not just database queries. To avoid unexpected side effects on application code, the dialect emits a warning when `DECFLOAT` values are retrieved without full precision enabled, guiding users to opt-in explicitly.
489
+
490
+ ### VECTOR Data Type Support
491
+
492
+ Snowflake SQLAlchemy supports the `VECTOR` data type with varying element type and dimension.
493
+ For more information, see the [Snowflake documentation](https://docs.snowflake.com/en/sql-reference/data-types-vector).
494
+
495
+ ```python
496
+ from sqlalchemy import Column, Integer, Float, MetaData, Table
497
+ from snowflake.sqlalchemy import VECTOR
498
+
499
+ metadata = MetaData()
500
+ t = Table('my_table', metadata,
501
+ Column('id', Integer, primary_key=True),
502
+ Column('int_vec', VECTOR(Integer, 20)),
503
+ Column('float_vec', VECTOR(Float, 40)),
504
+ )
505
+ metadata.create_all(engine)
506
+ ```
507
+
508
+ ### Timestamp and Timezone Support
509
+
510
+ Snowflake SQLAlchemy provides three Snowflake-specific timestamp types that map directly to their Snowflake counterparts:
511
+
512
+ ```python
513
+ from sqlalchemy import Column, Integer, MetaData, Table, create_engine
514
+ from snowflake.sqlalchemy import TIMESTAMP_NTZ, TIMESTAMP_TZ, TIMESTAMP_LTZ
515
+
516
+ engine = create_engine(...)
517
+ metadata = MetaData()
518
+ t = Table('events', metadata,
519
+ Column('id', Integer, primary_key=True),
520
+ Column('created_at', TIMESTAMP_NTZ()), # TIMESTAMP WITHOUT TIME ZONE
521
+ Column('scheduled_at', TIMESTAMP_TZ()), # TIMESTAMP WITH TIME ZONE
522
+ Column('logged_at', TIMESTAMP_LTZ()), # TIMESTAMP WITH LOCAL TIME ZONE
523
+ )
524
+ metadata.create_all(engine)
525
+ ```
526
+
527
+ SQLAlchemy's generic `DateTime` and `TIMESTAMP` types also support timezone-aware columns via the `timezone` parameter. When `timezone=True` is set, the dialect emits `TIMESTAMP_TZ` instead of the default `TIMESTAMP_NTZ`:
528
+
529
+ ```python
530
+ from sqlalchemy import Column, DateTime, Integer, MetaData, Table, create_engine
531
+ from sqlalchemy.types import TIMESTAMP
532
+
533
+ engine = create_engine(...)
534
+ metadata = MetaData()
535
+ t = Table('events', metadata,
536
+ Column('id', Integer, primary_key=True),
537
+ Column('naive_ts', DateTime()), # produces TIMESTAMP_NTZ
538
+ Column('aware_ts', DateTime(timezone=True)), # produces TIMESTAMP_TZ
539
+ Column('naive_ts2', TIMESTAMP()), # produces TIMESTAMP_NTZ
540
+ Column('aware_ts2', TIMESTAMP(timezone=True)), # produces TIMESTAMP_TZ
541
+ )
542
+ metadata.create_all(engine)
543
+ ```
544
+
545
+ This also applies when using pandas `to_sql()` with timezone-aware datetime columns, which infers `DateTime(timezone=True)` automatically (see [#199](https://github.com/snowflakedb/snowflake-sqlalchemy/issues/199)).
546
+
547
+ **Note on `Time` and timezones:** SQLAlchemy's `Time` type accepts a `timezone` parameter, but [Snowflake's TIME data type does not support time zones](https://docs.snowflake.com/en/sql-reference/data-types-datetime#time). Using `Time(timezone=True)` will compile to plain `TIME` and the `timezone` flag will have no effect. If you need to store time data with time-zone information, use a timestamp type such as `TIMESTAMP_TZ` or `DateTime(timezone=True)` instead.
548
+
362
549
  ### Cache Column Metadata
363
550
 
364
551
  SQLAlchemy provides [the runtime inspection API](http://docs.sqlalchemy.org/en/latest/core/inspection.html) to get the runtime information about the various objects. One of the common use case is get all tables and their column metadata in a schema in order to construct a schema catalog. For example, [alembic](http://alembic.zzzcomputing.com/) on top of SQLAlchemy manages database schema migrations. A pseudo code flow is as follows:
@@ -8,7 +8,47 @@
8
8
 
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
+ Table of contents:
12
+ <!-- TOC -->
13
+ * [Snowflake SQLAlchemy](#snowflake-sqlalchemy)
14
+ * [Prerequisites](#prerequisites)
15
+ * [Snowflake Connector for Python](#snowflake-connector-for-python)
16
+ * [Data Analytics and Web Application Frameworks (Optional)](#data-analytics-and-web-application-frameworks-optional)
17
+ * [Installing Snowflake SQLAlchemy](#installing-snowflake-sqlalchemy)
18
+ * [Verifying Your Installation](#verifying-your-installation)
19
+ * [Parameters and Behavior](#parameters-and-behavior)
20
+ * [Connection Parameters](#connection-parameters)
21
+ * [Escaping Special Characters such as `%, @` signs in Passwords](#escaping-special-characters-such-as---signs-in-passwords)
22
+ * [Using a proxy server](#using-a-proxy-server)
23
+ * [Using session parameters](#using-session-parameters)
24
+ * [Opening and Closing Connection](#opening-and-closing-connection)
25
+ * [Auto-increment Behavior](#auto-increment-behavior)
26
+ * [Object Name Case Handling](#object-name-case-handling)
27
+ * [Index Support](#index-support)
28
+ * [Single Column Index](#single-column-index)
29
+ * [Multi-Column Index](#multi-column-index)
30
+ * [Numpy Data Type Support](#numpy-data-type-support)
31
+ * [DECFLOAT Data Type Support](#decfloat-data-type-support)
32
+ * [DECFLOAT Precision](#decfloat-precision)
33
+ * [VECTOR Data Type Support](#vector-data-type-support)
34
+ * [Cache Column Metadata](#cache-column-metadata)
35
+ * [VARIANT, ARRAY and OBJECT Support](#variant-array-and-object-support)
36
+ * [Structured Data Types Support](#structured-data-types-support)
37
+ * [MAP](#map)
38
+ * [OBJECT](#object)
39
+ * [ARRAY](#array)
40
+ * [CLUSTER BY Support](#cluster-by-support)
41
+ * [Alembic Support](#alembic-support)
42
+ * [Key Pair Authentication Support](#key-pair-authentication-support)
43
+ * [Merge Command Support](#merge-command-support)
44
+ * [CopyIntoStorage Support](#copyintostorage-support)
45
+ * [Iceberg Table with Snowflake Catalog support](#iceberg-table-with-snowflake-catalog-support)
46
+ * [Hybrid Table support](#hybrid-table-support)
47
+ * [Dynamic Tables support](#dynamic-tables-support)
48
+ * [Notes](#notes)
49
+ * [Verifying Package Signatures](#verifying-package-signatures)
50
+ * [Support](#support)
51
+ <!-- TOC -->
12
52
 
13
53
  ## Prerequisites
14
54
 
@@ -184,6 +224,44 @@ engine = create_engine(URL(
184
224
 
185
225
  Use the supported environment variables, `HTTPS_PROXY`, `HTTP_PROXY` and `NO_PROXY` to configure a proxy server.
186
226
 
227
+ #### Using session parameters
228
+
229
+ Snowflake [session parameters](https://docs.snowflake.com/en/sql-reference/parameters#session-parameters) (such as [`QUERY_TAG`](https://docs.snowflake.com/en/sql-reference/parameters#query-tag)) cannot be set directly through the `URL` helper.
230
+ Instead, pass them via the `connect_args` parameter of `create_engine`, using the `session_parameters` dict — the same way you would [through the Python connector](https://docs.snowflake.com/en/developer-guide/python-connector/python-connector-connect#setting-session-parameters):
231
+
232
+ ```python
233
+ from snowflake.sqlalchemy import URL
234
+ from sqlalchemy import create_engine
235
+
236
+ engine = create_engine(
237
+ URL(
238
+ # CONNECTION_PARAMETERS
239
+ ),
240
+ connect_args={
241
+ "session_parameters": {
242
+ "QUERY_TAG": "SOME_QUERY_TAGS",
243
+ }
244
+ },
245
+ )
246
+ ```
247
+
248
+ Session parameters set this way apply to all queries executed within the session.
249
+ To change a session parameter for specific queries mid-session, use `ALTER SESSION`:
250
+
251
+ ```python
252
+ from sqlalchemy import text
253
+
254
+ with engine.connect() as conn:
255
+ conn.execute(text("ALTER SESSION SET QUERY_TAG = 'batch_job_1'"))
256
+ conn.execute(text("...")) # Uses 'batch_job_1'
257
+
258
+ conn.execute(text("ALTER SESSION SET QUERY_TAG = 'batch_job_2'"))
259
+ conn.execute(text("...")) # Uses 'batch_job_2'
260
+
261
+ conn.execute(text("ALTER SESSION UNSET QUERY_TAG"))
262
+ conn.execute(text("...")) # No tag
263
+ ```
264
+
187
265
  ### Opening and Closing Connection
188
266
 
189
267
  Open a connection by executing `engine.connect()`; avoid using `engine.execute()`. Make certain to close the connection by executing `connection.close()` before
@@ -303,6 +381,116 @@ The following `NumPy` data types are supported:
303
381
  - numpy.float64
304
382
  - numpy.datatime64
305
383
 
384
+ ### DECFLOAT Data Type Support
385
+
386
+ Snowflake SQLAlchemy supports the `DECFLOAT` data type, which provides decimal floating-point with up to 38 significant digits. For more information, see the [Snowflake DECFLOAT documentation](https://docs.snowflake.com/en/sql-reference/data-types-numeric#decfloat).
387
+
388
+ ```python
389
+ from sqlalchemy import Column, Integer, MetaData, Table
390
+ from snowflake.sqlalchemy import DECFLOAT
391
+
392
+ metadata = MetaData()
393
+ t = Table('my_table', metadata,
394
+ Column('id', Integer, primary_key=True),
395
+ Column('value', DECFLOAT()),
396
+ )
397
+ metadata.create_all(engine)
398
+ ```
399
+
400
+ #### DECFLOAT Precision
401
+
402
+ The Snowflake Python connector uses Python's `decimal` module context when converting `DECFLOAT` values to Python `Decimal` objects. Python's default decimal context precision is 28 digits, which can truncate `DECFLOAT` values that use up to 38 digits.
403
+
404
+ To preserve full 38-digit precision, add `enable_decfloat=True` to the connection URL:
405
+
406
+ ```python
407
+ from sqlalchemy import create_engine
408
+
409
+ engine = create_engine(
410
+ 'snowflake://testuser1:0123456@abc123/testdb/public?warehouse=testwh&enable_decfloat=True'
411
+ )
412
+ ```
413
+
414
+ Or using the `snowflake.sqlalchemy.URL` helper:
415
+
416
+ ```python
417
+ from snowflake.sqlalchemy import URL
418
+ from sqlalchemy import create_engine
419
+
420
+ engine = create_engine(URL(
421
+ account = 'abc123',
422
+ user = 'testuser1',
423
+ password = '0123456',
424
+ database = 'testdb',
425
+ schema = 'public',
426
+ warehouse = 'testwh',
427
+ enable_decfloat = True,
428
+ ))
429
+ ```
430
+
431
+ **Note**: `DECFLOAT` does not support special values (`inf`, `-inf`, `NaN`) unlike `FLOAT`.
432
+
433
+ **Why is `enable_decfloat` not enabled by default?** Enabling it sets `decimal.getcontext().prec = 38`, which modifies Python's thread-local decimal context and affects all `Decimal` operations in that thread, not just database queries. To avoid unexpected side effects on application code, the dialect emits a warning when `DECFLOAT` values are retrieved without full precision enabled, guiding users to opt-in explicitly.
434
+
435
+ ### VECTOR Data Type Support
436
+
437
+ Snowflake SQLAlchemy supports the `VECTOR` data type with varying element type and dimension.
438
+ For more information, see the [Snowflake documentation](https://docs.snowflake.com/en/sql-reference/data-types-vector).
439
+
440
+ ```python
441
+ from sqlalchemy import Column, Integer, Float, MetaData, Table
442
+ from snowflake.sqlalchemy import VECTOR
443
+
444
+ metadata = MetaData()
445
+ t = Table('my_table', metadata,
446
+ Column('id', Integer, primary_key=True),
447
+ Column('int_vec', VECTOR(Integer, 20)),
448
+ Column('float_vec', VECTOR(Float, 40)),
449
+ )
450
+ metadata.create_all(engine)
451
+ ```
452
+
453
+ ### Timestamp and Timezone Support
454
+
455
+ Snowflake SQLAlchemy provides three Snowflake-specific timestamp types that map directly to their Snowflake counterparts:
456
+
457
+ ```python
458
+ from sqlalchemy import Column, Integer, MetaData, Table, create_engine
459
+ from snowflake.sqlalchemy import TIMESTAMP_NTZ, TIMESTAMP_TZ, TIMESTAMP_LTZ
460
+
461
+ engine = create_engine(...)
462
+ metadata = MetaData()
463
+ t = Table('events', metadata,
464
+ Column('id', Integer, primary_key=True),
465
+ Column('created_at', TIMESTAMP_NTZ()), # TIMESTAMP WITHOUT TIME ZONE
466
+ Column('scheduled_at', TIMESTAMP_TZ()), # TIMESTAMP WITH TIME ZONE
467
+ Column('logged_at', TIMESTAMP_LTZ()), # TIMESTAMP WITH LOCAL TIME ZONE
468
+ )
469
+ metadata.create_all(engine)
470
+ ```
471
+
472
+ SQLAlchemy's generic `DateTime` and `TIMESTAMP` types also support timezone-aware columns via the `timezone` parameter. When `timezone=True` is set, the dialect emits `TIMESTAMP_TZ` instead of the default `TIMESTAMP_NTZ`:
473
+
474
+ ```python
475
+ from sqlalchemy import Column, DateTime, Integer, MetaData, Table, create_engine
476
+ from sqlalchemy.types import TIMESTAMP
477
+
478
+ engine = create_engine(...)
479
+ metadata = MetaData()
480
+ t = Table('events', metadata,
481
+ Column('id', Integer, primary_key=True),
482
+ Column('naive_ts', DateTime()), # produces TIMESTAMP_NTZ
483
+ Column('aware_ts', DateTime(timezone=True)), # produces TIMESTAMP_TZ
484
+ Column('naive_ts2', TIMESTAMP()), # produces TIMESTAMP_NTZ
485
+ Column('aware_ts2', TIMESTAMP(timezone=True)), # produces TIMESTAMP_TZ
486
+ )
487
+ metadata.create_all(engine)
488
+ ```
489
+
490
+ This also applies when using pandas `to_sql()` with timezone-aware datetime columns, which infers `DateTime(timezone=True)` automatically (see [#199](https://github.com/snowflakedb/snowflake-sqlalchemy/issues/199)).
491
+
492
+ **Note on `Time` and timezones:** SQLAlchemy's `Time` type accepts a `timezone` parameter, but [Snowflake's TIME data type does not support time zones](https://docs.snowflake.com/en/sql-reference/data-types-datetime#time). Using `Time(timezone=True)` will compile to plain `TIME` and the `timezone` flag will have no effect. If you need to store time data with time-zone information, use a timestamp type such as `TIMESTAMP_TZ` or `DateTime(timezone=True)` instead.
493
+
306
494
  ### Cache Column Metadata
307
495
 
308
496
  SQLAlchemy provides [the runtime inspection API](http://docs.sqlalchemy.org/en/latest/core/inspection.html) to get the runtime information about the various objects. One of the common use case is get all tables and their column metadata in a schema in order to construct a schema catalog. For example, [alembic](http://alembic.zzzcomputing.com/) on top of SQLAlchemy manages database schema migrations. A pseudo code flow is as follows:
@@ -22,5 +22,5 @@ echo "[Info] Building snowflake-sqlalchemy with $PYTHON"
22
22
  # Clean up possible build artifacts
23
23
  rm -rf build generated_version.py
24
24
  export UV_NO_CACHE=true
25
- ${PYTHON} -m pip install uv hatch
26
- ${PYTHON} -m hatch build
25
+ ${PYTHON} -m pip install uv
26
+ ${PYTHON} -m uv build
@@ -7,6 +7,8 @@
7
7
  # - This is the script that test_docker.sh runs inside of the docker container
8
8
 
9
9
  PYTHON_VERSIONS="${1:-3.8 3.9 3.10 3.11 3.12 3.13 3.14}"
10
+ # Python versions where pyarrow (required by pandas extra) is not available
11
+ PANDAS_SKIP_VERSIONS="3.14"
10
12
  THIS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
13
  SQLALCHEMY_DIR="$(dirname "${THIS_DIR}")"
12
14
 
@@ -19,7 +21,12 @@ for PYTHON_VERSION in ${PYTHON_VERSIONS}; do
19
21
  echo "[Info] Testing with ${PYTHON_VERSION}"
20
22
  SHORT_VERSION=$(python3 -c "print('${PYTHON_VERSION}'.replace('.', ''))")
21
23
  SQLALCHEMY_WHL=$(ls $SQLALCHEMY_DIR/dist/snowflake_sqlalchemy-*-py3-none-any.whl | sort -r | head -n 1)
22
- TEST_ENVLIST=fix_lint,py${SHORT_VERSION}-ci,py${SHORT_VERSION}-coverage,py${SHORT_VERSION}-pandas-ci,py${SHORT_VERSION}-pandas-coverage
24
+ TEST_ENVLIST=fix_lint,py${SHORT_VERSION}-ci
25
+ if [[ ! " ${PANDAS_SKIP_VERSIONS} " =~ " ${PYTHON_VERSION} " ]]; then
26
+ TEST_ENVLIST="${TEST_ENVLIST},py${SHORT_VERSION}-pandas-ci"
27
+ else
28
+ echo "[Info] Skipping pandas tests for Python ${PYTHON_VERSION} (pyarrow not available)"
29
+ fi
23
30
  echo "[Info] Running tox for ${TEST_ENVLIST}"
24
- python3 -m tox -e ${TEST_ENVLIST} --installpkg ${SQLALCHEMY_WHL}
31
+ python3 -m tox -p auto --parallel-no-spinner -e ${TEST_ENVLIST} --installpkg ${SQLALCHEMY_WHL}
25
32
  done
@@ -55,7 +55,6 @@ development = [
55
55
  "pytest-xdist",
56
56
  "pytz",
57
57
  "numpy",
58
- "mock",
59
58
  "syrupy",
60
59
  ]
61
60
  pandas = ["snowflake-connector-python[pandas]"]
@@ -85,7 +84,11 @@ installer = "uv"
85
84
  [tool.hatch.envs.sa14]
86
85
  installer = "uv"
87
86
  builder = true
88
- extra-dependencies = ["SQLAlchemy>=1.4.19,<2.0.0", "pandas<2.1", "numpy<2"]
87
+ extra-dependencies = [
88
+ "SQLAlchemy>=1.4.19,<2.0.0",
89
+ "pandas>=2.1.1,<2.2",
90
+ "numpy<2",
91
+ ]
89
92
  features = ["development", "pandas"]
90
93
  python = "3.12"
91
94
 
@@ -142,4 +145,5 @@ markers = [
142
145
  "external: tests that could but should only run on our external CI",
143
146
  "feature_max_lob_size: tests that could but should only run on our external CI",
144
147
  "feature_v20: tests that could but should only run on SqlAlchemy v20",
148
+ "mypy: typing tests",
145
149
  ]
@@ -46,6 +46,7 @@ from .custom_types import ( # noqa
46
46
  BYTEINT,
47
47
  CHARACTER,
48
48
  DEC,
49
+ DECFLOAT,
49
50
  DOUBLE,
50
51
  FIXED,
51
52
  GEOGRAPHY,
@@ -61,6 +62,7 @@ from .custom_types import ( # noqa
61
62
  TINYINT,
62
63
  VARBINARY,
63
64
  VARIANT,
65
+ VECTOR,
64
66
  )
65
67
  from .sql.custom_schema import ( # noqa
66
68
  DynamicTable,
@@ -93,6 +95,7 @@ _custom_types = (
93
95
  "DATE",
94
96
  "DATETIME",
95
97
  "DECIMAL",
98
+ "DECFLOAT",
96
99
  "FLOAT",
97
100
  "INT",
98
101
  "INTEGER",
@@ -120,6 +123,7 @@ _custom_types = (
120
123
  "TINYINT",
121
124
  "VARBINARY",
122
125
  "VARIANT",
126
+ "VECTOR",
123
127
  "MAP",
124
128
  )
125
129
 
@@ -7,7 +7,7 @@ import operator
7
7
  import re
8
8
  import string
9
9
  import warnings
10
- from typing import List
10
+ from typing import Any, List
11
11
 
12
12
  from sqlalchemy import exc as sa_exc
13
13
  from sqlalchemy import inspect, sql
@@ -16,7 +16,7 @@ from sqlalchemy.engine import default
16
16
  from sqlalchemy.orm import context
17
17
  from sqlalchemy.orm.context import _MapperEntity
18
18
  from sqlalchemy.schema import Sequence, Table
19
- from sqlalchemy.sql import compiler, expression, functions
19
+ from sqlalchemy.sql import compiler, expression, functions, sqltypes
20
20
  from sqlalchemy.sql.base import CompileState
21
21
  from sqlalchemy.sql.elements import BindParameter, quoted_name
22
22
  from sqlalchemy.sql.expression import Executable
@@ -526,6 +526,9 @@ class SnowflakeCompiler(compiler.SQLCompiler):
526
526
  def visit_now_func(self, now, **kw):
527
527
  return "CURRENT_TIMESTAMP"
528
528
 
529
+ def visit_sysdate_func(self, sysdate, **kw):
530
+ return "SYSDATE()"
531
+
529
532
  def visit_merge_into(self, merge_into, **kw):
530
533
  clauses = " ".join(
531
534
  clause._compiler_dispatch(self, **kw) for clause in merge_into.clauses
@@ -779,6 +782,24 @@ class SnowflakeCompiler(compiler.SQLCompiler):
779
782
  def visit_not_regexp_match_op_binary(self, binary, operator, **kw):
780
783
  return f"NOT {self.visit_regexp_match_op_binary(binary, operator, **kw)}"
781
784
 
785
+ def visit_ilike_op_binary(self, binary, operator, **kw):
786
+ return self._render_ilike(binary, negate=False, **kw)
787
+
788
+ def visit_not_ilike_op_binary(self, binary, operator, **kw):
789
+ return self._render_ilike(binary, negate=True, **kw)
790
+
791
+ def _render_ilike(self, binary, negate=False, **kw):
792
+ left = binary.left._compiler_dispatch(self, **kw)
793
+ right = binary.right._compiler_dispatch(self, **kw)
794
+ escape = binary.modifiers.get("escape")
795
+ escape_clause = (
796
+ " ESCAPE " + self.render_literal_value(escape, sqltypes.STRINGTYPE)
797
+ if escape is not None
798
+ else ""
799
+ )
800
+ operator = "NOT ILIKE" if negate else "ILIKE"
801
+ return f"{left} {operator} {right}{escape_clause}"
802
+
782
803
  def visit_join(self, join, asfrom=False, from_linter=None, **kwargs):
783
804
  if from_linter:
784
805
  from_linter.edges.update(
@@ -1146,7 +1167,6 @@ class SnowflakeTypeCompiler(compiler.GenericTypeCompiler):
1146
1167
  else:
1147
1168
  contents = []
1148
1169
  for key in type_.items_types:
1149
-
1150
1170
  row_text = f"{key} {type_.items_types[key][0].compile()}"
1151
1171
  # Type and not null is specified
1152
1172
  if len(type_.items_types[key]) > 1:
@@ -1157,10 +1177,14 @@ class SnowflakeTypeCompiler(compiler.GenericTypeCompiler):
1157
1177
  def visit_BLOB(self, type_, **kw):
1158
1178
  return "BINARY"
1159
1179
 
1160
- def visit_datetime(self, type_, **kw):
1180
+ def visit_datetime(self, type_: sqltypes.DateTime, **kw: Any) -> str:
1181
+ if type_.timezone:
1182
+ return "TIMESTAMP_TZ"
1161
1183
  return "datetime"
1162
1184
 
1163
- def visit_DATETIME(self, type_, **kw):
1185
+ def visit_DATETIME(self, type_: sqltypes.DateTime, **kw: Any) -> str:
1186
+ if type_.timezone:
1187
+ return "TIMESTAMP_TZ"
1164
1188
  return "DATETIME"
1165
1189
 
1166
1190
  def visit_TIMESTAMP_NTZ(self, type_, **kw):
@@ -1172,7 +1196,9 @@ class SnowflakeTypeCompiler(compiler.GenericTypeCompiler):
1172
1196
  def visit_TIMESTAMP_LTZ(self, type_, **kw):
1173
1197
  return "TIMESTAMP_LTZ"
1174
1198
 
1175
- def visit_TIMESTAMP(self, type_, **kw):
1199
+ def visit_TIMESTAMP(self, type_: sqltypes.TIMESTAMP, **kw: Any) -> str:
1200
+ if type_.timezone:
1201
+ return "TIMESTAMP_TZ"
1176
1202
  return "TIMESTAMP"
1177
1203
 
1178
1204
  def visit_GEOGRAPHY(self, type_, **kw):
@@ -1181,6 +1207,12 @@ class SnowflakeTypeCompiler(compiler.GenericTypeCompiler):
1181
1207
  def visit_GEOMETRY(self, type_, **kw):
1182
1208
  return "GEOMETRY"
1183
1209
 
1210
+ def visit_DECFLOAT(self, type_, **kw):
1211
+ return "DECFLOAT"
1212
+
1213
+ def visit_VECTOR(self, type_, **kw):
1214
+ return f"VECTOR({type_.element_type}, {type_.dimension})"
1215
+
1184
1216
 
1185
1217
  construct_arguments = [(Table, {"clusterby": None})]
1186
1218
 
@@ -11,7 +11,7 @@ from sqlalchemy import util
11
11
  string_types = (str,)
12
12
  returns_unicode = util.symbol("RETURNS_UNICODE")
13
13
 
14
- IS_VERSION_20 = tuple(int(v) for v in SA_VERSION.split(".")) >= (2, 0, 0)
14
+ IS_VERSION_20 = tuple(int(v) for v in SA_VERSION.split(".")[:2]) >= (2, 0)
15
15
 
16
16
 
17
17
  def args_reducer(positions_to_drop: tuple):