snowflake-sqlalchemy 1.7.5__tar.gz → 1.7.7__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 (129) hide show
  1. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/DESCRIPTION.md +6 -1
  2. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/PKG-INFO +1 -1
  3. snowflake_sqlalchemy-1.7.7/src/snowflake/sqlalchemy/name_utils.py +36 -0
  4. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/src/snowflake/sqlalchemy/parser/custom_type_parser.py +3 -3
  5. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/src/snowflake/sqlalchemy/snowdialect.py +65 -153
  6. snowflake_sqlalchemy-1.7.7/src/snowflake/sqlalchemy/structured_type_info_manager.py +142 -0
  7. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/src/snowflake/sqlalchemy/version.py +1 -1
  8. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/conftest.py +4 -1
  9. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/sqlalchemy_test_suite/test_suite_20.py +71 -11
  10. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/test_core.py +37 -20
  11. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/test_structured_datatypes.py +53 -0
  12. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/test_transactions.py +3 -2
  13. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/.gitignore +0 -0
  14. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/.gitmodules +0 -0
  15. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/.pre-commit-config.yaml +0 -0
  16. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/LICENSE.txt +0 -0
  17. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/MANIFEST.in +0 -0
  18. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/README.md +0 -0
  19. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/ci/build.sh +0 -0
  20. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/ci/build_docker.sh +0 -0
  21. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/ci/docker/sqlalchemy_build/Dockerfile +0 -0
  22. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/ci/docker/sqlalchemy_build/scripts/entrypoint.sh +0 -0
  23. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/ci/set_base_image.sh +0 -0
  24. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/ci/test.sh +0 -0
  25. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/ci/test_docker.sh +0 -0
  26. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/ci/test_linux.sh +0 -0
  27. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/license_header.txt +0 -0
  28. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/pyproject.toml +0 -0
  29. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/setup.cfg +0 -0
  30. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/snyk/requirements.txt +0 -0
  31. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/snyk/requiremtnts.txt +0 -0
  32. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/snyk/update_requirements.py +0 -0
  33. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/src/snowflake/sqlalchemy/__init__.py +0 -0
  34. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/src/snowflake/sqlalchemy/_constants.py +0 -0
  35. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/src/snowflake/sqlalchemy/base.py +0 -0
  36. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/src/snowflake/sqlalchemy/compat.py +0 -0
  37. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/src/snowflake/sqlalchemy/custom_commands.py +0 -0
  38. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/src/snowflake/sqlalchemy/custom_types.py +0 -0
  39. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/src/snowflake/sqlalchemy/exc.py +0 -0
  40. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/src/snowflake/sqlalchemy/functions.py +0 -0
  41. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/src/snowflake/sqlalchemy/provision.py +0 -0
  42. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/src/snowflake/sqlalchemy/requirements.py +0 -0
  43. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/src/snowflake/sqlalchemy/sql/__init__.py +0 -0
  44. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/src/snowflake/sqlalchemy/sql/custom_schema/__init__.py +0 -0
  45. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/src/snowflake/sqlalchemy/sql/custom_schema/clustered_table.py +0 -0
  46. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/src/snowflake/sqlalchemy/sql/custom_schema/custom_table_base.py +0 -0
  47. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/src/snowflake/sqlalchemy/sql/custom_schema/custom_table_prefix.py +0 -0
  48. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/src/snowflake/sqlalchemy/sql/custom_schema/dynamic_table.py +0 -0
  49. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/src/snowflake/sqlalchemy/sql/custom_schema/hybrid_table.py +0 -0
  50. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/src/snowflake/sqlalchemy/sql/custom_schema/iceberg_table.py +0 -0
  51. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/src/snowflake/sqlalchemy/sql/custom_schema/options/__init__.py +0 -0
  52. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/src/snowflake/sqlalchemy/sql/custom_schema/options/as_query_option.py +0 -0
  53. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/src/snowflake/sqlalchemy/sql/custom_schema/options/cluster_by_option.py +0 -0
  54. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/src/snowflake/sqlalchemy/sql/custom_schema/options/identifier_option.py +0 -0
  55. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/src/snowflake/sqlalchemy/sql/custom_schema/options/invalid_table_option.py +0 -0
  56. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/src/snowflake/sqlalchemy/sql/custom_schema/options/keyword_option.py +0 -0
  57. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/src/snowflake/sqlalchemy/sql/custom_schema/options/keywords.py +0 -0
  58. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/src/snowflake/sqlalchemy/sql/custom_schema/options/literal_option.py +0 -0
  59. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/src/snowflake/sqlalchemy/sql/custom_schema/options/table_option.py +0 -0
  60. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/src/snowflake/sqlalchemy/sql/custom_schema/options/target_lag_option.py +0 -0
  61. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/src/snowflake/sqlalchemy/sql/custom_schema/snowflake_table.py +0 -0
  62. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/src/snowflake/sqlalchemy/sql/custom_schema/table_from_query.py +0 -0
  63. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/src/snowflake/sqlalchemy/util.py +0 -0
  64. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tested_requirements/requirements_310.reqs +0 -0
  65. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tested_requirements/requirements_37.reqs +0 -0
  66. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tested_requirements/requirements_38.reqs +0 -0
  67. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tested_requirements/requirements_39.reqs +0 -0
  68. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/README.rst +0 -0
  69. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/__init__.py +0 -0
  70. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/__snapshots__/test_compile_dynamic_table.ambr +0 -0
  71. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/__snapshots__/test_core.ambr +0 -0
  72. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/__snapshots__/test_orm.ambr +0 -0
  73. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/__snapshots__/test_reflect_dynamic_table.ambr +0 -0
  74. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/__snapshots__/test_structured_datatypes.ambr +0 -0
  75. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/__snapshots__/test_unit_structured_types.ambr +0 -0
  76. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/custom_tables/__init__.py +0 -0
  77. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/custom_tables/__snapshots__/test_compile_dynamic_table.ambr +0 -0
  78. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/custom_tables/__snapshots__/test_compile_hybrid_table.ambr +0 -0
  79. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/custom_tables/__snapshots__/test_compile_iceberg_table.ambr +0 -0
  80. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/custom_tables/__snapshots__/test_compile_snowflake_table.ambr +0 -0
  81. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/custom_tables/__snapshots__/test_create_dynamic_table.ambr +0 -0
  82. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/custom_tables/__snapshots__/test_create_hybrid_table.ambr +0 -0
  83. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/custom_tables/__snapshots__/test_create_iceberg_table.ambr +0 -0
  84. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/custom_tables/__snapshots__/test_create_snowflake_table.ambr +0 -0
  85. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/custom_tables/__snapshots__/test_generic_options.ambr +0 -0
  86. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/custom_tables/__snapshots__/test_reflect_hybrid_table.ambr +0 -0
  87. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/custom_tables/__snapshots__/test_reflect_snowflake_table.ambr +0 -0
  88. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/custom_tables/test_compile_dynamic_table.py +0 -0
  89. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/custom_tables/test_compile_hybrid_table.py +0 -0
  90. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/custom_tables/test_compile_iceberg_table.py +0 -0
  91. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/custom_tables/test_compile_snowflake_table.py +0 -0
  92. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/custom_tables/test_create_dynamic_table.py +0 -0
  93. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/custom_tables/test_create_hybrid_table.py +0 -0
  94. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/custom_tables/test_create_iceberg_table.py +0 -0
  95. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/custom_tables/test_create_snowflake_table.py +0 -0
  96. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/custom_tables/test_generic_options.py +0 -0
  97. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/custom_tables/test_reflect_dynamic_table.py +0 -0
  98. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/custom_tables/test_reflect_hybrid_table.py +0 -0
  99. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/custom_tables/test_reflect_snowflake_table.py +0 -0
  100. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/data/users.txt +0 -0
  101. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/sqlalchemy_test_suite/README.md +0 -0
  102. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/sqlalchemy_test_suite/__init__.py +0 -0
  103. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/sqlalchemy_test_suite/conftest.py +0 -0
  104. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/sqlalchemy_test_suite/test_suite.py +0 -0
  105. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/test_compiler.py +0 -0
  106. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/test_copy.py +0 -0
  107. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/test_create.py +0 -0
  108. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/test_custom_functions.py +0 -0
  109. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/test_custom_types.py +0 -0
  110. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/test_geography.py +0 -0
  111. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/test_geometry.py +0 -0
  112. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/test_imports.py +0 -0
  113. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/test_index_reflection.py +0 -0
  114. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/test_multivalues_insert.py +0 -0
  115. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/test_orm.py +0 -0
  116. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/test_pandas.py +0 -0
  117. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/test_qmark.py +0 -0
  118. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/test_quote.py +0 -0
  119. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/test_quote_identifiers.py +0 -0
  120. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/test_semi_structured_datatypes.py +0 -0
  121. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/test_sequence.py +0 -0
  122. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/test_timestamp.py +0 -0
  123. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/test_unit_core.py +0 -0
  124. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/test_unit_cte.py +0 -0
  125. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/test_unit_structured_types.py +0 -0
  126. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/test_unit_types.py +0 -0
  127. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/test_unit_url.py +0 -0
  128. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tests/util.py +0 -0
  129. {snowflake_sqlalchemy-1.7.5 → snowflake_sqlalchemy-1.7.7}/tox.ini +0 -0
