snowflake-sqlalchemy 1.7.1__tar.gz → 1.7.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 (130) hide show
  1. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/DESCRIPTION.md +8 -1
  2. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/PKG-INFO +78 -3
  3. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/README.md +73 -0
  4. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/pyproject.toml +3 -0
  5. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/src/snowflake/sqlalchemy/base.py +20 -3
  6. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/src/snowflake/sqlalchemy/custom_types.py +33 -3
  7. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/src/snowflake/sqlalchemy/parser/custom_type_parser.py +78 -23
  8. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/src/snowflake/sqlalchemy/snowdialect.py +134 -105
  9. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/src/snowflake/sqlalchemy/version.py +1 -1
  10. snowflake_sqlalchemy-1.7.2/tests/__snapshots__/test_structured_datatypes.ambr +246 -0
  11. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/conftest.py +27 -0
  12. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/test_compiler.py +16 -1
  13. snowflake_sqlalchemy-1.7.2/tests/test_index_reflection.py +68 -0
  14. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/test_quote.py +23 -0
  15. snowflake_sqlalchemy-1.7.2/tests/test_quote_identifiers.py +43 -0
  16. snowflake_sqlalchemy-1.7.2/tests/test_structured_datatypes.py +582 -0
  17. snowflake_sqlalchemy-1.7.2/tests/test_transactions.py +157 -0
  18. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/test_unit_structured_types.py +10 -2
  19. snowflake_sqlalchemy-1.7.1/tests/__snapshots__/test_structured_datatypes.ambr +0 -90
  20. snowflake_sqlalchemy-1.7.1/tests/test_index_reflection.py +0 -34
  21. snowflake_sqlalchemy-1.7.1/tests/test_structured_datatypes.py +0 -271
  22. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/.gitignore +0 -0
  23. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/.gitmodules +0 -0
  24. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/.pre-commit-config.yaml +0 -0
  25. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/LICENSE.txt +0 -0
  26. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/MANIFEST.in +0 -0
  27. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/ci/build.sh +0 -0
  28. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/ci/build_docker.sh +0 -0
  29. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/ci/docker/sqlalchemy_build/Dockerfile +0 -0
  30. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/ci/docker/sqlalchemy_build/scripts/entrypoint.sh +0 -0
  31. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/ci/set_base_image.sh +0 -0
  32. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/ci/test.sh +0 -0
  33. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/ci/test_docker.sh +0 -0
  34. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/ci/test_linux.sh +0 -0
  35. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/license_header.txt +0 -0
  36. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/setup.cfg +0 -0
  37. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/snyk/requirements.txt +0 -0
  38. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/snyk/requiremtnts.txt +0 -0
  39. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/snyk/update_requirements.py +0 -0
  40. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/src/snowflake/sqlalchemy/__init__.py +0 -0
  41. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/src/snowflake/sqlalchemy/_constants.py +0 -0
  42. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/src/snowflake/sqlalchemy/compat.py +0 -0
  43. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/src/snowflake/sqlalchemy/custom_commands.py +0 -0
  44. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/src/snowflake/sqlalchemy/exc.py +0 -0
  45. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/src/snowflake/sqlalchemy/functions.py +0 -0
  46. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/src/snowflake/sqlalchemy/provision.py +0 -0
  47. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/src/snowflake/sqlalchemy/requirements.py +0 -0
  48. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/src/snowflake/sqlalchemy/sql/__init__.py +0 -0
  49. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/src/snowflake/sqlalchemy/sql/custom_schema/__init__.py +0 -0
  50. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/src/snowflake/sqlalchemy/sql/custom_schema/clustered_table.py +0 -0
  51. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/src/snowflake/sqlalchemy/sql/custom_schema/custom_table_base.py +0 -0
  52. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/src/snowflake/sqlalchemy/sql/custom_schema/custom_table_prefix.py +0 -0
  53. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/src/snowflake/sqlalchemy/sql/custom_schema/dynamic_table.py +0 -0
  54. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/src/snowflake/sqlalchemy/sql/custom_schema/hybrid_table.py +0 -0
  55. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/src/snowflake/sqlalchemy/sql/custom_schema/iceberg_table.py +0 -0
  56. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/src/snowflake/sqlalchemy/sql/custom_schema/options/__init__.py +0 -0
  57. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/src/snowflake/sqlalchemy/sql/custom_schema/options/as_query_option.py +0 -0
  58. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/src/snowflake/sqlalchemy/sql/custom_schema/options/cluster_by_option.py +0 -0
  59. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/src/snowflake/sqlalchemy/sql/custom_schema/options/identifier_option.py +0 -0
  60. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/src/snowflake/sqlalchemy/sql/custom_schema/options/invalid_table_option.py +0 -0
  61. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/src/snowflake/sqlalchemy/sql/custom_schema/options/keyword_option.py +0 -0
  62. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/src/snowflake/sqlalchemy/sql/custom_schema/options/keywords.py +0 -0
  63. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/src/snowflake/sqlalchemy/sql/custom_schema/options/literal_option.py +0 -0
  64. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/src/snowflake/sqlalchemy/sql/custom_schema/options/table_option.py +0 -0
  65. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/src/snowflake/sqlalchemy/sql/custom_schema/options/target_lag_option.py +0 -0
  66. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/src/snowflake/sqlalchemy/sql/custom_schema/snowflake_table.py +0 -0
  67. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/src/snowflake/sqlalchemy/sql/custom_schema/table_from_query.py +0 -0
  68. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/src/snowflake/sqlalchemy/util.py +0 -0
  69. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tested_requirements/requirements_310.reqs +0 -0
  70. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tested_requirements/requirements_37.reqs +0 -0
  71. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tested_requirements/requirements_38.reqs +0 -0
  72. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tested_requirements/requirements_39.reqs +0 -0
  73. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/README.rst +0 -0
  74. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/__init__.py +0 -0
  75. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/__snapshots__/test_compile_dynamic_table.ambr +0 -0
  76. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/__snapshots__/test_core.ambr +0 -0
  77. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/__snapshots__/test_orm.ambr +0 -0
  78. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/__snapshots__/test_reflect_dynamic_table.ambr +0 -0
  79. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/__snapshots__/test_unit_structured_types.ambr +0 -0
  80. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/custom_tables/__init__.py +0 -0
  81. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/custom_tables/__snapshots__/test_compile_dynamic_table.ambr +0 -0
  82. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/custom_tables/__snapshots__/test_compile_hybrid_table.ambr +0 -0
  83. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/custom_tables/__snapshots__/test_compile_iceberg_table.ambr +0 -0
  84. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/custom_tables/__snapshots__/test_compile_snowflake_table.ambr +0 -0
  85. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/custom_tables/__snapshots__/test_create_dynamic_table.ambr +0 -0
  86. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/custom_tables/__snapshots__/test_create_hybrid_table.ambr +0 -0
  87. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/custom_tables/__snapshots__/test_create_iceberg_table.ambr +0 -0
  88. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/custom_tables/__snapshots__/test_create_snowflake_table.ambr +0 -0
  89. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/custom_tables/__snapshots__/test_generic_options.ambr +0 -0
  90. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/custom_tables/__snapshots__/test_reflect_hybrid_table.ambr +0 -0
  91. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/custom_tables/__snapshots__/test_reflect_snowflake_table.ambr +0 -0
  92. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/custom_tables/test_compile_dynamic_table.py +0 -0
  93. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/custom_tables/test_compile_hybrid_table.py +0 -0
  94. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/custom_tables/test_compile_iceberg_table.py +0 -0
  95. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/custom_tables/test_compile_snowflake_table.py +0 -0
  96. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/custom_tables/test_create_dynamic_table.py +0 -0
  97. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/custom_tables/test_create_hybrid_table.py +0 -0
  98. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/custom_tables/test_create_iceberg_table.py +0 -0
  99. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/custom_tables/test_create_snowflake_table.py +0 -0
  100. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/custom_tables/test_generic_options.py +0 -0
  101. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/custom_tables/test_reflect_dynamic_table.py +0 -0
  102. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/custom_tables/test_reflect_hybrid_table.py +0 -0
  103. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/custom_tables/test_reflect_snowflake_table.py +0 -0
  104. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/data/users.txt +0 -0
  105. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/sqlalchemy_test_suite/README.md +0 -0
  106. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/sqlalchemy_test_suite/__init__.py +0 -0
  107. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/sqlalchemy_test_suite/conftest.py +0 -0
  108. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/sqlalchemy_test_suite/test_suite.py +0 -0
  109. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/sqlalchemy_test_suite/test_suite_20.py +0 -0
  110. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/test_copy.py +0 -0
  111. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/test_core.py +0 -0
  112. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/test_create.py +0 -0
  113. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/test_custom_functions.py +0 -0
  114. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/test_custom_types.py +0 -0
  115. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/test_geography.py +0 -0
  116. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/test_geometry.py +0 -0
  117. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/test_imports.py +0 -0
  118. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/test_multivalues_insert.py +0 -0
  119. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/test_orm.py +0 -0
  120. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/test_pandas.py +0 -0
  121. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/test_qmark.py +0 -0
  122. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/test_semi_structured_datatypes.py +0 -0
  123. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/test_sequence.py +0 -0
  124. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/test_timestamp.py +0 -0
  125. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/test_unit_core.py +0 -0
  126. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/test_unit_cte.py +0 -0
  127. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/test_unit_types.py +0 -0
  128. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/test_unit_url.py +0 -0
  129. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tests/util.py +0 -0
  130. {snowflake_sqlalchemy-1.7.1 → snowflake_sqlalchemy-1.7.2}/tox.ini +0 -0
