snowflake-sqlalchemy 1.7.6__tar.gz → 1.8.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 (131) hide show
  1. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/DESCRIPTION.md +11 -0
  2. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/PKG-INFO +3 -5
  3. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/README.md +0 -3
  4. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/ci/docker/sqlalchemy_build/Dockerfile +10 -3
  5. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/ci/test_docker.sh +19 -19
  6. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/ci/test_linux.sh +2 -2
  7. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/pyproject.toml +8 -7
  8. snowflake_sqlalchemy-1.8.0/snyk/requirements.txt +2 -0
  9. snowflake_sqlalchemy-1.8.0/src/snowflake/sqlalchemy/name_utils.py +36 -0
  10. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/parser/custom_type_parser.py +3 -3
  11. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/snowdialect.py +123 -163
  12. snowflake_sqlalchemy-1.8.0/src/snowflake/sqlalchemy/structured_type_info_manager.py +142 -0
  13. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/version.py +1 -1
  14. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/conftest.py +26 -34
  15. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/sqlalchemy_test_suite/test_suite_20.py +71 -11
  16. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/test_core.py +40 -23
  17. snowflake_sqlalchemy-1.8.0/tests/test_dialect_connect.py +124 -0
  18. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/test_pandas.py +3 -1
  19. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/test_qmark.py +3 -1
  20. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/test_structured_datatypes.py +53 -0
  21. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/test_transactions.py +3 -2
  22. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tox.ini +10 -4
  23. snowflake_sqlalchemy-1.7.6/snyk/requirements.txt +0 -2
  24. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/.gitignore +0 -0
  25. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/.gitmodules +0 -0
  26. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/.pre-commit-config.yaml +0 -0
  27. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/LICENSE.txt +0 -0
  28. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/MANIFEST.in +0 -0
  29. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/ci/build.sh +0 -0
  30. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/ci/build_docker.sh +0 -0
  31. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/ci/docker/sqlalchemy_build/scripts/entrypoint.sh +0 -0
  32. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/ci/set_base_image.sh +0 -0
  33. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/ci/test.sh +0 -0
  34. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/license_header.txt +0 -0
  35. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/setup.cfg +0 -0
  36. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/snyk/requiremtnts.txt +0 -0
  37. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/snyk/update_requirements.py +0 -0
  38. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/__init__.py +0 -0
  39. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/_constants.py +0 -0
  40. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/base.py +0 -0
  41. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/compat.py +0 -0
  42. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/custom_commands.py +0 -0
  43. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/custom_types.py +0 -0
  44. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/exc.py +0 -0
  45. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/functions.py +0 -0
  46. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/provision.py +0 -0
  47. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/requirements.py +0 -0
  48. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/sql/__init__.py +0 -0
  49. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/sql/custom_schema/__init__.py +0 -0
  50. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/sql/custom_schema/clustered_table.py +0 -0
  51. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/sql/custom_schema/custom_table_base.py +0 -0
  52. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/sql/custom_schema/custom_table_prefix.py +0 -0
  53. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/sql/custom_schema/dynamic_table.py +0 -0
  54. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/sql/custom_schema/hybrid_table.py +0 -0
  55. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/sql/custom_schema/iceberg_table.py +0 -0
  56. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/sql/custom_schema/options/__init__.py +0 -0
  57. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/sql/custom_schema/options/as_query_option.py +0 -0
  58. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/sql/custom_schema/options/cluster_by_option.py +0 -0
  59. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/sql/custom_schema/options/identifier_option.py +0 -0
  60. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/sql/custom_schema/options/invalid_table_option.py +0 -0
  61. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/sql/custom_schema/options/keyword_option.py +0 -0
  62. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/sql/custom_schema/options/keywords.py +0 -0
  63. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/sql/custom_schema/options/literal_option.py +0 -0
  64. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/sql/custom_schema/options/table_option.py +0 -0
  65. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/sql/custom_schema/options/target_lag_option.py +0 -0
  66. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/sql/custom_schema/snowflake_table.py +0 -0
  67. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/sql/custom_schema/table_from_query.py +0 -0
  68. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/util.py +0 -0
  69. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tested_requirements/requirements_310.reqs +0 -0
  70. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tested_requirements/requirements_37.reqs +0 -0
  71. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tested_requirements/requirements_38.reqs +0 -0
  72. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tested_requirements/requirements_39.reqs +0 -0
  73. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/README.rst +0 -0
  74. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/__init__.py +0 -0
  75. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/__snapshots__/test_compile_dynamic_table.ambr +0 -0
  76. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/__snapshots__/test_core.ambr +0 -0
  77. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/__snapshots__/test_orm.ambr +0 -0
  78. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/__snapshots__/test_reflect_dynamic_table.ambr +0 -0
  79. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/__snapshots__/test_structured_datatypes.ambr +0 -0
  80. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/__snapshots__/test_unit_structured_types.ambr +0 -0
  81. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/__init__.py +0 -0
  82. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/__snapshots__/test_compile_dynamic_table.ambr +0 -0
  83. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/__snapshots__/test_compile_hybrid_table.ambr +0 -0
  84. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/__snapshots__/test_compile_iceberg_table.ambr +0 -0
  85. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/__snapshots__/test_compile_snowflake_table.ambr +0 -0
  86. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/__snapshots__/test_create_dynamic_table.ambr +0 -0
  87. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/__snapshots__/test_create_hybrid_table.ambr +0 -0
  88. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/__snapshots__/test_create_iceberg_table.ambr +0 -0
  89. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/__snapshots__/test_create_snowflake_table.ambr +0 -0
  90. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/__snapshots__/test_generic_options.ambr +0 -0
  91. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/__snapshots__/test_reflect_hybrid_table.ambr +0 -0
  92. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/__snapshots__/test_reflect_snowflake_table.ambr +0 -0
  93. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/test_compile_dynamic_table.py +0 -0
  94. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/test_compile_hybrid_table.py +0 -0
  95. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/test_compile_iceberg_table.py +0 -0
  96. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/test_compile_snowflake_table.py +0 -0
  97. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/test_create_dynamic_table.py +0 -0
  98. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/test_create_hybrid_table.py +0 -0
  99. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/test_create_iceberg_table.py +0 -0
  100. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/test_create_snowflake_table.py +0 -0
  101. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/test_generic_options.py +0 -0
  102. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/test_reflect_dynamic_table.py +0 -0
  103. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/test_reflect_hybrid_table.py +0 -0
  104. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/test_reflect_snowflake_table.py +0 -0
  105. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/data/users.txt +0 -0
  106. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/sqlalchemy_test_suite/README.md +0 -0
  107. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/sqlalchemy_test_suite/__init__.py +0 -0
  108. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/sqlalchemy_test_suite/conftest.py +0 -0
  109. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/sqlalchemy_test_suite/test_suite.py +0 -0
  110. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/test_compiler.py +0 -0
  111. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/test_copy.py +0 -0
  112. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/test_create.py +0 -0
  113. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/test_custom_functions.py +0 -0
  114. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/test_custom_types.py +0 -0
  115. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/test_geography.py +0 -0
  116. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/test_geometry.py +0 -0
  117. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/test_imports.py +0 -0
  118. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/test_index_reflection.py +0 -0
  119. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/test_multivalues_insert.py +0 -0
  120. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/test_orm.py +0 -0
  121. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/test_quote.py +0 -0
  122. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/test_quote_identifiers.py +0 -0
  123. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/test_semi_structured_datatypes.py +0 -0
  124. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/test_sequence.py +0 -0
  125. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/test_timestamp.py +0 -0
  126. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/test_unit_core.py +0 -0
  127. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/test_unit_cte.py +0 -0
  128. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/test_unit_structured_types.py +0 -0
  129. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/test_unit_types.py +0 -0
  130. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/test_unit_url.py +0 -0
  131. {snowflake_sqlalchemy-1.7.6 → snowflake_sqlalchemy-1.8.0}/tests/util.py +0 -0