@@ -7,9 +7,14 @@ Snowflake Documentation is available at:
7
7
  Source code is also available at:
8
8
  <https://github.com/snowflakedb/snowflake-sqlalchemy>
9
9
  # Unreleased Notes
10
- - Compiling Merge and Copy Into
11
10
 
12
11
  # Release Notes
12
+ - v1.7.7(September 3, 2025)
13
+ - Fix exception for structured type columns dropped while collecting meetadata
14
+
15
+ - v1.7.6(July 10, 2025)
16
+ - Fix get_multi_indexes issue, wrong assign of returned indexes when processing multiple indexes in a table
17
+
13
18
  - v1.7.5(June 20, 2025)
14
19
  - Fix compilation of Merge and Copy Into was not working
15
20
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: snowflake-sqlalchemy
3
- Version: 1.7.5
3
+ Version: 1.7.7
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
@@ -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,7 +2,6 @@
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
@@ -16,7 +15,6 @@ from sqlalchemy import util as sa_util
16
15
  from sqlalchemy.engine import URL, default, reflection
17
16
  from sqlalchemy.schema import Table
18
17
  from sqlalchemy.sql import text
19
- from sqlalchemy.sql.elements import quoted_name
20
18
  from sqlalchemy.sql.sqltypes import NullType
21
19
  from sqlalchemy.types import FLOAT, Date, DateTime, Float, Time