@@ -9,12 +9,19 @@ Source code is also available at:
9
9
 
10
10
  # Release Notes
11
11
 
12
+ - v1.7.2(December 18, 2024)
13
+ - Fix quoting of `_` as column name
14
+ - Fix index columns was not being reflected
15
+ - Fix index reflection cache not working
16
+ - Add support for structured OBJECT datatype
17
+ - Add support for structured ARRAY datatype
18
+
12
19
  - v1.7.1(December 02, 2024)
13
20
  - Add support for partition by to copy into <location>
14
21
  - Fix BOOLEAN type not found in snowdialect
22
+ - Add support for autocommit Isolation Level
15
23
 
16
24
  - v1.7.0(November 21, 2024)
17
-
18
25
  - Add support for dynamic tables and required options
19
26
  - Add support for hybrid tables
20
27
  - Fixed SAWarning when registering functions with existing name in default namespace
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: snowflake-sqlalchemy
3
- Version: 1.7.1
3
+ Version: 1.7.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
@@ -8,7 +8,8 @@ Project-URL: Homepage, https://www.snowflake.com/
8
8
  Project-URL: Issues, https://github.com/snowflakedb/snowflake-sqlalchemy/issues
9
9
  Project-URL: Source, https://github.com/snowflakedb/snowflake-sqlalchemy
