snowflake-sqlalchemy 1.10.0__tar.gz → 1.10.2__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 (145) hide show
  1. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/.pre-commit-config.yaml +9 -5
  2. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/DESCRIPTION.md +7 -0
  3. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/PKG-INFO +1 -1
  4. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/ci/build.sh +1 -1
  5. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/ci/test_docker.sh +1 -1
  6. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/ci/test_linux.sh +1 -1
  7. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/base.py +2 -2
  8. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/custom_types.py +1 -1
  9. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/exc.py +12 -6
  10. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/name_utils.py +42 -0
  11. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/snowdialect.py +8 -39
  12. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/sql/custom_schema/options/as_query_option.py +1 -1
  13. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/sql/custom_schema/options/target_lag_option.py +1 -1
  14. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/structured_type_info_manager.py +5 -11
  15. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/version.py +1 -1
  16. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_compiler.py +28 -0
  17. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_core.py +1 -1
  18. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_structured_datatypes.py +5 -12
  19. snowflake_sqlalchemy-1.10.2/tests/test_unit_name_utils.py +144 -0
  20. snowflake_sqlalchemy-1.10.2/tests/test_unit_structured_type_info_manager.py +247 -0
  21. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tox.ini +4 -4
  22. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/.gitignore +0 -0
  23. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/.gitmodules +0 -0
  24. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/LICENSE.txt +0 -0
  25. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/MANIFEST.in +0 -0
  26. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/README.md +0 -0
  27. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/ci/build_docker.sh +0 -0
  28. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/ci/docker/sqlalchemy_build/Dockerfile +0 -0
  29. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/ci/docker/sqlalchemy_build/scripts/entrypoint.sh +0 -0
  30. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/ci/set_base_image.sh +0 -0
  31. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/ci/test.sh +0 -0
  32. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/license_header.txt +0 -0
  33. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/pyproject.toml +0 -0
  34. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/setup.cfg +0 -0
  35. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/snyk/requirements.txt +0 -0
  36. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/snyk/requiremtnts.txt +0 -0
  37. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/snyk/update_requirements.py +0 -0
  38. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/__init__.py +0 -0
  39. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/_constants.py +0 -0
  40. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/alembic_util.py +0 -0
  41. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/compat.py +0 -0
  42. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/custom_commands.py +0 -0
  43. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/functions.py +0 -0
  44. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/orm.py +0 -0
  45. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/parser/custom_type_parser.py +0 -0
  46. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/provision.py +0 -0
  47. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/requirements.py +0 -0
  48. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/sql/__init__.py +0 -0
  49. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/sql/custom_schema/__init__.py +0 -0
  50. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/sql/custom_schema/clustered_table.py +0 -0
  51. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/sql/custom_schema/custom_table_base.py +0 -0
  52. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/sql/custom_schema/custom_table_prefix.py +0 -0
  53. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/sql/custom_schema/dynamic_table.py +0 -0
  54. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/sql/custom_schema/hybrid_table.py +0 -0
  55. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/sql/custom_schema/iceberg_table.py +0 -0
  56. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/sql/custom_schema/options/__init__.py +0 -0
  57. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/sql/custom_schema/options/cluster_by_option.py +0 -0
  58. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/sql/custom_schema/options/identifier_option.py +0 -0
  59. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/sql/custom_schema/options/invalid_table_option.py +0 -0
  60. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/sql/custom_schema/options/keyword_option.py +0 -0
  61. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/sql/custom_schema/options/keywords.py +0 -0
  62. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/sql/custom_schema/options/literal_option.py +0 -0
  63. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/sql/custom_schema/options/table_option.py +0 -0
  64. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/sql/custom_schema/snowflake_table.py +0 -0
  65. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/sql/custom_schema/table_from_query.py +0 -0
  66. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/util.py +0 -0
  67. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tested_requirements/requirements_310.reqs +0 -0
  68. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tested_requirements/requirements_37.reqs +0 -0
  69. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tested_requirements/requirements_38.reqs +0 -0
  70. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tested_requirements/requirements_39.reqs +0 -0
  71. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/README.rst +0 -0
  72. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/__init__.py +0 -0
  73. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/__snapshots__/test_compile_dynamic_table.ambr +0 -0
  74. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/__snapshots__/test_core.ambr +0 -0
  75. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/__snapshots__/test_orm.ambr +0 -0
  76. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/__snapshots__/test_reflect_dynamic_table.ambr +0 -0
  77. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/__snapshots__/test_structured_datatypes.ambr +0 -0
  78. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/__snapshots__/test_unit_structured_types.ambr +0 -0
  79. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/alembic_integration/README.md +0 -0
  80. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/alembic_integration/conftest.py +0 -0
  81. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/alembic_integration/test_multi_schema_fk.py +0 -0
  82. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/conftest.py +0 -0
  83. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/__init__.py +0 -0
  84. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/__snapshots__/test_compile_dynamic_table.ambr +0 -0
  85. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/__snapshots__/test_compile_hybrid_table.ambr +0 -0
  86. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/__snapshots__/test_compile_iceberg_table.ambr +0 -0
  87. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/__snapshots__/test_compile_snowflake_table.ambr +0 -0
  88. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/__snapshots__/test_create_dynamic_table.ambr +0 -0
  89. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/__snapshots__/test_create_hybrid_table.ambr +0 -0
  90. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/__snapshots__/test_create_iceberg_table.ambr +0 -0
  91. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/__snapshots__/test_create_snowflake_table.ambr +0 -0
  92. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/__snapshots__/test_generic_options.ambr +0 -0
  93. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/__snapshots__/test_reflect_hybrid_table.ambr +0 -0
  94. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/__snapshots__/test_reflect_snowflake_table.ambr +0 -0
  95. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/test_compile_dynamic_table.py +0 -0
  96. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/test_compile_hybrid_table.py +0 -0
  97. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/test_compile_iceberg_table.py +0 -0
  98. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/test_compile_snowflake_table.py +0 -0
  99. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/test_create_dynamic_table.py +0 -0
  100. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/test_create_hybrid_table.py +0 -0
  101. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/test_create_iceberg_table.py +0 -0
  102. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/test_create_snowflake_table.py +0 -0
  103. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/test_generic_options.py +0 -0
  104. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/test_reflect_dynamic_table.py +0 -0
  105. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/test_reflect_hybrid_table.py +0 -0
  106. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/test_reflect_snowflake_table.py +0 -0
  107. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/data/users.txt +0 -0
  108. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/sqlalchemy_test_suite/README.md +0 -0
  109. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/sqlalchemy_test_suite/__init__.py +0 -0
  110. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/sqlalchemy_test_suite/conftest.py +0 -0
  111. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/sqlalchemy_test_suite/test_suite.py +0 -0
  112. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/sqlalchemy_test_suite/test_suite_20.py +0 -0
  113. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_compat.py +0 -0
  114. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_copy.py +0 -0
  115. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_create.py +0 -0
  116. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_cross_database_reflection.py +0 -0
  117. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_custom_functions.py +0 -0
  118. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_custom_types.py +0 -0
  119. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_decfloat.py +0 -0
  120. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_dialect_connect.py +0 -0
  121. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_geography.py +0 -0
  122. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_geometry.py +0 -0
  123. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_imports.py +0 -0
  124. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_index_reflection.py +0 -0
  125. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_keys.py +0 -0
  126. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_loader_criteria_regression.py +0 -0
  127. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_multivalues_insert.py +0 -0
  128. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_orm.py +0 -0
  129. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_pandas.py +0 -0
  130. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_qmark.py +0 -0
  131. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_quote.py +0 -0
  132. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_quote_identifiers.py +0 -0
  133. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_semi_structured_datatypes.py +0 -0
  134. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_sequence.py +0 -0
  135. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_timestamp.py +0 -0
  136. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_transactions.py +0 -0
  137. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_unit_case_sensitivity.py +0 -0
  138. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_unit_core.py +0 -0
  139. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_unit_cte.py +0 -0
  140. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_unit_orm.py +0 -0
  141. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_unit_structured_types.py +0 -0
  142. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_unit_types.py +0 -0
  143. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_unit_url.py +0 -0
  144. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_vector.py +0 -0
  145. {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/util.py +0 -0
@@ -1,7 +1,9 @@
1
1
  exclude: '^(.*egg.info.*|.*/parameters.py).*$'
2
+ default_language_version:
3
+ python: python3
2
4
  repos:
3
5
  - repo: https://github.com/pre-commit/pre-commit-hooks
4
- rev: v4.5.0
6
+ rev: v6.0.0
5
7
  hooks:
6
8
  - id: trailing-whitespace
7
9
  exclude: '\.ambr$'
@@ -14,19 +16,21 @@ repos:
14
16
  hooks:
15
17
  - id: isort
16
18
  - repo: https://github.com/asottile/pyupgrade
17
- rev: v3.15.1
19
+ rev: v3.18.0
18
20
  hooks:
19
21
  - id: pyupgrade
20
22
  args: [--py37-plus]
21
23
  - repo: https://github.com/psf/black
22
- rev: 24.2.0
24
+ rev: 24.10.0
23
25
  hooks:
24
26
  - id: black
25
27
  args:
26
28
  - --safe
29
+ - --target-version
30
+ - py38
27
31
  language_version: python3
28
32
  - repo: https://github.com/Lucas-C/pre-commit-hooks.git
29
- rev: v1.5.5
33
+ rev: v1.5.6
30
34
  hooks:
31
35
  - id: insert-license
32
36
  name: insert-py-license
@@ -40,7 +44,7 @@ repos:
40
44
  - --license-filepath
41
45
  - license_header.txt
42
46
  - repo: https://github.com/pycqa/flake8
43
- rev: 7.0.0
47
+ rev: 7.3.0
44
48
  hooks:
45
49
  - id: flake8
46
50
  additional_dependencies:
@@ -11,6 +11,13 @@ Source code is also available at:
11
11
 
12
12
  # Release Notes
13
13
 
14
+ - v1.10.2 (June 18, 2026)
15
+ - Fix double-escaped identifier quoting for database-qualified schemas in `_StructuredTypeInfoManager.get_table_columns`. Since v1.10.1 the schema was quoted as a single identifier, so a qualified schema such as `"MYDB"."MYSCHEMA"` became `"""MYDB"".""MYSCHEMA"""` and the `DESC TABLE` fallback failed (emitting a `Failed to reflect table ... sqlalchemy:_get_schema_columns` warning) for every structured-typed table when reflecting a non-default `database.schema`. The schema is now split on the dot and each component is double-quoted individually, preserving the SNOW-3480955 injection guard while correctly handling qualified schemas.
16
+
17
+ - v1.10.1 (June 15, 2026)
18
+ - Fix `regexp_match` and `regexp_replace` flags rendered as bound parameters instead of literal strings ([#SNOW-3573046](https://github.com/snowflakedb/snowflake-sqlalchemy)). Flags passed to `ColumnElement.regexp_match(..., flags=...)` and `ColumnElement.regexp_replace(..., flags=...)` were processed through the standard parameter pipeline, producing incorrect SQL. Flags are now rendered as inline string literals, matching Snowflake's expected `REGEXP_LIKE(col, pattern, 'i')` / `REGEXP_REPLACE(col, pattern, replacement, 'i')` syntax.
19
+ - Fix inconsistent identifier quoting in `_StructuredTypeInfoManager.get_table_columns`. The `DESC TABLE` fallback path used raw denormalised names in an f-string while all other reflection paths apply `ip.quote(denormalize_name(...))` via `_always_quote_join`. Schema and table components are now consistently double-quoted before the statement is constructed, and the method delegates to `get_table_columns_by_full_name` to collapse the two previously divergent code paths.
20
+
14
21
  - v1.10.0 (May 20, 2026)
15
22
  - Fix `with_loader_criteria` silently dropping filters on non-Snowflake dialects ([#676](https://github.com/snowflakedb/snowflake-sqlalchemy/issues/676)). Importing `snowflake-sqlalchemy` previously altered SQLAlchemy's ORM compilation for every dialect in the process, causing loader-criteria filters to be omitted inside sealed subqueries when using PostgreSQL, MySQL, SQLite, etc. Snowflake dialect behavior is unchanged; the BCR-1057 lateral-join workaround is now scoped to Snowflake connections only.
16
23
  - Map Snowflake `UUID` column type to `sqlalchemy.sql.sqltypes.UUID` for reflection on SQLAlchemy 2.x ([#681](https://github.com/snowflakedb/snowflake-sqlalchemy/issues/681)). Previously reflected as `NullType`. Values are returned as plain strings (`as_uuid=False`) rather than `uuid.UUID` instances. No change on SQLAlchemy 1.4 where the generic `UUID` type does not exist.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: snowflake-sqlalchemy
3
- Version: 1.10.0
3
+ Version: 1.10.2
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
@@ -3,7 +3,7 @@
3
3
  # Build snowflake-sqlalchemy
4
4
  set -o pipefail
5
5
 
6
- PYTHON="python3.8"
6
+ PYTHON="python3.9"
7
7
  THIS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
8
  SQLALCHEMY_DIR="$(dirname "${THIS_DIR}")"
9
9
  DIST_DIR="${SQLALCHEMY_DIR}/dist"
@@ -7,7 +7,7 @@
7
7
  set -o pipefail
8
8
 
9
9
  # In case this is ran from dev-vm
10
- PYTHON_ENV=${1:-3.8}
10
+ PYTHON_ENV=${1:-3.9}
11
11
 
12
12
  # Set constants
13
13
  THIS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -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 3.12 3.13 3.14}"
9
+ PYTHON_VERSIONS="${1:-3.9 3.10 3.11 3.12 3.13 3.14}"
10
10
  # Python versions where pyarrow (required by pandas extra) is not available
11
11
  PANDAS_SKIP_VERSIONS="3.14"
12
12
  THIS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -152,7 +152,7 @@ class SnowflakeSelectState(SelectState):
152
152
  raw_columns, left, right, onclause
153
153
  )
154
154
  else:
155
- (replace_from_obj_index) = self._join_place_explicit_left_side(left)
155
+ replace_from_obj_index = self._join_place_explicit_left_side(left)
156
156
 
157
157
  if replace_from_obj_index is not None:
158
158
  # splice into an existing element in the
@@ -793,7 +793,7 @@ class SnowflakeCompiler(compiler.SQLCompiler):
793
793
  pattern = self.process(binary.right, **kw)
794
794
  flags = binary.modifiers["flags"]
795
795
  if flags is not None:
796
- flags = self.process(flags, **kw)
796
+ flags = self.render_literal_value(flags, sqltypes.STRINGTYPE)
797
797
  return string, pattern, flags
798
798
 
799
799
  def visit_regexp_match_op_binary(self, binary, operator, **kw):
@@ -86,7 +86,7 @@ class VECTOR(SnowflakeType):
86
86
 
87
87
  @staticmethod
88
88
  def _map_sqlalchemy_type(
89
- element_type: Union[sqltypes.Integer, sqltypes.Float]
89
+ element_type: Union[sqltypes.Integer, sqltypes.Float],
90
90
  ) -> str:
91
91
  if isinstance(element_type, sqltypes.Integer):
92
92
  return "INT"
@@ -26,14 +26,14 @@ class UnsupportedPrimaryKeysAndForeignKeysError(ArgumentError):
26
26
 
27
27
 
28
28
  class RequiredParametersNotProvidedError(ArgumentError):
29
- def __init__(self, target: str, parameters: List[str]):
29
+ def __init__(self, target: str, parameters: List[str]): # noqa: B042
30
30
  super().__init__(
31
31
  f"{target} requires the following parameters: %s." % ", ".join(parameters)
32
32
  )
33
33
 
34
34
 
35
35
  class UnexpectedTableOptionKeyError(ArgumentError):
36
- def __init__(self, expected: str, actual: str):
36
+ def __init__(self, expected: str, actual: str): # noqa: B042
37
37
  super().__init__(f"Expected table option {expected} but got {actual}.")
38
38
 
39
39
 
@@ -45,7 +45,9 @@ class OptionKeyNotProvidedError(ArgumentError):
45
45
 
46
46
 
47
47
  class UnexpectedOptionParameterTypeError(ArgumentError):
48
- def __init__(self, parameter_name: str, target: str, types: List[str]):
48
+ def __init__( # noqa: B042
49
+ self, parameter_name: str, target: str, types: List[str]
50
+ ):
49
51
  super().__init__(
50
52
  f"Parameter {parameter_name} of {target} requires to be one"
51
53
  f" of following types: {', '.join(types)}."
@@ -67,7 +69,9 @@ class UnexpectedOptionTypeError(ArgumentError):
67
69
 
68
70
 
69
71
  class InvalidTableParameterTypeError(ArgumentError):
70
- def __init__(self, name: str, input_type: str, expected_types: List[str]):
72
+ def __init__( # noqa: B042
73
+ self, name: str, input_type: str, expected_types: List[str]
74
+ ):
71
75
  expected_types_str = "', '".join(expected_types)
72
76
  super().__init__(
73
77
  f"Invalid parameter type '{input_type}' provided for '{name}'. "
@@ -76,7 +80,7 @@ class InvalidTableParameterTypeError(ArgumentError):
76
80
 
77
81
 
78
82
  class MultipleErrors(ArgumentError):
79
- def __init__(self, errors):
83
+ def __init__(self, errors): # noqa: B042
80
84
  self.errors = errors
81
85
 
82
86
  def __str__(self):
@@ -84,7 +88,9 @@ class MultipleErrors(ArgumentError):
84
88
 
85
89
 
86
90
  class StructuredTypeNotSupportedInTableColumnsError(ArgumentError):
87
- def __init__(self, table_type: str, table_name: str, column_name: str):
91
+ def __init__( # noqa: B042
92
+ self, table_type: str, table_name: str, column_name: str
93
+ ):
88
94
  super().__init__(
89
95
  f"Column '{column_name}' is of a structured type, which is only supported on Iceberg tables. "
90
96
  f"The table '{table_name}' is of type '{table_type}', not Iceberg."
@@ -1,6 +1,9 @@
1
1
  #
2
2
  # Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved.
3
3
 
4
+ import operator
5
+ from functools import reduce
6
+
4
7
  from sqlalchemy.sql.compiler import IdentifierPreparer
5
8
  from sqlalchemy.sql.elements import quoted_name
6
9
 
@@ -61,3 +64,42 @@ class _NameUtils:
61
64
  ):
62
65
  name = name.upper()
63
66
  return name
67
+
68
+ def _quote_component(self, component) -> str:
69
+ """Unconditionally double-quote a single pre-split identifier component.
70
+
71
+ Components marked ``quote=True`` are taken verbatim (case preserved);
72
+ others are denormalized first so a plain lowercase name maps to the
73
+ Snowflake-stored uppercase form.
74
+
75
+ Use this only for parts that were already extracted by
76
+ ``_split_schema_by_dot`` — do NOT call it on dotted strings because
77
+ it will not split them first.
78
+ """
79
+ ip = self.identifier_preparer
80
+ name = str(component)
81
+ if getattr(component, "quote", None):
82
+ return ip.quote_identifier(name)
83
+ return ip.quote_identifier(self.denormalize_name(name))
84
+
85
+ def always_quote_join(self, *idents) -> str:
86
+ """Build a dot-joined SQL identifier string that always quotes every part.
87
+
88
+ Each identifier in *idents is split on unquoted dots via
89
+ ``_split_schema_by_dot`` (so ``"db.schema"`` becomes two components),
90
+ then every component is unconditionally double-quoted via
91
+ ``_quote_component``.
92
+
93
+ Do NOT pass pre-split parts that may contain literal dots (e.g. a
94
+ component extracted from ``'"my.schema"'``). Use ``_quote_component``
95
+ directly on each pre-split part instead.
96
+ """
97
+ split_idents = reduce(
98
+ operator.add,
99
+ [
100
+ self.identifier_preparer._split_schema_by_dot(ids)
101
+ for ids in idents
102
+ if ids is not None
103
+ ],
104
+ )
105
+ return ".".join(self._quote_component(i) for i in split_idents)
@@ -354,28 +354,11 @@ class SnowflakeDialect(default.DefaultDialect):
354
354
  def _always_quote_join(self, *idents):
355
355
  """Build a dot-joined identifier string that always quotes every part.
356
356
 
357
- Unlike _denormalize_quote_join (which quotes only when _requires_quotes
358
- demands it), this helper denormalizes each segment first and then
359
- unconditionally wraps it in double-quotes. This is safe because quoting
360
- a denormalized Snowflake identifier is semantically equivalent to the
361
- unquoted form for case-insensitive names, while also being correct for
362
- case-sensitive ones.
363
-
364
- IMPORTANT: denormalization must happen before quoting. Quoting the
365
- SA-normalized (lowercase) form would produce "my_table" which Snowflake
366
- resolves as a case-sensitive reference to a table literally stored as
367
- my_table — different from the stored MY_TABLE.
368
-
369
- Only use this for new, single-table SQL helpers. Existing callers of
370
- _denormalize_quote_join must not be changed to avoid altering SQL output
371
- for existing users (backward-compatibility constraint).
357
+ Delegates to ``_NameUtils.always_quote_join`` see that method for
358
+ the full contract. The dialect accessor exists for backward
359
+ compatibility with callers inside this class.
372
360
  """
373
- ip = self.identifier_preparer
374
- split_idents = reduce(
375
- operator.add,
376
- [ip._split_schema_by_dot(ids) for ids in idents if ids is not None],
377
- )
378
- return ".".join(ip.quote(self.denormalize_name(i)) for i in split_idents)
361
+ return self.name_utils.always_quote_join(*idents)
379
362
 
380
363
  def _get_full_schema_name(self, connection, schema=None, **kw):
381
364
  """
@@ -407,24 +390,10 @@ class SnowflakeDialect(default.DefaultDialect):
407
390
  f"'database.schema', got {len(parts)} parts"
408
391
  )
409
392
 
410
- # Quote each part unconditionally and preserve explicit quoted-name
411
- # boundaries from _split_schema_by_dot. Do NOT pass through
412
- # _denormalize_quote_join, which would re-split parts containing
413
- # literal dots (e.g. "schema.with.dots").
414
- quoted_parts = []
415
- for part in parts:
416
- part_name = str(part)
417
- if getattr(part, "quote", None):
418
- quoted_parts.append(
419
- self.identifier_preparer.quote_identifier(part_name)
420
- )
421
- else:
422
- quoted_parts.append(
423
- self.identifier_preparer.quote_identifier(
424
- self.denormalize_name(part_name)
425
- )
426
- )
427
- return ".".join(quoted_parts)
393
+ # Quote each pre-split part unconditionally, preserving explicit
394
+ # quoted-name boundaries. Do NOT re-split via always_quote_join
395
+ # because parts may contain literal dots (e.g. "schema.with.dots").
396
+ return ".".join(self.name_utils._quote_component(p) for p in parts)
428
397
 
429
398
  @reflection.cache
430
399
  def _current_database_schema(self, connection, **kw):
@@ -29,7 +29,7 @@ class AsQueryOption(TableOption):
29
29
 
30
30
  @staticmethod
31
31
  def create(
32
- value: Optional[Union["AsQueryOption", str, Selectable]]
32
+ value: Optional[Union["AsQueryOption", str, Selectable]],
33
33
  ) -> "TableOption":
34
34
  if isinstance(value, (NoneType, AsQueryOption)):
35
35
  return value
@@ -52,7 +52,7 @@ class TargetLagOption(TableOption):
52
52
 
53
53
  @staticmethod
54
54
  def create(
55
- value: Union["TargetLagOption", Tuple[int, TimeUnit], KeywordOptionType]
55
+ value: Union["TargetLagOption", Tuple[int, TimeUnit], KeywordOptionType],
56
56
  ) -> Optional[TableOption]:
57
57
  if isinstance(value, NoneType):
58
58
  return value
@@ -84,18 +84,12 @@ class _StructuredTypeInfoManager:
84
84
  schema = schema if schema else self.default_schema
85
85
 
86
86
  if "." in str(table_name):
87
- parts = self.name_utils.identifier_preparer._split_schema_by_dot(
88
- str(table_name)
89
- )
90
- table_name = str(parts[-1])
87
+ ip = self.name_utils.identifier_preparer
88
+ table_name = ip._split_schema_by_dot(str(table_name))[-1]
91
89
 
92
- table_schema = self.name_utils.denormalize_name(schema)
93
- table_name = self.name_utils.denormalize_name(table_name)
94
- result = self._execute_desc(f"{table_schema}.{table_name}")
95
- if not result:
96
- return []
97
-
98
- return self._parse_desc_result(result)
90
+ return self.get_table_columns_by_full_name(
91
+ self.name_utils.always_quote_join(schema, table_name)
92
+ )
99
93
 
100
94
  def _parse_desc_result(self, result):
101
95
  """Parse DESC TABLE result into column information"""
@@ -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.10.0"
6
+ VERSION = "1.10.2"
@@ -125,6 +125,34 @@ class TestSnowflakeCompiler(AssertsCompiledSQL):
125
125
  "SELECT table1.name FROM table1 WHERE table1.name NOT ILIKE %(name_1)s ESCAPE '\\\\'",
126
126
  )
127
127
 
128
+ def test_regexp_match_with_flags_compilation(self):
129
+ statement = select(table1.c.name).where(
130
+ table1.c.name.regexp_match("ann", flags="i")
131
+ )
132
+ self.assert_compile(
133
+ statement,
134
+ "SELECT table1.name FROM table1 WHERE REGEXP_LIKE(table1.name, %(name_1)s, 'i')",
135
+ dialect="snowflake",
136
+ )
137
+
138
+ def test_not_regexp_match_with_flags_compilation(self):
139
+ statement = select(table1.c.name).where(
140
+ ~table1.c.name.regexp_match("ann", flags="i")
141
+ )
142
+ self.assert_compile(
143
+ statement,
144
+ "SELECT table1.name FROM table1 WHERE NOT REGEXP_LIKE(table1.name, %(name_1)s, 'i')",
145
+ dialect="snowflake",
146
+ )
147
+
148
+ def test_regexp_replace_with_flags_compilation(self):
149
+ statement = select(table1.c.name.regexp_replace("ann", "bob", flags="i"))
150
+ self.assert_compile(
151
+ statement,
152
+ "SELECT REGEXP_REPLACE(table1.name, %(name_1)s, %(name_2)s, 'i') AS anon_1 FROM table1",
153
+ dialect="snowflake",
154
+ )
155
+
128
156
  def test_drop_table_comment(self):
129
157
  self.assert_compile(DropTableComment(table1), "COMMENT ON TABLE table1 IS ''")
130
158
  self.assert_compile(
@@ -2450,7 +2450,7 @@ def test_true_division_operation(engine_testaccount, operation):
2450
2450
  [literal(3), literal(2), 1.5, 1.5],
2451
2451
  [literal(4), literal(1.5), 2.6666666666666665, 2.0],
2452
2452
  [literal(5.5), literal(10.7), 0.5140186915887851, 0],
2453
- [literal(5.5), literal(8), 0.6875, 0.6875],
2453
+ [literal(5.5), literal(8), 0.6875, 0.0],
2454
2454
  ],
2455
2455
  )
2456
2456
  @pytest.mark.feature_v20
@@ -25,6 +25,7 @@ from snowflake.sqlalchemy import NUMBER, IcebergTable, SnowflakeTable
25
25
  from snowflake.sqlalchemy.custom_types import ARRAY, MAP, OBJECT, TEXT
26
26
  from snowflake.sqlalchemy.exc import StructuredTypeNotSupportedInTableColumnsError
27
27
  from snowflake.sqlalchemy.name_utils import _NameUtils
28
+ from snowflake.sqlalchemy.snowdialect import SnowflakeDialect
28
29
  from snowflake.sqlalchemy.structured_type_info_manager import _StructuredTypeInfoManager
29
30
 
30
31
 
@@ -603,14 +604,10 @@ def test_structured_type_not_supported_in_table_columns_error(
603
604
 
604
605
 
605
606
  @patch.object(_StructuredTypeInfoManager, "_execute_desc")
606
- @patch.object(_NameUtils, "denormalize_name")
607
- def test_structured_type_on_dropped_table(
608
- mocked_denormalize_name_method, mocked_execute_desc_method
609
- ):
607
+ def test_structured_type_on_dropped_table(mocked_execute_desc_method):
610
608
  mocked_execute_desc_method.return_value = None
611
- mocked_denormalize_name_method.side_effect = lambda v: v
612
609
  structured_type_info = _StructuredTypeInfoManager(
613
- None, _NameUtils(None), "mySchema"
610
+ None, _NameUtils(SnowflakeDialect().identifier_preparer), "mySchema"
614
611
  )
615
612
  result = structured_type_info.get_column_info(
616
613
  "mySchema", "dropped_table", "structured_type_col"
@@ -619,10 +616,7 @@ def test_structured_type_on_dropped_table(
619
616
 
620
617
 
621
618
  @patch.object(_StructuredTypeInfoManager, "_execute_desc")
622
- @patch.object(_NameUtils, "denormalize_name")
623
- def test_structured_type_on_table_with_map(
624
- mocked_denormalize_name_method, mocked_execute_desc_method
625
- ):
619
+ def test_structured_type_on_table_with_map(mocked_execute_desc_method):
626
620
  mocked_execute_desc_method.return_value = [
627
621
  [
628
622
  "myCol",
@@ -637,9 +631,8 @@ def test_structured_type_on_table_with_map(
637
631
  "MapColumn",
638
632
  ]
639
633
  ]
640
- mocked_denormalize_name_method.side_effect = lambda v: v
641
634
  structured_type_info = _StructuredTypeInfoManager(
642
- None, _NameUtils(None), "mySchema"
635
+ None, _NameUtils(SnowflakeDialect().identifier_preparer), "mySchema"
643
636
  )
644
637
  result = structured_type_info.get_column_info("mySchema", "dropped_table", "myCol")
645
638
  assert result is not None
@@ -0,0 +1,144 @@
1
+ #
2
+ # Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved.
3
+ #
4
+ """
5
+ Unit tests for _NameUtils._quote_component and _NameUtils.always_quote_join.
6
+
7
+ These methods are the canonical always-quote helpers shared by
8
+ SnowflakeDialect._always_quote_join, _get_full_schema_name, and
9
+ _StructuredTypeInfoManager.get_table_columns. Correctness here guarantees
10
+ all three callers stay consistent automatically.
11
+ """
12
+
13
+ import re
14
+
15
+ import pytest
16
+ from sqlalchemy.sql.elements import quoted_name
17
+
18
+ from snowflake.sqlalchemy.name_utils import _NameUtils
19
+ from snowflake.sqlalchemy.snowdialect import SnowflakeDialect
20
+
21
+
22
+ @pytest.fixture
23
+ def nu():
24
+ return _NameUtils(SnowflakeDialect().identifier_preparer)
25
+
26
+
27
+ # ---------------------------------------------------------------------------
28
+ # _quote_component
29
+ # ---------------------------------------------------------------------------
30
+
31
+
32
+ @pytest.mark.parametrize(
33
+ "component, expected",
34
+ [
35
+ pytest.param(
36
+ quoted_name("myschema", None), '"MYSCHEMA"', id="lowercase→uppercase"
37
+ ),
38
+ pytest.param(quoted_name("MYSCHEMA", None), '"MYSCHEMA"', id="uppercase"),
39
+ pytest.param(quoted_name("MySchema", None), '"MySchema"', id="mixed-case"),
40
+ pytest.param(
41
+ quoted_name("myschema", True),
42
+ '"myschema"',
43
+ id="quote=True preserves lowercase",
44
+ ),
45
+ pytest.param(
46
+ quoted_name("MySchema", True),
47
+ '"MySchema"',
48
+ id="quote=True preserves mixed-case",
49
+ ),
50
+ ],
51
+ )
52
+ def test_quote_component(nu, component, expected):
53
+ assert nu._quote_component(component) == expected
54
+
55
+
56
+ def test_quote_component_escapes_internal_double_quote(nu):
57
+ result = nu._quote_component(quoted_name('my"schema', None))
58
+ assert result.startswith('"') and result.endswith('"')
59
+ assert '""' in result # SQL double-quote escaping
60
+
61
+
62
+ # ---------------------------------------------------------------------------
63
+ # always_quote_join
64
+ # ---------------------------------------------------------------------------
65
+
66
+
67
+ @pytest.mark.parametrize(
68
+ "idents, expected",
69
+ [
70
+ # plain identifiers
71
+ pytest.param(
72
+ ("myschema", "mytable"), '"MYSCHEMA"."MYTABLE"', id="plain lowercase"
73
+ ),
74
+ pytest.param(
75
+ ("MYSCHEMA", "MYTABLE"), '"MYSCHEMA"."MYTABLE"', id="plain uppercase"
76
+ ),
77
+ pytest.param(("MySchema", "MyTable"), '"MySchema"."MyTable"', id="mixed case"),
78
+ pytest.param(
79
+ (None, "myschema", "mytable"), '"MYSCHEMA"."MYTABLE"', id="None skipped"
80
+ ),
81
+ pytest.param(("myschema",), '"MYSCHEMA"', id="single ident"),
82
+ # database-qualified schemas — 1.10.1 regression guard
83
+ pytest.param(
84
+ ("MYDB.MYSCHEMA", "mytable"),
85
+ '"MYDB"."MYSCHEMA"."MYTABLE"',
86
+ id="unquoted db-qualified schema",
87
+ ),
88
+ pytest.param(
89
+ ("mydb.myschema", "mytable"),
90
+ '"MYDB"."MYSCHEMA"."MYTABLE"',
91
+ id="lowercase db-qualified schema",
92
+ ),
93
+ pytest.param(
94
+ ('"MYDB"."MYSCHEMA"', "mytable"),
95
+ '"MYDB"."MYSCHEMA"."MYTABLE"',
96
+ id="pre-quoted db-qualified schema",
97
+ ),
98
+ pytest.param(
99
+ ('"my.schema"', "mytable"),
100
+ '"my.schema"."MYTABLE"',
101
+ id="literal dot in quoted component not split",
102
+ ),
103
+ pytest.param(
104
+ ("myschema", "foo.bar"),
105
+ '"MYSCHEMA"."FOO"."BAR"',
106
+ id="dot in table splits into components",
107
+ ),
108
+ # case-sensitivity signal (quote=True) preserved
109
+ pytest.param(
110
+ (quoted_name("myschema", True), "mytable"),
111
+ '"myschema"."MYTABLE"',
112
+ id="quote=True lowercase case preserved",
113
+ ),
114
+ pytest.param(
115
+ (quoted_name("MySchema", True), "mytable"),
116
+ '"MySchema"."MYTABLE"',
117
+ id="quote=True mixed-case preserved",
118
+ ),
119
+ ],
120
+ )
121
+ def test_always_quote_join(nu, idents, expected):
122
+ assert nu.always_quote_join(*idents) == expected
123
+ assert '"""' not in nu.always_quote_join(*idents)
124
+
125
+
126
+ # ---------------------------------------------------------------------------
127
+ # always_quote_join — identifier quoting correctness
128
+ # ---------------------------------------------------------------------------
129
+
130
+
131
+ @pytest.mark.parametrize(
132
+ "idents, blocked",
133
+ [
134
+ pytest.param(
135
+ ("myschema; DROP TABLE users--", "mytable"), ";", id="semicolon in schema"
136
+ ),
137
+ pytest.param(("myschema", "mytable; SELECT 1--"), ";", id="semicolon in table"),
138
+ pytest.param(("x'; DROP TABLE t--", "t"), "DROP", id="DROP in schema"),
139
+ ],
140
+ )
141
+ def test_always_quote_join_identifier_quoting(nu, idents, blocked):
142
+ result = nu.always_quote_join(*idents)
143
+ stripped = re.sub(r'"[^"]*"', "", result)
144
+ assert blocked not in stripped, f"Pattern {blocked!r} escaped quoting: {result!r}"