22
20
 
@@ -24,6 +22,8 @@ from snowflake.connector import errors as sf_errors
24
22
  from snowflake.connector.connection import DEFAULT_CONFIGURATION
25
23
  from snowflake.connector.constants import UTF8
26
24
  from snowflake.sqlalchemy.compat import returns_unicode
25
+ from snowflake.sqlalchemy.name_utils import _NameUtils
26
+ from snowflake.sqlalchemy.structured_type_info_manager import _StructuredTypeInfoManager
27
27
 
28
28
  from ._constants import DIALECT_NAME
29
29
  from .base import (
@@ -42,7 +42,7 @@ from .custom_types import (
42
42
  )
43
43
  from .parser.custom_type_parser import * # noqa
44
44
  from .parser.custom_type_parser import _CUSTOM_DECIMAL # noqa
45
- from .parser.custom_type_parser import ischema_names, parse_index_columns, parse_type
45
+ from .parser.custom_type_parser import ischema_names, parse_index_columns
46
46
  from .sql.custom_schema.custom_table_prefix import CustomTablePrefix
47
47
  from .util import (
48
48
  _update_connection_application_name,
@@ -157,6 +157,7 @@ class SnowflakeDialect(default.DefaultDialect):
157
157
  super().__init__(isolation_level=isolation_level, **kwargs)
158
158
  self.force_div_is_floordiv = force_div_is_floordiv
159
159
  self.div_is_floordiv = force_div_is_floordiv
160
+ self.name_utils = _NameUtils(self.identifier_preparer)
160
161
 
161
162
  def initialize(self, connection):
162
163
  super().initialize(connection)
@@ -282,29 +283,10 @@ class SnowflakeDialect(default.DefaultDialect):
282
283
  raise
283
284
 
284
285
  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
286
+ return self.name_utils.normalize_name(name)
297
287
 
298
288
  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
289
+ return self.name_utils.denormalize_name(name)
308
290
 
309
291
  def _denormalize_quote_join(self, *idents):
310
292
  ip = self.identifier_preparer
@@ -491,53 +473,31 @@ class SnowflakeDialect(default.DefaultDialect):
491
473
  )
492
474
  return foreign_key_map.get(table_name, [])
493
475
 
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
476
  @reflection.cache
501
477
  def _get_schema_columns(self, connection, schema, **kw):
502
478
  """Get all columns in the schema, if we hit 'Information schema query returned too much data' problem return
503
479
  None, as it is cacheable and is an unexpected return type for this function"""
504
480
  ans = {}
505
- current_database, _ = self._current_database_schema(connection, **kw)
481
+
482
+ schema_name = self.denormalize_name(schema)
483
+
484
+ result = self._query_all_columns_info(connection, schema_name, **kw)
485
+ if result is None:
486
+ return None
487
+
488
+ current_database, default_schema = self._current_database_schema(
489
+ connection, **kw
490
+ )
506
491
  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
492
 
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
493
+ schema_primary_keys = self._get_schema_primary_keys(
494
+ connection, full_schema_name, **kw
495
+ )
496
+
497
+ structured_type_info_manager = _StructuredTypeInfoManager(
498
+ connection, self.name_utils, default_schema
499
+ )
500
+
541
501
  for (
542
502
  table_name,
543
503
  column_name,
@@ -572,25 +532,11 @@ class SnowflakeDialect(default.DefaultDialect):
572
532
  elif issubclass(col_type, (sqltypes.String, sqltypes.BINARY)):
573
533
  col_type_kw["length"] = character_maximum_length
574
534
  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
- )
535
+ column_info = structured_type_info_manager.get_column_info(
536
+ schema_name, table_name, column_name, **kw
537
+ )
538
+ if column_info:
539
+ ans[table_name].append(column_info)
594
540
  continue
595
541
  else:
596
542
  col_type = NullType
@@ -628,72 +574,6 @@ class SnowflakeDialect(default.DefaultDialect):
628
574
  }