10
10
  Author-email: "Snowflake Inc." <triage-snowpark-python-api-dl@snowflake.com>
11
- License: Apache-2.0
11
+ License-Expression: Apache-2.0
12
+ License-File: LICENSE.txt
12
13
  Keywords: Snowflake,analytics,cloud,database,db,warehouse
13
14
  Classifier: Development Status :: 5 - Production/Stable
14
15
  Classifier: Environment :: Console
@@ -45,6 +46,7 @@ Requires-Dist: pytest-cov; extra == 'development'
45
46
  Requires-Dist: pytest-rerunfailures; extra == 'development'
46
47
  Requires-Dist: pytest-timeout; extra == 'development'
47
48
  Requires-Dist: pytz; extra == 'development'
49
+ Requires-Dist: setuptools; extra == 'development'
48
50
  Requires-Dist: syrupy==4.6.1; extra == 'development'
49
51
  Provides-Extra: pandas
50
52
  Requires-Dist: snowflake-connector-python[pandas]; extra == 'pandas'
@@ -419,6 +421,79 @@ data_object = json.loads(row[1])
419
421
  data_array = json.loads(row[2])
420
422
  ```
421
423
 
424
+ ### Structured Data Types Support
425
+
426
+ This module defines custom SQLAlchemy types for Snowflake structured data, specifically for **Iceberg tables**.
427
+ The types —**MAP**, **OBJECT**, and **ARRAY**— allow you to store complex data structures in your SQLAlchemy models.
428
+ For detailed information, refer to the Snowflake [Structured data types](https://docs.snowflake.com/en/sql-reference/data-types-structured) documentation.
429
+
430
+ ---
431
+
432
+ #### MAP
433
+
434
+ The `MAP` type represents a collection of key-value pairs, where each key and value can have different types.
435
+
436
+ - **Key Type**: The type of the keys (e.g., `TEXT`, `NUMBER`).
437
+ - **Value Type**: The type of the values (e.g., `TEXT`, `NUMBER`).
438
+ - **Not Null**: Whether `NULL` values are allowed (default is `False`).
439
+
440
+ *Example Usage*
441
+
442
+ ```python
443
+ IcebergTable(
444
+ table_name,
445
+ metadata,
446
+ Column("id", Integer, primary_key=True),
447
+ Column("map_col", MAP(NUMBER(10, 0), TEXT(16777216))),
448
+ external_volume="external_volume",
449
+ base_location="base_location",
450
+ )
451
+ ```
452
+
453
+ #### OBJECT
454
+
455
+ The `OBJECT` type represents a semi-structured object with named fields. Each field can have a specific type, and you can also specify whether each field is nullable.
456
+
457
+ - **Items Types**: A dictionary of field names and their types. The type can optionally include a nullable flag (`True` for not nullable, `False` for nullable, default is `False`).
458
+
459
+ *Example Usage*
460
+
461
+ ```python
462
+ IcebergTable(
463
+ table_name,
464
+ metadata,
465
+ Column("id", Integer, primary_key=True),
466
+ Column(
467
+ "object_col",
468
+ OBJECT(key1=(TEXT(16777216), False), key2=(NUMBER(10, 0), False)),
469
+ OBJECT(key1=TEXT(16777216), key2=NUMBER(10, 0)), # Without nullable flag
470
+ ),
471
+ external_volume="external_volume",
472
+ base_location="base_location",
473
+ )
474
+ ```
475
+
476
+ #### ARRAY
477
+
478
+ The `ARRAY` type represents an ordered list of values, where each element has the same type. The type of the elements is defined when creating the array.
479
+
480
+ - **Value Type**: The type of the elements in the array (e.g., `TEXT`, `NUMBER`).
481
+ - **Not Null**: Whether `NULL` values are allowed (default is `False`).
482
+
483
+ *Example Usage*
484
+
485
+ ```python
486
+ IcebergTable(
487
+ table_name,
488
+ metadata,
489
+ Column("id", Integer, primary_key=True),
490
+ Column("array_col", ARRAY(TEXT(16777216))),
491
+ external_volume="external_volume",
492
+ base_location="base_location",
493
+ )
494
+ ```
495
+
496
+
422
497
  ### CLUSTER BY Support
423
498
 
424
499
  Snowflake SQLAchemy supports the `CLUSTER BY` parameter for tables. For information about the parameter, see :doc:`/sql-reference/sql/create-table`.
@@ -367,6 +367,79 @@ data_object = json.loads(row[1])
367
367
  data_array = json.loads(row[2])
368
368
  ```