@@ -6,9 +6,20 @@ Snowflake Documentation is available at:
6
6
 
7
7
  Source code is also available at:
8
8
  <https://github.com/snowflakedb/snowflake-sqlalchemy>
9
+
9
10
  # Unreleased Notes
10
11
 
11
12
  # Release Notes
13
+
14
+ - v1.8.0(December 5, 2025)
15
+ - Add logging of SQLAlchemy version
16
+ - Bump `snowflake-connector-python<5.0.0`
17
+ - Add python up to 3.14
18
+ - Add logging of SQLAlchemy version and pandas (if used)
19
+
20
+ - v1.7.7(September 3, 2025)
21
+ - Fix exception for structured type columns dropped while collecting metadata
22
+
12
23
  - v1.7.6(July 10, 2025)
13
24
  - Fix get_multi_indexes issue, wrong assign of returned indexes when processing multiple indexes in a table
14
25
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: snowflake-sqlalchemy
3
- Version: 1.7.6
3
+ Version: 1.8.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
@@ -27,6 +27,7 @@ Classifier: Programming Language :: Python :: 3.9
27
27
  Classifier: Programming Language :: Python :: 3.10
28
28
  Classifier: Programming Language :: Python :: 3.11
29
29
  Classifier: Programming Language :: Python :: 3.12