629
575
  return ans
630
576
 
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
577
  def get_columns(self, connection, table_name, schema=None, **kw):
698
578
  """
699
579
  Gets all column info given the table info
@@ -704,8 +584,11 @@ class SnowflakeDialect(default.DefaultDialect):
704
584
 
705
585
  schema_columns = self._get_schema_columns(connection, schema, **kw)
706
586
  if schema_columns is None:
587
+ column_info_manager = _StructuredTypeInfoManager(
588
+ connection, self.name_utils, self.default_schema_name
589
+ )
707
590
  # Too many results, fall back to only query about single table
708
- return self._get_table_columns(connection, table_name, schema, **kw)
591
+ return column_info_manager.get_table_columns(table_name, schema)
709
592
  normalized_table_name = self.normalize_name(table_name)
710
593
  if normalized_table_name not in schema_columns:
711
594
  raise sa_exc.NoSuchTableError()
@@ -719,6 +602,37 @@ class SnowflakeDialect(default.DefaultDialect):
719
602
  prefixes_found.append(valid_prefix.name)
720
603
  return prefixes_found
721
604
 
605
+ @reflection.cache
606
+ def _query_all_columns_info(self, connection, schema_name, **kw):
607
+ try:
608
+ return connection.execute(
609
+ text(
610
+ """
611
+ SELECT /* sqlalchemy:_get_schema_columns */
612
+ ic.table_name,
613
+ ic.column_name,
614
+ ic.data_type,
615
+ ic.character_maximum_length,
616
+ ic.numeric_precision,
617
+ ic.numeric_scale,
618
+ ic.is_nullable,
619
+ ic.column_default,
620
+ ic.is_identity,
621
+ ic.comment,
622
+ ic.identity_start,
623
+ ic.identity_increment
624
+ FROM information_schema.columns ic
625
+ WHERE ic.table_schema=:table_schema
626
+ ORDER BY ic.ordinal_position"""
627
+ ),
628
+ {"table_schema": schema_name},
629
+ )
630
+ except sa_exc.ProgrammingError as pe:
631
+ if pe.orig.errno == 90030:
632
+ # This means that there are too many tables in the schema, we need to go more granular
633
+ return None # None triggers get_table_columns while staying cacheable
634
+ raise
635
+
722
636
  @reflection.cache
723
637
  def _get_schema_tables_info(self, connection, schema=None, **kw):
724
638
  """