369
369
 
370
+ ### Structured Data Types Support
371
+
372
+ This module defines custom SQLAlchemy types for Snowflake structured data, specifically for **Iceberg tables**.
373
+ The types —**MAP**, **OBJECT**, and **ARRAY**— allow you to store complex data structures in your SQLAlchemy models.
374
+ For detailed information, refer to the Snowflake [Structured data types](https://docs.snowflake.com/en/sql-reference/data-types-structured) documentation.
375
+
376
+ ---
377
+
378
+ #### MAP
379
+
380
+ The `MAP` type represents a collection of key-value pairs, where each key and value can have different types.
381
+
382
+ - **Key Type**: The type of the keys (e.g., `TEXT`, `NUMBER`).
383
+ - **Value Type**: The type of the values (e.g., `TEXT`, `NUMBER`).
384
+ - **Not Null**: Whether `NULL` values are allowed (default is `False`).
385
+
386
+ *Example Usage*
387
+
388
+ ```python
389
+ IcebergTable(
390
+ table_name,
391
+ metadata,
392
+ Column("id", Integer, primary_key=True),
393
+ Column("map_col", MAP(NUMBER(10, 0), TEXT(16777216))),
394
+ external_volume="external_volume",
395
+ base_location="base_location",
396
+ )
397
+ ```
398
+
399
+ #### OBJECT
400
+
401
+ The `OBJECT` type represents a semi-structured object with named fields. Each field can have a specific type, and you can also specify whether each field is nullable.
402
+
403
+ - **Items Types**: A dictionary of field names and their types. The type can optionally include a nullable flag (`True` for not nullable, `False` for nullable, default is `False`).
404
+
405
+ *Example Usage*
406
+
407
+ ```python
408
+ IcebergTable(
409
+ table_name,
410
+ metadata,
411
+ Column("id", Integer, primary_key=True),
412
+ Column(
413
+ "object_col",
414
+ OBJECT(key1=(TEXT(16777216), False), key2=(NUMBER(10, 0), False)),
415
+ OBJECT(key1=TEXT(16777216), key2=NUMBER(10, 0)), # Without nullable flag
416
+ ),
417
+ external_volume="external_volume",
418
+ base_location="base_location",
419
+ )
420
+ ```
421
+
422
+ #### ARRAY
423
+
424
+ The `ARRAY` type represents an ordered list of values, where each element has the same type. The type of the elements is defined when creating the array.
425
+
426
+ - **Value Type**: The type of the elements in the array (e.g., `TEXT`, `NUMBER`).
427
+ - **Not Null**: Whether `NULL` values are allowed (default is `False`).
428
+
429
+ *Example Usage*
430
+
431
+ ```python
432
+ IcebergTable(
433
+ table_name,
434
+ metadata,
435
+ Column("id", Integer, primary_key=True),
436
+ Column("array_col", ARRAY(TEXT(16777216))),
437
+ external_volume="external_volume",
438
+ base_location="base_location",
439
+ )
440
+ ```
441
+
442
+
370
443
  ### CLUSTER BY Support
371
444
 
372
445
  Snowflake SQLAchemy supports the `CLUSTER BY` parameter for tables. For information about the parameter, see :doc:`/sql-reference/sql/create-table`.
@@ -47,6 +47,7 @@ path = "src/snowflake/sqlalchemy/version.py"
47
47
  development = [
48
48
  "pre-commit",
49
49
  "pytest",
50
+ "setuptools",
50
51
  "pytest-cov",
51
52
  "pytest-timeout",
52
53
  "pytest-rerunfailures",
@@ -74,6 +75,8 @@ exclude = ["/.github"]
74
75
  packages = ["src/snowflake"]
75
76
 
76
77
  [tool.hatch.envs.default]
78
+ path = ".venv"
79
+ type = "virtual"
77
80
  extra-dependencies = ["SQLAlchemy>=1.4.19,<2.1.0"]
78
81
  features = ["development", "pandas"]
79
82
  python = "3.8"
@@ -5,6 +5,7 @@
5
5
  import itertools
6
6
  import operator
7
7
  import re
8
+ import string
8
9
  from typing import List
9
10
 
10
11
  from sqlalchemy import exc as sa_exc
@@ -114,7 +115,8 @@ RESERVED_WORDS = frozenset(
114
115
  AUTOCOMMIT_REGEXP = re.compile(
115
116
  r"\s*(?:UPDATE|INSERT|DELETE|MERGE|COPY)", re.I | re.UNICODE
116
117
  )
117
-
118
+ # used for quoting identifiers ie. table names, column names, etc.
119
+ ILLEGAL_INITIAL_CHARACTERS = frozenset({d for d in string.digits}.union({"_", "$"}))
118
120
 
119
121
  """
120
122
  Overwrite methods to handle Snowflake BCR change:
@@ -439,6 +441,7 @@ class SnowflakeORMSelectCompileState(context.ORMSelectCompileState):
439
441
 
440
442
  class SnowflakeIdentifierPreparer(compiler.IdentifierPreparer):
441
443
  reserved_words = {x.lower() for x in RESERVED_WORDS}
444
+ illegal_initial_characters = ILLEGAL_INITIAL_CHARACTERS
442
445
 
443
446
  def __init__(self, dialect, **kw):
444
447
  quote = '"'
@@ -1093,10 +1096,24 @@ class SnowflakeTypeCompiler(compiler.GenericTypeCompiler):
1093
1096
  )
1094
1097
 
1095
1098
  def visit_ARRAY(self, type_, **kw):
1096
- return "ARRAY"
1099
+ if type_.is_semi_structured:
1100
+ return "ARRAY"
1101
+ not_null = f" {NOT_NULL}" if type_.not_null else ""
1102
+ return f"ARRAY({type_.value_type.compile()}{not_null})"
1097
1103
 
1098
1104
  def visit_OBJECT(self, type_, **kw):
1099
- return "OBJECT"
1105
+ if type_.is_semi_structured:
1106
+ return "OBJECT"
1107
+ else:
1108
+ contents = []
1109
+ for key in type_.items_types:
1110
+
1111
+ row_text = f"{key} {type_.items_types[key][0].compile()}"
1112
+ # Type and not null is specified
1113
+ if len(type_.items_types[key]) > 1:
1114
+ row_text += f"{' NOT NULL' if type_.items_types[key][1] else ''}"
1115
+ contents.append(row_text)
1116
+ return "OBJECT" if contents == [] else f"OBJECT({', '.join(contents)})"
1100
1117
 
1101
1118
  def visit_BLOB(self, type_, **kw):
1102
1119
  return "BINARY"
@@ -1,9 +1,11 @@
1
1
  #
2
2
  # Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved.
3
3
  #
4
+ from typing import Optional, Tuple, Union
4
5
 
5
6
  import sqlalchemy.types as sqltypes
6
7
  import sqlalchemy.util as util
8
+ from sqlalchemy.types import TypeEngine
7
9
 
8
10
  TEXT = sqltypes.VARCHAR
9
11
  CHARACTER = sqltypes.CHAR
@@ -38,7 +40,8 @@ class VARIANT(SnowflakeType):
38
40
 
39
41
 
40
42
  class StructuredType(SnowflakeType):
41
- def __init__(self):
43
+ def __init__(self, is_semi_structured: bool = False):
44
+ self.is_semi_structured = is_semi_structured
42
45
  super().__init__()
43
46
 
44
47
 
@@ -57,13 +60,40 @@ class MAP(StructuredType):
57
60
  super().__init__()
58
61
 
59
62
 
60
- class OBJECT(SnowflakeType):
63
+ class OBJECT(StructuredType):
61
64
  __visit_name__ = "OBJECT"
62
65
 
66
+ def __init__(self, **items_types: Union[TypeEngine, Tuple[TypeEngine, bool]]):
67
+ for key, value in items_types.items():
68
+ if not isinstance(value, tuple):
69
+ items_types[key] = (value, False)
63
70
 
64
- class ARRAY(SnowflakeType):
71
+ self.items_types = items_types
72
+ self.is_semi_structured = len(items_types) == 0
73
+ super().__init__()
74
+
75
+ def __repr__(self):
76
+ quote_char = "'"
77
+ return "OBJECT(%s)" % ", ".join(
78
+ [
79
+ f"{repr(key).strip(quote_char)}={repr(value)}"
80
+ for key, value in self.items_types.items()
81
+ ]
82
+ )
83
+
84
+
85
+ class ARRAY(StructuredType):
65
86
  __visit_name__ = "ARRAY"
66
87
 
88
+ def __init__(
89
+ self,
90
+ value_type: Optional[sqltypes.TypeEngine] = None,
91
+ not_null: bool = False,
92
+ ):
93
+ self.value_type = value_type
94
+ self.not_null = not_null
95
+ super().__init__(is_semi_structured=value_type is None)
96
+
67
97
 
68
98
  class TIMESTAMP_TZ(SnowflakeType):
69
99
  __visit_name__ = "TIMESTAMP_TZ"
@@ -1,5 +1,6 @@
1
1
  #
2
2
  # Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved.
3
+ from typing import List
3
4
 
4
5
  import sqlalchemy.types as sqltypes
5
6
  from sqlalchemy.sql.type_api import TypeEngine
@@ -48,11 +49,10 @@ ischema_names = {
48
49
  "DECIMAL": DECIMAL,
49
50
  "DOUBLE": DOUBLE,
50
51
  "FIXED": DECIMAL,
51
- "FLOAT": FLOAT, # Snowflake FLOAT datatype doesn't has parameters
52
+ "FLOAT": FLOAT, # Snowflake FLOAT datatype doesn't have parameters
52
53
  "INT": INTEGER,
53
54
  "INTEGER": INTEGER,
54
55
  "NUMBER": _CUSTOM_DECIMAL,
55
- # 'OBJECT': ?
56
56
  "REAL": REAL,
57
57
  "BYTEINT": SMALLINT,
58
58
  "SMALLINT": SMALLINT,
@@ -74,19 +74,22 @@ ischema_names = {
74
74
  "GEOMETRY": GEOMETRY,
75
75
  }
76
76
 
77
+ NOT_NULL_STR = "NOT NULL"
77
78
 
78
- def extract_parameters(text: str) -> list:
79
+
80
+ def tokenize_parameters(text: str, character_for_strip=",") -> list:
79
81
  """
80
82
  Extracts parameters from a comma-separated string, handling parentheses.
81
83
 
82
84
  :param text: A string with comma-separated parameters, which may include parentheses.
83
85
 
86
+ :param character_for_strip: A character to strip the text.
87
+
84
88
  :return: A list of parameters as strings.
85
89
 
86
90
  :example:
87
91
  For input `"a, (b, c), d"`, the output is `['a', '(b, c)', 'd']`.
88
92
  """
89
-
90
93
  output_parameters = []
91
94
  parameter = ""
92
95
  open_parenthesis = 0
@@ -97,9 +100,9 @@ def extract_parameters(text: str) -> list:
97
100
  elif c == ")":
98
101
  open_parenthesis -= 1
99
102
 
100
- if open_parenthesis > 0 or c != ",":
103
+ if open_parenthesis > 0 or c != character_for_strip:
101
104
  parameter += c
102
- elif c == ",":
105
+ elif c == character_for_strip:
103
106
  output_parameters.append(parameter.strip(" "))
104
107
  parameter = ""
105
108
  if parameter != "":
@@ -107,6 +110,21 @@ def extract_parameters(text: str) -> list:
107
110
  return output_parameters
108
111
 
109
112
 
113
+ def parse_index_columns(columns: str) -> List[str]:
114
+ """
115
+ Parses a string with a list of columns for an index.
116
+
117
+ :param columns: A string with a list of columns for an index, which may include parentheses.
118
+ :param compiler: A SQLAlchemy compiler.
119
+
120
+ :return: A list of columns as strings.
121
+
122
+ :example:
123
+ For input `"[A, B, C]"`, the output is `['A', 'B', 'C']`.
124
+ """
125
+ return [column.strip() for column in columns.strip("[]").split(",")]
126
+
127
+
110
128
  def parse_type(type_text: str) -> TypeEngine:
111
129
  """
112
130
  Parses a type definition string and returns the corresponding SQLAlchemy type.
@@ -122,14 +140,17 @@ def parse_type(type_text: str) -> TypeEngine:
122
140
  parse_type("VARCHAR(255)")
123
141
  String(length=255)
124
142
  """
143
+
125
144
  index = type_text.find("(")
126
145
  type_name = type_text[:index] if index != -1 else type_text
146
+
127
147
  parameters = (
128
- extract_parameters(type_text[index + 1 : -1]) if type_name != type_text else []
148
+ tokenize_parameters(type_text[index + 1 : -1]) if type_name != type_text else []
129
149
  )
130
150
 
131
151
  col_type_class = ischema_names.get(type_name, None)
132
152
  col_type_kw = {}
153
+
133
154
  if col_type_class is None:
134
155
  col_type_class = NullType
135
156
  else:
@@ -139,6 +160,10 @@ def parse_type(type_text: str) -> TypeEngine:
139
160
  col_type_kw = __parse_type_with_length_parameters(parameters)
140
161
  elif issubclass(col_type_class, MAP):
141
162
  col_type_kw = __parse_map_type_parameters(parameters)
163
+ elif issubclass(col_type_class, OBJECT):
164
+ col_type_kw = __parse_object_type_parameters(parameters)
165
+ elif issubclass(col_type_class, ARRAY):
166
+ col_type_kw = __parse_nullable_parameter(parameters)
142
167
  if col_type_kw is None:
143
168
  col_type_class = NullType
144
169
  col_type_kw = {}
@@ -146,33 +171,63 @@ def parse_type(type_text: str) -> TypeEngine:
146
171
  return col_type_class(**col_type_kw)
147
172
 
148
173
 
149
- def __parse_map_type_parameters(parameters):
150
- if len(parameters) != 2:
174
+ def __parse_object_type_parameters(parameters):
175
+ object_rows = {}
176
+ not_null_parts = NOT_NULL_STR.split(" ")
177
+ for parameter in parameters:
178
+ parameter_parts = tokenize_parameters(parameter, " ")
179
+ if len(parameter_parts) >= 2:
180
+ key = parameter_parts[0]
181
+ value_type = parse_type(parameter_parts[1])
182
+ if isinstance(value_type, NullType):
183
+ return None
184
+ not_null = (
185
+ len(parameter_parts) == 4
186
+ and parameter_parts[2] == not_null_parts[0]
187
+ and parameter_parts[3] == not_null_parts[1]
188
+ )
189
+ object_rows[key] = (value_type, not_null)
190
+ return object_rows
191
+
192
+
193
+ def __parse_nullable_parameter(parameters):
194
+ if len(parameters) < 1:
195
+ return {}
196
+ elif len(parameters) > 1:
151
197
  return None
152
-
153
- key_type_str = parameters[0]
154
- value_type_str = parameters[1]
155
- not_null_str = "NOT NULL"
156
- not_null = False
198
+ parameter_str = parameters[0]
199
+ is_not_null = False
157
200
  if (
158
- len(value_type_str) >= len(not_null_str)
159
- and value_type_str[-len(not_null_str) :] == not_null_str
201
+ len(parameter_str) >= len(NOT_NULL_STR)
202
+ and parameter_str[-len(NOT_NULL_STR) :] == NOT_NULL_STR
160
203
  ):
161
- not_null = True
162
- value_type_str = value_type_str[: -len(not_null_str) - 1]
204
+ is_not_null = True
205
+ parameter_str = parameter_str[: -len(NOT_NULL_STR) - 1]
163
206
 
164
- key_type: TypeEngine = parse_type(key_type_str)
165
- value_type: TypeEngine = parse_type(value_type_str)
166
- if isinstance(key_type, NullType) or isinstance(value_type, NullType):
207
+ value_type: TypeEngine = parse_type(parameter_str)
208
+ if isinstance(value_type, NullType):
167
209
  return None
168
210
 
169
211
  return {
170
- "key_type": key_type,
171
212
  "value_type": value_type,
172
- "not_null": not_null,
213
+ "not_null": is_not_null,
173
214
  }
174
215
 
175
216
 
217
+ def __parse_map_type_parameters(parameters):
218
+ if len(parameters) != 2:
219
+ return None
220
+
221
+ key_type_str = parameters[0]
222
+ value_type_str = parameters[1]
223
+ key_type: TypeEngine = parse_type(key_type_str)
224
+ value_type = __parse_nullable_parameter([value_type_str])
225
+ if isinstance(value_type, NullType) or isinstance(key_type, NullType):
226
+ return None
227
+
228
+ return {"key_type": key_type, **value_type}
229
+
230
+
176
231
  def __parse_type_with_length_parameters(parameters):
177
232
  return (
178
233
  {"length": int(parameters[0])}