30
+ Classifier: Programming Language :: Python :: 3.13
30
31
  Classifier: Programming Language :: SQL
31
32
  Classifier: Topic :: Database
32
33
  Classifier: Topic :: Scientific/Engineering :: Information Analysis
@@ -35,7 +36,7 @@ Classifier: Topic :: Software Development :: Libraries
35
36
  Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
36
37
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
37
38
  Requires-Python: >=3.8
38
- Requires-Dist: snowflake-connector-python<4.0.0
39
+ Requires-Dist: snowflake-connector-python<5.0.0
39
40
  Requires-Dist: sqlalchemy>=1.4.19
40
41
  Provides-Extra: development
41
42
  Requires-Dist: mock; extra == 'development'
@@ -63,9 +64,6 @@ Description-Content-Type: text/markdown
63
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.
64
65
 
65
66
 
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
- |---------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
68
-
69
67
 
70
68
  ## Prerequisites
71
69
 
@@ -9,9 +9,6 @@
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: | 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
- |---------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
14
-
15
12
 
16
13
  ## Prerequisites
17
14
 
@@ -4,9 +4,9 @@ FROM $BASE_IMAGE
4
4
 
5
5
  # This is to solve permission issue, read https://denibertovic.com/posts/handling-permissions-with-docker-volumes/
6
6
  ARG GOSU_URL=https://github.com/tianon/gosu/releases/download/1.14/gosu-amd64
7
- ENV GOSU_PATH $GOSU_URL
7
+ ENV GOSU_PATH=$GOSU_URL
8
8
  RUN curl -o /usr/local/bin/gosu -SL $GOSU_PATH \
9
- && chmod +x /usr/local/bin/gosu
9
+ && chmod +x /usr/local/bin/gosu
10
10
 
11
11
  COPY scripts/entrypoint.sh /usr/local/bin/entrypoint.sh
12
12
  RUN chmod +x /usr/local/bin/entrypoint.sh
@@ -14,6 +14,13 @@ RUN chmod +x /usr/local/bin/entrypoint.sh
14
14
  WORKDIR /home/user
15
15
  RUN chmod 777 /home/user
16
16
 
17
- ENV PATH="${PATH}:/opt/python/cp37-cp37m/bin:/opt/python/cp38-cp38/bin:/opt/python/cp39-cp39/bin:/opt/python/cp310-cp310/bin:/opt/python/cp311-cp311/bin"
17
+ ENV PATH="${PATH}:/opt/python/cp37-cp37/bin"
18
+ ENV PATH="${PATH}:/opt/python/cp38-cp38/bin"
19
+ ENV PATH="${PATH}:/opt/python/cp39-cp39/bin"
20
+ ENV PATH="${PATH}:/opt/python/cp310-cp310/bin"
21
+ ENV PATH="${PATH}:/opt/python/cp311-cp311/bin"
22
+ ENV PATH="${PATH}:/opt/python/cp312-cp312/bin"
23
+ ENV PATH="${PATH}:/opt/python/cp313-cp313/bin"
24
+ ENV PATH="${PATH}:/opt/python/cp314-cp314/bin"
18
25
 
19
26
  ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
@@ -7,11 +7,11 @@
7
7
  set -o pipefail
8
8
 
9
9
  # In case this is ran from dev-vm
10
- PYTHON_ENV=${1:-3.7}
10
+ PYTHON_ENV=${1:-3.8}
11
11
 
12
12
  # Set constants
13
- THIS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
14
- SQLALCHEMY_DIR="$( dirname "${THIS_DIR}")"
13
+ THIS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
+ SQLALCHEMY_DIR="$(dirname "${THIS_DIR}")"
15
15
  WORKSPACE=${WORKSPACE:-$SQLALCHEMY_DIR}
16
16
  source $THIS_DIR/set_base_image.sh
17
17
 
@@ -35,19 +35,19 @@ echo "[Info] Start building docker image and testing"
35
35
 
36
36
  user_id=$(id -u ${USER})
37
37
  docker run \