@@ -955,9 +869,7 @@ class SnowflakeDialect(default.DefaultDialect):
955
869
  }
956
870
 
957
871
  if (schema, table_name) in indexes:
958
- indexes[(schema, table_name)] = indexes[(schema, table_name)].append(
959
- index
960
- )
872
+ indexes[(schema, table_name)].append(index)
961
873
  else:
962
874
  indexes[(schema, table_name)] = [index]
963
875
 
@@ -0,0 +1,142 @@
1
+ #
2
+ # Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved.
3
+
4
+ import re
5
+
6
+ from sqlalchemy import util as sa_util
7
+ from sqlalchemy.sql import text
8
+
9
+ from snowflake.sqlalchemy.name_utils import _NameUtils
10
+ from snowflake.sqlalchemy.parser.custom_type_parser import NullType, parse_type
11
+
12
+
13
+ class _StructuredTypeInfoManager:
14
+ """
15
+ Manager for handling structured type information in Snowflake tables.
16
+ This class is responsible for retrieving, caching, and providing
17
+ column information for structured types in Snowflake tables. It maintains
18
+ a cache of column descriptions to avoid repeated database queries.
19
+ Attributes:
20
+ connection: The database connection to use for queries
21
+ full_columns_descriptions (dict): Cache of column descriptions by schema and table
22
+ name_utils (_NameUtils): Utility for normalizing and denormalizing names
23
+ default_schema (str): The default schema to use when none is specified
24
+ """
25
+
26
+ def __init__(self, connection, name_utils: _NameUtils, default_schema: str):
27
+ self.connection = connection
28
+ self.full_columns_descriptions = {}
29
+ self.name_utils = name_utils
30
+ self.default_schema = default_schema
31
+
32
+ def get_column_info(
33
+ self, schema_name: str, table_name: str, column_name: str, **kwargs
34
+ ):
35
+ self._load_structured_type_info(schema_name, table_name)
36
+ if (
37
+ (schema_name, table_name) in self.full_columns_descriptions
38
+ and column_name in self.full_columns_descriptions[(schema_name, table_name)]
39
+ ):
40
+ return self.full_columns_descriptions[(schema_name, table_name)][
41
+ column_name
42
+ ]
43
+ return None
44
+
45
+ def _load_structured_type_info(self, schema_name: str, table_name: str):
46
+ """Get column information for a structured type"""
47
+ if (schema_name, table_name) not in self.full_columns_descriptions:
48
+
49
+ column_definitions = self.get_table_columns(table_name, schema_name)
50
+ if not column_definitions:
51
+ self.full_columns_descriptions[(schema_name, table_name)] = {}
52
+ return False
53
+
54
+ self.full_columns_descriptions[(schema_name, table_name)] = (
55
+ self._table_columns_as_dict(column_definitions)
56
+ )
57
+ return True
58
+
59
+ def _table_columns_as_dict(self, columns: list):
60
+ result = {}
61
+ for column in columns:
62
+ result[column["name"]] = column
63
+ return result
64
+
65
+ def get_table_columns(self, table_name: str, schema: str = None):
66
+ """Get all columns in a table in a schema"""
67
+ ans = []
68
+
69
+ schema = schema if schema else self.default_schema
70
+
71
+ table_schema = self.name_utils.denormalize_name(schema)
72
+ table_name = self.name_utils.denormalize_name(table_name)
73
+ result = self._execute_desc(table_schema, table_name)
74
+ if not result:
75
+ return []
76
+
77
+ for desc_data in result:
78
+ column_name = desc_data[0]
79
+ coltype = desc_data[1]
80
+ is_nullable = desc_data[3]
81
+ column_default = desc_data[4]
82
+ primary_key = desc_data[5]
83
+ comment = desc_data[9]
84
+
85
+ column_name = self.name_utils.normalize_name(column_name)
86
+ if column_name.startswith("sys_clustering_column"):
87
+ continue # ignoring clustering column
88
+ type_instance = parse_type(coltype)
89
+ if isinstance(type_instance, NullType):
90
+ sa_util.warn(
91
+ f"Did not recognize type '{coltype}' of column '{column_name}'"
92
+ )
93
+
94
+ identity = None
95
+ match = re.match(
96
+ r"IDENTITY START (?P<start>\d+) INCREMENT (?P<increment>\d+) (?P<order_type>ORDER|NOORDER)",
97
+ column_default if column_default else "",
98
+ )
99
+ if match:
100
+ identity = {
101
+ "start": int(match.group("start")),
102
+ "increment": int(match.group("increment")),
103
+ "order_type": match.group("order_type"),
104
+ }
105
+ is_identity = identity is not None
106
+
107
+ ans.append(
108
+ {
109
+ "name": column_name,
110
+ "type": type_instance,
111
+ "nullable": is_nullable == "Y",
112
+ "default": None if is_identity else column_default,
113
+ "autoincrement": is_identity,
114
+ "comment": comment if comment != "" else None,
115
+ "primary_key": primary_key == "Y",
116
+ }
117
+ )
118
+
119
+ if is_identity:
120
+ ans[-1]["identity"] = identity
121
+
122
+ # If we didn't find any columns for the table, the table doesn't exist.
123
+ if len(ans) == 0:
124
+ return []
125
+ return ans
126
+
127
+ def _execute_desc(self, table_schema: str, table_name: str):
128
+ """Execute a DESC command handling a possible exception.
129
+ Exception can be caused by another session dropping the table while
130
+ once this process has started"""
131
+ try:
132
+ return self.connection.execute(
133
+ text(
134
+ "DESC /* sqlalchemy:_get_schema_columns */"
135
+ f" TABLE {table_schema}.{table_name} TYPE = COLUMNS"
136
+ )
137
+ )
138
+ except Exception:
139
+ sa_util.warn(
140
+ f"Failed to reflect '{table_schema}' .'{table_name}' table using sqlalchemy:_get_schema_columns"
141
+ )
142
+ return None
@@ -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.5"
6
+ VERSION = "1.7.7"
@@ -232,8 +232,11 @@ def assert_text_in_buf():
232
232
  def go(expected, occurrences=1):
