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.
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/.pre-commit-config.yaml +9 -5
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/DESCRIPTION.md +7 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/PKG-INFO +1 -1
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/ci/build.sh +1 -1
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/ci/test_docker.sh +1 -1
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/ci/test_linux.sh +1 -1
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/base.py +2 -2
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/custom_types.py +1 -1
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/exc.py +12 -6
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/name_utils.py +42 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/snowdialect.py +8 -39
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/sql/custom_schema/options/as_query_option.py +1 -1
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/sql/custom_schema/options/target_lag_option.py +1 -1
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/structured_type_info_manager.py +5 -11
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/version.py +1 -1
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_compiler.py +28 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_core.py +1 -1
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_structured_datatypes.py +5 -12
- snowflake_sqlalchemy-1.10.2/tests/test_unit_name_utils.py +144 -0
- snowflake_sqlalchemy-1.10.2/tests/test_unit_structured_type_info_manager.py +247 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tox.ini +4 -4
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/.gitignore +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/.gitmodules +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/LICENSE.txt +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/MANIFEST.in +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/README.md +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/ci/build_docker.sh +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/ci/docker/sqlalchemy_build/Dockerfile +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/ci/docker/sqlalchemy_build/scripts/entrypoint.sh +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/ci/set_base_image.sh +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/ci/test.sh +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/license_header.txt +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/pyproject.toml +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/setup.cfg +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/snyk/requirements.txt +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/snyk/requiremtnts.txt +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/snyk/update_requirements.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/__init__.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/_constants.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/alembic_util.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/compat.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/custom_commands.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/functions.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/orm.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/parser/custom_type_parser.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/provision.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/requirements.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/sql/__init__.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/sql/custom_schema/__init__.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/sql/custom_schema/clustered_table.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/sql/custom_schema/custom_table_base.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/sql/custom_schema/custom_table_prefix.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/sql/custom_schema/dynamic_table.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/sql/custom_schema/hybrid_table.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/sql/custom_schema/iceberg_table.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/sql/custom_schema/options/__init__.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/sql/custom_schema/options/cluster_by_option.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/sql/custom_schema/options/identifier_option.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/sql/custom_schema/options/invalid_table_option.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/sql/custom_schema/options/keyword_option.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/sql/custom_schema/options/keywords.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/sql/custom_schema/options/literal_option.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/sql/custom_schema/options/table_option.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/sql/custom_schema/snowflake_table.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/sql/custom_schema/table_from_query.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/util.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tested_requirements/requirements_310.reqs +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tested_requirements/requirements_37.reqs +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tested_requirements/requirements_38.reqs +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tested_requirements/requirements_39.reqs +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/README.rst +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/__init__.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/__snapshots__/test_compile_dynamic_table.ambr +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/__snapshots__/test_core.ambr +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/__snapshots__/test_orm.ambr +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/__snapshots__/test_reflect_dynamic_table.ambr +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/__snapshots__/test_structured_datatypes.ambr +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/__snapshots__/test_unit_structured_types.ambr +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/alembic_integration/README.md +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/alembic_integration/conftest.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/alembic_integration/test_multi_schema_fk.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/conftest.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/__init__.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/__snapshots__/test_compile_dynamic_table.ambr +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/__snapshots__/test_compile_hybrid_table.ambr +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/__snapshots__/test_compile_iceberg_table.ambr +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/__snapshots__/test_compile_snowflake_table.ambr +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/__snapshots__/test_create_dynamic_table.ambr +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/__snapshots__/test_create_hybrid_table.ambr +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/__snapshots__/test_create_iceberg_table.ambr +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/__snapshots__/test_create_snowflake_table.ambr +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/__snapshots__/test_generic_options.ambr +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/__snapshots__/test_reflect_hybrid_table.ambr +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/__snapshots__/test_reflect_snowflake_table.ambr +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/test_compile_dynamic_table.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/test_compile_hybrid_table.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/test_compile_iceberg_table.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/test_compile_snowflake_table.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/test_create_dynamic_table.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/test_create_hybrid_table.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/test_create_iceberg_table.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/test_create_snowflake_table.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/test_generic_options.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/test_reflect_dynamic_table.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/test_reflect_hybrid_table.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/custom_tables/test_reflect_snowflake_table.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/data/users.txt +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/sqlalchemy_test_suite/README.md +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/sqlalchemy_test_suite/__init__.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/sqlalchemy_test_suite/conftest.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/sqlalchemy_test_suite/test_suite.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/sqlalchemy_test_suite/test_suite_20.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_compat.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_copy.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_create.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_cross_database_reflection.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_custom_functions.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_custom_types.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_decfloat.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_dialect_connect.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_geography.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_geometry.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_imports.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_index_reflection.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_keys.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_loader_criteria_regression.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_multivalues_insert.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_orm.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_pandas.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_qmark.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_quote.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_quote_identifiers.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_semi_structured_datatypes.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_sequence.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_timestamp.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_transactions.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_unit_case_sensitivity.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_unit_core.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_unit_cte.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_unit_orm.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_unit_structured_types.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_unit_types.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_unit_url.py +0 -0
- {snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_vector.py +0 -0
- {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:
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
@@ -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.
|
|
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)"
|
{snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/base.py
RENAMED
|
@@ -152,7 +152,7 @@ class SnowflakeSelectState(SelectState):
|
|
|
152
152
|
raw_columns, left, right, onclause
|
|
153
153
|
)
|
|
154
154
|
else:
|
|
155
|
-
|
|
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.
|
|
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):
|
{snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/custom_types.py
RENAMED
|
@@ -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__(
|
|
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__(
|
|
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__(
|
|
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."
|
{snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/name_utils.py
RENAMED
|
@@ -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)
|
{snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/src/snowflake/sqlalchemy/snowdialect.py
RENAMED
|
@@ -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
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
|
|
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
|
|
411
|
-
# boundaries
|
|
412
|
-
#
|
|
413
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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"""
|
|
@@ -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.
|
|
2453
|
+
[literal(5.5), literal(8), 0.6875, 0.0],
|
|
2454
2454
|
],
|
|
2455
2455
|
)
|
|
2456
2456
|
@pytest.mark.feature_v20
|
{snowflake_sqlalchemy-1.10.0 → snowflake_sqlalchemy-1.10.2}/tests/test_structured_datatypes.py
RENAMED
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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}"
|