38
- --rm \
39
- --network=host \
40
- -e TERM=vt102 \
41
- -e PIP_DISABLE_PIP_VERSION_CHECK=1 \
42
- -e OPENSSL_FIPS=1 \
43
- -e LOCAL_USER_ID=${user_id} \
44
- -e AWS_ACCESS_KEY_ID \
45
- -e AWS_SECRET_ACCESS_KEY \
46
- -e SF_REGRESS_LOGS \
47
- -e SF_PROJECT_ROOT \
48
- -e cloud_provider \
49
- -e JENKINS_HOME \
50
- -e GITHUB_ACTIONS \
51
- --mount type=bind,source="${SQLALCHEMY_DIR}",target=/home/user/snowflake-sqlalchemy \
52
- $(docker build --pull --build-arg BASE_IMAGE=$BASE_IMAGE --build-arg GOSU_URL="$GOSU_URL" -q .) \
53
- /home/user/snowflake-sqlalchemy/ci/test_linux.sh ${PYTHON_ENV}
38
+ --rm \
39
+ --network=host \
40
+ -e TERM=vt102 \
41
+ -e PIP_DISABLE_PIP_VERSION_CHECK=1 \
42
+ -e OPENSSL_FIPS=1 \
43
+ -e LOCAL_USER_ID=${user_id} \
44
+ -e AWS_ACCESS_KEY_ID \
45
+ -e AWS_SECRET_ACCESS_KEY \
46
+ -e SF_REGRESS_LOGS \
47
+ -e SF_PROJECT_ROOT \
48
+ -e cloud_provider \
49
+ -e JENKINS_HOME \
50
+ -e GITHUB_ACTIONS \
51
+ --mount type=bind,source="${SQLALCHEMY_DIR}",target=/home/user/snowflake-sqlalchemy \
52
+ $(docker build --pull --build-arg BASE_IMAGE=$BASE_IMAGE --build-arg GOSU_URL="$GOSU_URL" -q .) \
53
+ /home/user/snowflake-sqlalchemy/ci/test_linux.sh ${PYTHON_ENV}
@@ -6,7 +6,7 @@
6
6
  # - This script assumes that ../dist/repaired_wheels has the wheel(s) built for all versions to be tested
7
7
  # - This is the script that test_docker.sh runs inside of the docker container
8
8
 
9
- PYTHON_VERSIONS="${1:-3.8 3.9 3.10 3.11}"
9
+ PYTHON_VERSIONS="${1:-3.8 3.9 3.10 3.11 3.12 3.13 3.14}"
10
10
  THIS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  SQLALCHEMY_DIR="$(dirname "${THIS_DIR}")"
12
12
 
@@ -19,7 +19,7 @@ for PYTHON_VERSION in ${PYTHON_VERSIONS}; do
19
19
  echo "[Info] Testing with ${PYTHON_VERSION}"
20
20
  SHORT_VERSION=$(python3 -c "print('${PYTHON_VERSION}'.replace('.', ''))")
21
21
  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
22
+ TEST_ENVLIST=fix_lint,py${SHORT_VERSION}-ci,py${SHORT_VERSION}-coverage,py${SHORT_VERSION}-pandas-ci,py${SHORT_VERSION}-pandas-coverage
23
23
  echo "[Info] Running tox for ${TEST_ENVLIST}"
24
24
  python3 -m tox -e ${TEST_ENVLIST} --installpkg ${SQLALCHEMY_WHL}
25
25
  done