233
233
  assert buf.buffer
234
234
  buflines = [rec.getMessage() for rec in buf.buffer]
235
+ ocurrences_found = 0
236
+ for line in buflines:
237
+ if line.find(expected) != -1:
238
+ ocurrences_found += 1
235
239
 
236
- ocurrences_found = buflines.count(expected)
237
240
  assert occurrences == ocurrences_found, (
238
241
  f"Expected {occurrences} of {expected}, got {ocurrences_found} "
239
242
  f"occurrences in {buflines}."
@@ -3,11 +3,13 @@
3
3
  #
4
4
  import pytest
5
5
  from sqlalchemy import Integer, testing
6
+ from sqlalchemy import types as sql_types
6
7
  from sqlalchemy.schema import Column, Sequence, Table
7
8
  from sqlalchemy.testing import config
8
9
  from sqlalchemy.testing.assertions import eq_
10
+ from sqlalchemy.testing.suite import BizarroCharacterTest as _BizarroCharacterTest
9
11
  from sqlalchemy.testing.suite import (
10
- BizarroCharacterFKResolutionTest as _BizarroCharacterFKResolutionTest,
12
+ ComponentReflectionTestExtra as _ComponentReflectionTestExtra,
11
13
  )
12
14
  from sqlalchemy.testing.suite import (
13
15
  CompositeKeyReflectionTest as _CompositeKeyReflectionTest,
@@ -190,16 +192,74 @@ class CompositeKeyReflectionTest(_CompositeKeyReflectionTest):
190
192
  super().test_pk_column_order()
191
193
 
192
194
 
193
- class BizarroCharacterFKResolutionTest(_BizarroCharacterFKResolutionTest):
194
- @testing.combinations(
195
- ("id",), ("(3)",), ("col%p",), ("[brack]",), argnames="columnname"
196
- )
195
+ class BizarroCharacterTest(_BizarroCharacterTest):
196
+
197
+ def column_names_without_id():
198
+ return testing.combinations(
199
+ ("(3)",),
200
+ ("col%p",),
201
+ ("[brack]",),
202
+ argnames="columnname",
203
+ )
204
+
205
+ def column_names():
206
+ return testing.combinations(
207
+ ("id",),
208
+ ("(3)",),
209
+ ("col%p",),
210
+ ("[brack]",),
211
+ argnames="columnname",
212
+ )
213
+
214
+ def table_names():
215
+ return testing.combinations(
216
+ ("plain",),
217
+ ("(2)",),
218
+ ("[brackets]",),
219
+ argnames="tablename",
220
+ )
221
+
197
222
  @testing.variation("use_composite", [True, False])
198
- @testing.combinations(
199
- ("plain",),
200
- ("(2)",),
201
- ("[brackets]",),
202
- argnames="tablename",
203
- )
223
+ @column_names()
224
+ @table_names()
225
+ @testing.requires.foreign_key_constraint_reflection
204
226
  def test_fk_ref(self, connection, metadata, use_composite, tablename, columnname):
205
227
  super().test_fk_ref(connection, metadata, use_composite, tablename, columnname)
228
+
229
+ @column_names()
230
+ @table_names()
231
+ @testing.requires.identity_columns
232
+ def test_reflect_identity(self, tablename, columnname, connection, metadata):
233
+ super().test_reflect_identity(tablename, columnname, connection, metadata)
234
+
235
+ @column_names_without_id()
236
+ @table_names()
237
+ @testing.requires.comment_reflection
238
+ def test_reflect_comments(self, tablename, columnname, connection, metadata):
239
+ super().test_reflect_comments(tablename, columnname, connection, metadata)
240
+
241
+
242
+ class ComponentReflectionTestExtra(_ComponentReflectionTestExtra):
243
+
244
+ @testing.requires.table_reflection
245
+ @testing.combinations(
246
+ sql_types.String,
247
+ sql_types.VARCHAR,
248
+ sql_types.CHAR,
249
+ (sql_types.NVARCHAR, testing.requires.nvarchar_types),
250
+ (sql_types.NCHAR, testing.requires.nvarchar_types),
251
+ argnames="type_",
252
+ )
253
+ def test_string_length_reflection(self, connection, metadata, type_):
254
+ typ = self._type_round_trip(connection, metadata, type_(52))[0]
255
+ if issubclass(type_, sql_types.VARCHAR):
256
+ assert isinstance(typ, sql_types.VARCHAR)
257
+ elif issubclass(
258
+ type_, sql_types.CHAR
259
+ ): # char is not supported then mapped to VARCHAR
260
+ assert isinstance(typ, sql_types.VARCHAR)
261
+ else:
262
+ assert isinstance(typ, sql_types.String)
263
+
264
+ eq_(typ.length, 52)
265
+ assert isinstance(typ.length, int)