@@ -30,6 +30,7 @@ classifiers = [
30
30
  "Programming Language :: Python :: 3.10",
31
31
  "Programming Language :: Python :: 3.11",
32
32
  "Programming Language :: Python :: 3.12",
33
+ "Programming Language :: Python :: 3.13",
33
34
  "Programming Language :: SQL",
34
35
  "Topic :: Database",
35
36
  "Topic :: Scientific/Engineering :: Information Analysis",
@@ -38,7 +39,7 @@ classifiers = [
38
39
  "Topic :: Software Development :: Libraries :: Application Frameworks",
39
40
  "Topic :: Software Development :: Libraries :: Python Modules",
40
41
  ]
41
- dependencies = ["SQLAlchemy>=1.4.19", "snowflake-connector-python<4.0.0"]
42
+ dependencies = ["SQLAlchemy>=1.4.19", "snowflake-connector-python<5.0.0"]
42
43
 
43
44
  [tool.hatch.version]
44
45
  path = "src/snowflake/sqlalchemy/version.py"
@@ -75,17 +76,17 @@ exclude = ["/.github"]
75
76
  packages = ["src/snowflake"]
76
77
 
77
78
  [tool.hatch.envs.default]
78
- path = ".venv"
79
- type = "virtual"
80
79
  extra-dependencies = ["SQLAlchemy>=1.4.19,<2.1.0"]
81
80
  features = ["development", "pandas"]
82
- python = "3.8"
81
+ python = "3.12"
83
82
  installer = "uv"
84
83
 
85
84
  [tool.hatch.envs.sa14]
86
- extra-dependencies = ["SQLAlchemy>=1.4.19,<2.0.0"]
85
+ installer = "uv"
86
+ builder = true
87
+ extra-dependencies = ["SQLAlchemy>=1.4.19,<2.0.0", "pandas<2.1", "numpy<2"]
87
88
  features = ["development", "pandas"]
88
- python = "3.8"
89
+ python = "3.12"
89
90
 
90
91
  [tool.hatch.envs.sa14.scripts]
91
92
  test-dialect = "pytest --ignore_v20_test -ra -vvv --tb=short --cov snowflake.sqlalchemy --cov-append --junitxml ./junit.xml --ignore=tests/sqlalchemy_test_suite tests/"
@@ -105,7 +106,7 @@ gh-cache-sum = "python -VV | sha256sum | cut -d' ' -f1"
105
106
  check-import = "python -c 'import snowflake.sqlalchemy; print(snowflake.sqlalchemy.__version__)'"
106
107
 
107
108
  [[tool.hatch.envs.release.matrix]]
108
- python = ["3.8", "3.9", "3.10", "3.11", "3.12"]
109
+ python = ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
109
110
  features = ["development", "pandas"]
110
111
 
111
112
  [tool.hatch.envs.release.scripts]
@@ -0,0 +1,2 @@
1
+ SQLAlchemy>=1.4.19
2
+ snowflake-connector-python<5.0.0
@@ -0,0 +1,36 @@
1
+ #
2
+ # Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved.
3
+
4
+ from sqlalchemy.sql.compiler import IdentifierPreparer
5
+ from sqlalchemy.sql.elements import quoted_name
6
+
7
+
8
+ class _NameUtils:
9
+
10
+ def __init__(self, identifier_preparer: IdentifierPreparer) -> None:
11
+ self.identifier_preparer = identifier_preparer
12
+
13
+ def normalize_name(self, name):
14
+ if name is None:
15
+ return None
16
+ if name == "":
17
+ return ""
18
+ if name.upper() == name and not self.identifier_preparer._requires_quotes(
19
+ name.lower()
20
+ ):
21
+ return name.lower()
22
+ elif name.lower() == name:
23
+ return quoted_name(name, quote=True)
24
+ else:
25
+ return name
26
+
27
+ def denormalize_name(self, name):
28
+ if name is None:
29
+ return None
30
+ if name == "":
31
+ return ""
32
+ elif name.lower() == name and not self.identifier_preparer._requires_quotes(
33
+ name.lower()
34
+ ):
35
+ name = name.upper()
36
+ return name
@@ -2,9 +2,8 @@
2
2
  # Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved.
3
3
  from typing import List
4
4
 
5
- import sqlalchemy.types as sqltypes
6
- from sqlalchemy.sql.type_api import TypeEngine
7
- from sqlalchemy.types import (
5
+ import sqlalchemy.sql.sqltypes as sqltypes
6
+ from sqlalchemy.sql.sqltypes import (
8
7
  BIGINT,
9
8
  BINARY,
10
9
  BOOLEAN,
@@ -21,6 +20,7 @@ from sqlalchemy.types import (
21
20
  VARCHAR,
22
21
  NullType,
23
22
  )
23
+ from sqlalchemy.sql.type_api import TypeEngine
24
24
 
25
25
  from ..custom_types import (
26
26
  _CUSTOM_DECIMAL,
@@ -2,28 +2,33 @@
2
2
  # Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved.
3
3
  #
4
4
  import operator
5
- import re
6
5
  from collections import defaultdict
7
6
  from enum import Enum
8
7
  from functools import reduce
9
- from typing import Any, Collection, Optional
8
+ from logging import getLogger
9
+ from time import time as time_in_seconds
10
+ from typing import Any, Collection, Optional, cast
10
11
  from urllib.parse import unquote_plus
11
12
 
12
13
  import sqlalchemy.sql.sqltypes as sqltypes
14
+ from sqlalchemy import __version__ as SQLALCHEMY_VERSION
13
15
  from sqlalchemy import event as sa_vnt
14
16
  from sqlalchemy import exc as sa_exc
15
17
  from sqlalchemy import util as sa_util
16
18
  from sqlalchemy.engine import URL, default, reflection
17
19
  from sqlalchemy.schema import Table
18
20
  from sqlalchemy.sql import text
19
- from sqlalchemy.sql.elements import quoted_name
20
21
  from sqlalchemy.sql.sqltypes import NullType
21
22
  from sqlalchemy.types import FLOAT, Date, DateTime, Float, Time
22
23
 
23
24
  from snowflake.connector import errors as sf_errors
24
- from snowflake.connector.connection import DEFAULT_CONFIGURATION
25
+ from snowflake.connector.connection import DEFAULT_CONFIGURATION, SnowflakeConnection
25
26
  from snowflake.connector.constants import UTF8
27
+ from snowflake.connector.network import SnowflakeRestful
28
+ from snowflake.connector.telemetry import TelemetryClient, TelemetryData, TelemetryField
26
29
  from snowflake.sqlalchemy.compat import returns_unicode
30
+ from snowflake.sqlalchemy.name_utils import _NameUtils
31
+ from snowflake.sqlalchemy.structured_type_info_manager import _StructuredTypeInfoManager
27
32
 
28
33
  from ._constants import DIALECT_NAME
29
34
  from .base import (
@@ -42,7 +47,7 @@ from .custom_types import (
42
47
  )
43
48
  from .parser.custom_type_parser import * # noqa
44
49
  from .parser.custom_type_parser import _CUSTOM_DECIMAL # noqa
45
- from .parser.custom_type_parser import ischema_names, parse_index_columns, parse_type
50
+ from .parser.custom_type_parser import ischema_names, parse_index_columns
46
51
  from .sql.custom_schema.custom_table_prefix import CustomTablePrefix
47
52
  from .util import (
48
53
  _update_connection_application_name,
@@ -59,6 +64,12 @@ colspecs = {
59
64
 
60
65
  _ENABLE_SQLALCHEMY_AS_APPLICATION_NAME = True
61
66
 
67
+ logger = getLogger(__name__)
68
+
69
+
70
+ class TelemetryEvents(Enum):
71
+ NEW_CONNECTION = "sqlalchemy_new_connection"
72
+
62
73
 
63
74
  class SnowflakeIsolationLevel(Enum):
64
75
  READ_COMMITTED = "READ COMMITTED"
@@ -157,6 +168,7 @@ class SnowflakeDialect(default.DefaultDialect):
157
168
  super().__init__(isolation_level=isolation_level, **kwargs)
158
169
  self.force_div_is_floordiv = force_div_is_floordiv
159
170
  self.div_is_floordiv = force_div_is_floordiv
171
+ self.name_utils = _NameUtils(self.identifier_preparer)
160
172
 
161
173
  def initialize(self, connection):
162
174
  super().initialize(connection)
@@ -282,29 +294,10 @@ class SnowflakeDialect(default.DefaultDialect):
282
294
  raise
283
295
 
284
296
  def normalize_name(self, name):
285
- if name is None:
286
- return None
287
- if name == "":
288
- return ""
289
- if name.upper() == name and not self.identifier_preparer._requires_quotes(
290
- name.lower()
291
- ):
292
- return name.lower()
293
- elif name.lower() == name:
294
- return quoted_name(name, quote=True)
295
- else:
296
- return name
297
+ return self.name_utils.normalize_name(name)
297
298
 
298
299
  def denormalize_name(self, name):
299
- if name is None:
300
- return None
301
- if name == "":
302
- return ""
303
- elif name.lower() == name and not self.identifier_preparer._requires_quotes(
304
- name.lower()
305
- ):
306
- name = name.upper()
307
- return name
300
+ return self.name_utils.denormalize_name(name)
308
301
 
309
302
  def _denormalize_quote_join(self, *idents):
310
303
  ip = self.identifier_preparer
@@ -491,53 +484,31 @@ class SnowflakeDialect(default.DefaultDialect):
491
484
  )
492
485
  return foreign_key_map.get(table_name, [])
493
486
 
494
- def table_columns_as_dict(self, columns):
495
- result = {}
496
- for column in columns:
497
- result[column["name"]] = column
498
- return result
499
-
500
487
  @reflection.cache
501
488
  def _get_schema_columns(self, connection, schema, **kw):
502
489
  """Get all columns in the schema, if we hit 'Information schema query returned too much data' problem return
503
490
  None, as it is cacheable and is an unexpected return type for this function"""
504
491
  ans = {}
505
- current_database, _ = self._current_database_schema(connection, **kw)
492
+
493
+ schema_name = self.denormalize_name(schema)
494
+
495
+ result = self._query_all_columns_info(connection, schema_name, **kw)
496
+ if result is None:
497
+ return None
498
+
499
+ current_database, default_schema = self._current_database_schema(
500
+ connection, **kw
501
+ )
506
502
  full_schema_name = self._denormalize_quote_join(current_database, schema)
507
- full_columns_descriptions = {}
508
- try:
509
- schema_primary_keys = self._get_schema_primary_keys(
510
- connection, full_schema_name, **kw
511
- )
512
- schema_name = self.denormalize_name(schema)
513
503
 
514
- result = connection.execute(
515
- text(
516
- """
517
- SELECT /* sqlalchemy:_get_schema_columns */
518
- ic.table_name,
519
- ic.column_name,
520
- ic.data_type,
521
- ic.character_maximum_length,
522
- ic.numeric_precision,
523
- ic.numeric_scale,
524
- ic.is_nullable,
525
- ic.column_default,
526
- ic.is_identity,
527
- ic.comment,
528
- ic.identity_start,
529
- ic.identity_increment
530
- FROM information_schema.columns ic
531
- WHERE ic.table_schema=:table_schema
532
- ORDER BY ic.ordinal_position"""
533
- ),
534
- {"table_schema": schema_name},
535
- )
536
- except sa_exc.ProgrammingError as pe:
537
- if pe.orig.errno == 90030:
538
- # This means that there are too many tables in the schema, we need to go more granular
539
- return None # None triggers _get_table_columns while staying cacheable
540
- raise
504
+ schema_primary_keys = self._get_schema_primary_keys(
505
+ connection, full_schema_name, **kw
506
+ )
507
+
508
+ structured_type_info_manager = _StructuredTypeInfoManager(
509
+ connection, self.name_utils, default_schema
510
+ )
511
+
541
512
  for (
542
513
  table_name,
543
514
  column_name,
@@ -572,25 +543,11 @@ class SnowflakeDialect(default.DefaultDialect):
572
543
  elif issubclass(col_type, (sqltypes.String, sqltypes.BINARY)):
573
544
  col_type_kw["length"] = character_maximum_length
574
545
  elif issubclass(col_type, StructuredType):
575
- if (schema_name, table_name) not in full_columns_descriptions:
576
- full_columns_descriptions[(schema_name, table_name)] = (
577
- self.table_columns_as_dict(
578
- self._get_table_columns(
579
- connection, table_name, schema_name
580
- )
581
- )
582
- )
583
-
584
- if (
585
- (schema_name, table_name) in full_columns_descriptions
586
- and column_name
587
- in full_columns_descriptions[(schema_name, table_name)]
588
- ):
589
- ans[table_name].append(
590
- full_columns_descriptions[(schema_name, table_name)][
591
- column_name
592
- ]
593
- )
546
+ column_info = structured_type_info_manager.get_column_info(
547
+ schema_name, table_name, column_name, **kw
548
+ )
549
+ if column_info:
550
+ ans[table_name].append(column_info)
594
551
  continue
595
552
  else:
596
553
  col_type = NullType
@@ -628,72 +585,6 @@ class SnowflakeDialect(default.DefaultDialect):
628
585
  }
629
586
  return ans
630
587
 
631
- @reflection.cache
632
- def _get_table_columns(self, connection, table_name, schema=None, **kw):
633
- """Get all columns in a table in a schema"""
634
- ans = []
635
- current_database, default_schema = self._current_database_schema(
636
- connection, **kw
637
- )
638
- schema = schema if schema else default_schema
639
- table_schema = self.denormalize_name(schema)
640
- table_name = self.denormalize_name(table_name)
641
- result = connection.execute(
642
- text(
643
- "DESC /* sqlalchemy:_get_schema_columns */"
644
- f" TABLE {table_schema}.{table_name} TYPE = COLUMNS"
645
- )
646
- )
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]
654
-
655
- column_name = self.normalize_name(column_name)
656
- if column_name.startswith("sys_clustering_column"):
657
- continue # ignoring clustering column
658
- type_instance = parse_type(coltype)
659
- if isinstance(type_instance, NullType):
660
- sa_util.warn(
661
- f"Did not recognize type '{coltype}' of column '{column_name}'"
662
- )
663
-
664
- identity = None
665
- match = re.match(
666
- r"IDENTITY START (?P<start>\d+) INCREMENT (?P<increment>\d+) (?P<order_type>ORDER|NOORDER)",
667
- column_default if column_default else "",
668
- )
669
- if match:
670
- identity = {
671
- "start": int(match.group("start")),
672
- "increment": int(match.group("increment")),
673
- "order_type": match.group("order_type"),
674
- }
675
- is_identity = identity is not None
676
-
677
- ans.append(
678
- {
679
- "name": column_name,
680
- "type": type_instance,
681
- "nullable": is_nullable == "Y",
682
- "default": None if is_identity else column_default,
683
- "autoincrement": is_identity,
684
- "comment": comment if comment != "" else None,
685
- "primary_key": primary_key == "Y",
686
- }
687
- )
688
-
689
- if is_identity:
690
- ans[-1]["identity"] = identity
691
-
692
- # If we didn't find any columns for the table, the table doesn't exist.
693
- if len(ans) == 0:
694
- raise sa_exc.NoSuchTableError()
695
- return ans
696
-
697
588
  def get_columns(self, connection, table_name, schema=None, **kw):
698
589
  """
699
590
  Gets all column info given the table info
@@ -704,8 +595,11 @@ class SnowflakeDialect(default.DefaultDialect):
704
595
 
705
596
  schema_columns = self._get_schema_columns(connection, schema, **kw)
706
597
  if schema_columns is None:
598
+ column_info_manager = _StructuredTypeInfoManager(
599
+ connection, self.name_utils, self.default_schema_name
600
+ )
707
601
  # Too many results, fall back to only query about single table
708
- return self._get_table_columns(connection, table_name, schema, **kw)
602
+ return column_info_manager.get_table_columns(table_name, schema)
709
603
  normalized_table_name = self.normalize_name(table_name)
710
604
  if normalized_table_name not in schema_columns:
711
605
  raise sa_exc.NoSuchTableError()
@@ -719,6 +613,37 @@ class SnowflakeDialect(default.DefaultDialect):
719
613
  prefixes_found.append(valid_prefix.name)
720
614
  return prefixes_found
721
615
 
616
+ @reflection.cache
617
+ def _query_all_columns_info(self, connection, schema_name, **kw):
618
+ try:
619
+ return connection.execute(
620
+ text(
621
+ """
622
+ SELECT /* sqlalchemy:_get_schema_columns */
623
+ ic.table_name,
624
+ ic.column_name,
625
+ ic.data_type,
626
+ ic.character_maximum_length,
627
+ ic.numeric_precision,
628
+ ic.numeric_scale,
629
+ ic.is_nullable,
630
+ ic.column_default,
631
+ ic.is_identity,
632
+ ic.comment,
633
+ ic.identity_start,
634
+ ic.identity_increment
635
+ FROM information_schema.columns ic
636
+ WHERE ic.table_schema=:table_schema
637
+ ORDER BY ic.ordinal_position"""
638
+ ),
639
+ {"table_schema": schema_name},
640
+ )
641
+ except sa_exc.ProgrammingError as pe:
642
+ if pe.orig.errno == 90030:
643
+ # This means that there are too many tables in the schema, we need to go more granular
644
+ return None # None triggers get_table_columns while staying cacheable
645
+ raise
646
+
722
647
  @reflection.cache
723
648
  def _get_schema_tables_info(self, connection, schema=None, **kw):
724
649
  """
@@ -982,18 +907,53 @@ class SnowflakeDialect(default.DefaultDialect):
982
907
  return self._value_or_default(data, table_name, schema)
983
908
 
984
909
  def connect(self, *cargs, **cparams):
985
- return (
986
- super().connect(
987
- *cargs,
988
- **(
989
- _update_connection_application_name(**cparams)
990
- if _ENABLE_SQLALCHEMY_AS_APPLICATION_NAME
991
- else cparams
992
- ),
910
+ if _ENABLE_SQLALCHEMY_AS_APPLICATION_NAME:
911
+ cparams = _update_connection_application_name(**cparams)
912
+
913
+ connection = super().connect(*cargs, **cparams)
914
+ self._log_new_connection_event(connection)
915
+
916
+ return connection
917
+
918
+ def _log_new_connection_event(self, connection):
919
+ try:
920
+ snowflake_connection = cast(SnowflakeConnection, cast(object, connection))
921
+ snowflake_rest_client = SnowflakeRestful(
922
+ host=snowflake_connection.host,
923
+ port=snowflake_connection.port,
924
+ protocol="https",
925
+ connection=snowflake_connection,
926
+ )
927
+ snowflake_telemetry_client = TelemetryClient(rest=snowflake_rest_client)
928
+
929
+ telemetry_value = {
930
+ "SQLAlchemy": SQLALCHEMY_VERSION,
931
+ }
932
+ try:
933
+ from pandas import __version__ as PANDAS_VERSION
934
+
935
+ telemetry_value["pandas"] = PANDAS_VERSION
936
+ except ImportError:
937
+ pass
938
+
939
+ snowflake_telemetry_client.add_log_to_batch(
940
+ TelemetryData.from_telemetry_data_dict(
941
+ from_dict={
942
+ TelemetryField.KEY_TYPE.value: TelemetryEvents.NEW_CONNECTION.value,
943
+ TelemetryField.KEY_VALUE.value: str(telemetry_value),
944
+ },
945
+ timestamp=int(time_in_seconds() * 1000),
946
+ connection=snowflake_connection,
947
+ )
948
+ )
949
+ snowflake_telemetry_client.send_batch()
950
+ except Exception as e:
951
+ logger.debug(
952
+ "Failed to send telemetry data for %s event: %s: %s",
953
+ TelemetryEvents.NEW_CONNECTION.value,
954
+ type(e).__name__,
955
+ str(e),
993
956
  )
994
- if _ENABLE_SQLALCHEMY_AS_APPLICATION_NAME
995
- else super().connect(*cargs, **cparams)
996
- )
997
957
 
998
958
 
999
959
  @sa_vnt.listens_for(Table, "before_create")