snowflake-sqlalchemy 1.7.0__tar.gz → 1.7.1__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 (125) hide show
  1. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/DESCRIPTION.md +5 -1
  2. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/PKG-INFO +152 -4
  3. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/README.md +151 -3
  4. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/src/snowflake/sqlalchemy/base.py +20 -6
  5. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/src/snowflake/sqlalchemy/custom_commands.py +7 -2
  6. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/src/snowflake/sqlalchemy/snowdialect.py +2 -0
  7. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/src/snowflake/sqlalchemy/version.py +1 -1
  8. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/test_copy.py +48 -17
  9. snowflake_sqlalchemy-1.7.1/tests/test_imports.py +64 -0
  10. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/.gitignore +0 -0
  11. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/.gitmodules +0 -0
  12. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/.pre-commit-config.yaml +0 -0
  13. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/LICENSE.txt +0 -0
  14. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/MANIFEST.in +0 -0
  15. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/ci/build.sh +0 -0
  16. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/ci/build_docker.sh +0 -0
  17. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/ci/docker/sqlalchemy_build/Dockerfile +0 -0
  18. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/ci/docker/sqlalchemy_build/scripts/entrypoint.sh +0 -0
  19. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/ci/set_base_image.sh +0 -0
  20. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/ci/test.sh +0 -0
  21. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/ci/test_docker.sh +0 -0
  22. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/ci/test_linux.sh +0 -0
  23. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/license_header.txt +0 -0
  24. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/pyproject.toml +0 -0
  25. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/setup.cfg +0 -0
  26. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/snyk/requirements.txt +0 -0
  27. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/snyk/requiremtnts.txt +0 -0
  28. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/snyk/update_requirements.py +0 -0
  29. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/src/snowflake/sqlalchemy/__init__.py +0 -0
  30. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/src/snowflake/sqlalchemy/_constants.py +0 -0
  31. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/src/snowflake/sqlalchemy/compat.py +0 -0
  32. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/src/snowflake/sqlalchemy/custom_types.py +0 -0
  33. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/src/snowflake/sqlalchemy/exc.py +0 -0
  34. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/src/snowflake/sqlalchemy/functions.py +0 -0
  35. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/src/snowflake/sqlalchemy/parser/custom_type_parser.py +0 -0
  36. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/src/snowflake/sqlalchemy/provision.py +0 -0
  37. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/src/snowflake/sqlalchemy/requirements.py +0 -0
  38. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/src/snowflake/sqlalchemy/sql/__init__.py +0 -0
  39. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/src/snowflake/sqlalchemy/sql/custom_schema/__init__.py +0 -0
  40. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/src/snowflake/sqlalchemy/sql/custom_schema/clustered_table.py +0 -0
  41. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/src/snowflake/sqlalchemy/sql/custom_schema/custom_table_base.py +0 -0
  42. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/src/snowflake/sqlalchemy/sql/custom_schema/custom_table_prefix.py +0 -0
  43. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/src/snowflake/sqlalchemy/sql/custom_schema/dynamic_table.py +0 -0
  44. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/src/snowflake/sqlalchemy/sql/custom_schema/hybrid_table.py +0 -0
  45. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/src/snowflake/sqlalchemy/sql/custom_schema/iceberg_table.py +0 -0
  46. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/src/snowflake/sqlalchemy/sql/custom_schema/options/__init__.py +0 -0
  47. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/src/snowflake/sqlalchemy/sql/custom_schema/options/as_query_option.py +0 -0
  48. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/src/snowflake/sqlalchemy/sql/custom_schema/options/cluster_by_option.py +0 -0
  49. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/src/snowflake/sqlalchemy/sql/custom_schema/options/identifier_option.py +0 -0
  50. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/src/snowflake/sqlalchemy/sql/custom_schema/options/invalid_table_option.py +0 -0
  51. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/src/snowflake/sqlalchemy/sql/custom_schema/options/keyword_option.py +0 -0
  52. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/src/snowflake/sqlalchemy/sql/custom_schema/options/keywords.py +0 -0
  53. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/src/snowflake/sqlalchemy/sql/custom_schema/options/literal_option.py +0 -0
  54. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/src/snowflake/sqlalchemy/sql/custom_schema/options/table_option.py +0 -0
  55. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/src/snowflake/sqlalchemy/sql/custom_schema/options/target_lag_option.py +0 -0
  56. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/src/snowflake/sqlalchemy/sql/custom_schema/snowflake_table.py +0 -0
  57. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/src/snowflake/sqlalchemy/sql/custom_schema/table_from_query.py +0 -0
  58. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/src/snowflake/sqlalchemy/util.py +0 -0
  59. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tested_requirements/requirements_310.reqs +0 -0
  60. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tested_requirements/requirements_37.reqs +0 -0
  61. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tested_requirements/requirements_38.reqs +0 -0
  62. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tested_requirements/requirements_39.reqs +0 -0
  63. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/README.rst +0 -0
  64. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/__init__.py +0 -0
  65. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/__snapshots__/test_compile_dynamic_table.ambr +0 -0
  66. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/__snapshots__/test_core.ambr +0 -0
  67. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/__snapshots__/test_orm.ambr +0 -0
  68. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/__snapshots__/test_reflect_dynamic_table.ambr +0 -0
  69. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/__snapshots__/test_structured_datatypes.ambr +0 -0
  70. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/__snapshots__/test_unit_structured_types.ambr +0 -0
  71. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/conftest.py +0 -0
  72. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/custom_tables/__init__.py +0 -0
  73. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/custom_tables/__snapshots__/test_compile_dynamic_table.ambr +0 -0
  74. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/custom_tables/__snapshots__/test_compile_hybrid_table.ambr +0 -0
  75. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/custom_tables/__snapshots__/test_compile_iceberg_table.ambr +0 -0
  76. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/custom_tables/__snapshots__/test_compile_snowflake_table.ambr +0 -0
  77. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/custom_tables/__snapshots__/test_create_dynamic_table.ambr +0 -0
  78. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/custom_tables/__snapshots__/test_create_hybrid_table.ambr +0 -0
  79. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/custom_tables/__snapshots__/test_create_iceberg_table.ambr +0 -0
  80. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/custom_tables/__snapshots__/test_create_snowflake_table.ambr +0 -0
  81. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/custom_tables/__snapshots__/test_generic_options.ambr +0 -0
  82. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/custom_tables/__snapshots__/test_reflect_hybrid_table.ambr +0 -0
  83. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/custom_tables/__snapshots__/test_reflect_snowflake_table.ambr +0 -0
  84. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/custom_tables/test_compile_dynamic_table.py +0 -0
  85. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/custom_tables/test_compile_hybrid_table.py +0 -0
  86. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/custom_tables/test_compile_iceberg_table.py +0 -0
  87. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/custom_tables/test_compile_snowflake_table.py +0 -0
  88. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/custom_tables/test_create_dynamic_table.py +0 -0
  89. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/custom_tables/test_create_hybrid_table.py +0 -0
  90. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/custom_tables/test_create_iceberg_table.py +0 -0
  91. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/custom_tables/test_create_snowflake_table.py +0 -0
  92. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/custom_tables/test_generic_options.py +0 -0
  93. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/custom_tables/test_reflect_dynamic_table.py +0 -0
  94. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/custom_tables/test_reflect_hybrid_table.py +0 -0
  95. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/custom_tables/test_reflect_snowflake_table.py +0 -0
  96. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/data/users.txt +0 -0
  97. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/sqlalchemy_test_suite/README.md +0 -0
  98. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/sqlalchemy_test_suite/__init__.py +0 -0
  99. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/sqlalchemy_test_suite/conftest.py +0 -0
  100. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/sqlalchemy_test_suite/test_suite.py +0 -0
  101. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/sqlalchemy_test_suite/test_suite_20.py +0 -0
  102. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/test_compiler.py +0 -0
  103. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/test_core.py +0 -0
  104. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/test_create.py +0 -0
  105. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/test_custom_functions.py +0 -0
  106. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/test_custom_types.py +0 -0
  107. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/test_geography.py +0 -0
  108. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/test_geometry.py +0 -0
  109. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/test_index_reflection.py +0 -0
  110. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/test_multivalues_insert.py +0 -0
  111. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/test_orm.py +0 -0
  112. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/test_pandas.py +0 -0
  113. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/test_qmark.py +0 -0
  114. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/test_quote.py +0 -0
  115. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/test_semi_structured_datatypes.py +0 -0
  116. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/test_sequence.py +0 -0
  117. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/test_structured_datatypes.py +0 -0
  118. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/test_timestamp.py +0 -0
  119. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/test_unit_core.py +0 -0
  120. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/test_unit_cte.py +0 -0
  121. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/test_unit_structured_types.py +0 -0
  122. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/test_unit_types.py +0 -0
  123. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/test_unit_url.py +0 -0
  124. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tests/util.py +0 -0
  125. {snowflake_sqlalchemy-1.7.0 → snowflake_sqlalchemy-1.7.1}/tox.ini +0 -0
@@ -9,7 +9,11 @@ Source code is also available at:
9
9
 
10
10
  # Release Notes
11
11
 
12
- - v1.7.0(November 22, 2024)
12
+ - v1.7.1(December 02, 2024)
13
+ - Add support for partition by to copy into <location>
14
+ - Fix BOOLEAN type not found in snowdialect
15
+
16
+ - v1.7.0(November 21, 2024)
13
17
 
14
18
  - Add support for dynamic tables and required options
15
19
  - Add support for hybrid tables
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: snowflake-sqlalchemy
3
- Version: 1.7.0
3
+ Version: 1.7.1
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
@@ -60,6 +60,11 @@ Description-Content-Type: text/markdown
60
60
 
61
61
  Snowflake SQLAlchemy runs on the top of the Snowflake Connector for Python as a [dialect](http://docs.sqlalchemy.org/en/latest/dialects/) to bridge a Snowflake database and SQLAlchemy applications.
62
62
 
63
+
64
+ | :exclamation: | For production-affecting or urgent issues related to the connector, please [create a case with Snowflake Support](https://community.snowflake.com/s/article/How-To-Submit-a-Support-Case-in-Snowflake-Lodge). |
65
+ |---------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
66
+
67
+
63
68
  ## Prerequisites
64
69
 
65
70
  ### Snowflake Connector for Python
@@ -256,7 +261,7 @@ finally:
256
261
 
257
262
  # Best
258
263
  try:
259
- with engine.connext() as connection:
264
+ with engine.connect() as connection:
260
265
  connection.execute(text(<SQL>))
261
266
  # or
262
267
  connection.exec_driver_sql(<SQL>)
@@ -277,11 +282,43 @@ t = Table('mytable', metadata,
277
282
 
278
283
  ### Object Name Case Handling
279
284
 
280
- Snowflake stores all case-insensitive object names in uppercase text. In contrast, SQLAlchemy considers all lowercase object names to be case-insensitive. Snowflake SQLAlchemy converts the object name case during schema-level communication, i.e. during table and index reflection. If you use uppercase object names, SQLAlchemy assumes they are case-sensitive and encloses the names with quotes. This behavior will cause mismatches agaisnt data dictionary data received from Snowflake, so unless identifier names have been truly created as case sensitive using quotes, e.g., `"TestDb"`, all lowercase names should be used on the SQLAlchemy side.
285
+ Snowflake stores all case-insensitive object names in uppercase text. In contrast, SQLAlchemy considers all lowercase object names to be case-insensitive. Snowflake SQLAlchemy converts the object name case during schema-level communication, i.e. during table and index reflection. If you use uppercase object names, SQLAlchemy assumes they are case-sensitive and encloses the names with quotes. This behavior will cause mismatches against data dictionary data received from Snowflake, so unless identifier names have been truly created as case sensitive using quotes, e.g., `"TestDb"`, all lowercase names should be used on the SQLAlchemy side.
281
286
 
282
287
  ### Index Support
283
288
 
284
- Snowflake does not utilize indexes, so neither does Snowflake SQLAlchemy.
289
+ Indexes are supported only for Hybrid Tables in Snowflake SQLAlchemy. For more details on limitations and use cases, refer to the [Create Index documentation](https://docs.snowflake.com/en/sql-reference/constraints-indexes.html). You can create an index using the following methods:
290
+
291
+ #### Single Column Index
292
+
293
+ You can create a single column index by setting the `index=True` parameter on the column or by explicitly defining an `Index` object.
294
+
295
+ ```python
296
+ hybrid_test_table_1 = HybridTable(
297
+ "table_name",
298
+ metadata,
299
+ Column("column1", Integer, primary_key=True),
300
+ Column("column2", String, index=True),
301
+ Index("index_1", "column1", "column2")
302
+ )
303
+
304
+ metadata.create_all(engine_testaccount)
305
+ ```
306
+
307
+ #### Multi-Column Index
308
+
309
+ For multi-column indexes, you define the `Index` object specifying the columns that should be indexed.
310
+
311
+ ```python
312
+ hybrid_test_table_1 = HybridTable(
313
+ "table_name",
314
+ metadata,
315
+ Column("column1", Integer, primary_key=True),
316
+ Column("column2", String),
317
+ Index("index_1", "column1", "column2")
318
+ )
319
+
320
+ metadata.create_all(engine_testaccount)
321
+ ```
285
322
 
286
323
  ### Numpy Data Type Support
287
324
 
@@ -508,6 +545,117 @@ copy_into = CopyIntoStorage(from_=users,
508
545
  connection.execute(copy_into)
509
546
  ```
510
547
 
548
+ ### Iceberg Table with Snowflake Catalog support
549
+
550
+ Snowflake SQLAlchemy supports Iceberg Tables with the Snowflake Catalog, along with various related parameters. For detailed information about Iceberg Tables, refer to the Snowflake [CREATE ICEBERG](https://docs.snowflake.com/en/sql-reference/sql/create-iceberg-table-snowflake) documentation.
551
+
552
+ To create an Iceberg Table using Snowflake SQLAlchemy, you can define the table using the SQLAlchemy Core syntax as follows:
553
+
554
+ ```python
555
+ table = IcebergTable(
556
+ "myuser",
557
+ metadata,
558
+ Column("id", Integer, primary_key=True),
559
+ Column("name", String),
560
+ external_volume=external_volume_name,
561
+ base_location="my_iceberg_table",
562
+ as_query="SELECT * FROM table"
563
+ )
564
+ ```
565
+
566
+ Alternatively, you can define the table using a declarative approach:
567
+
568
+ ```python
569
+ class MyUser(Base):
570
+ __tablename__ = "myuser"
571
+
572
+ @classmethod
573
+ def __table_cls__(cls, name, metadata, *arg, **kw):
574
+ return IcebergTable(name, metadata, *arg, **kw)
575
+
576
+ __table_args__ = {
577
+ "external_volume": "my_external_volume",
578
+ "base_location": "my_iceberg_table",
579
+ "as_query": "SELECT * FROM table",
580
+ }
581
+
582
+ id = Column(Integer, primary_key=True)
583
+ name = Column(String)
584
+ ```
585
+
586
+ ### Hybrid Table support
587
+
588
+ Snowflake SQLAlchemy supports Hybrid Tables with indexes. For detailed information, refer to the Snowflake [CREATE HYBRID TABLE](https://docs.snowflake.com/en/sql-reference/sql/create-hybrid-table) documentation.
589
+
590
+ To create a Hybrid Table and add an index, you can use the SQLAlchemy Core syntax as follows:
591
+
592
+ ```python
593
+ table = HybridTable(
594
+ "myuser",
595
+ metadata,
596
+ Column("id", Integer, primary_key=True),
597
+ Column("name", String),
598
+ Index("idx_name", "name")
599
+ )
600
+ ```
601
+
602
+ Alternatively, you can define the table using the declarative approach:
603
+
604
+ ```python
605
+ class MyUser(Base):
606
+ __tablename__ = "myuser"
607
+
608
+ @classmethod
609
+ def __table_cls__(cls, name, metadata, *arg, **kw):
610
+ return HybridTable(name, metadata, *arg, **kw)
611
+
612
+ __table_args__ = (
613
+ Index("idx_name", "name"),
614
+ )
615
+
616
+ id = Column(Integer, primary_key=True)
617
+ name = Column(String)
618
+ ```
619
+
620
+ ### Dynamic Tables support
621
+
622
+ Snowflake SQLAlchemy supports Dynamic Tables. For detailed information, refer to the Snowflake [CREATE DYNAMIC TABLE](https://docs.snowflake.com/en/sql-reference/sql/create-dynamic-table) documentation.
623
+
624
+ To create a Dynamic Table, you can use the SQLAlchemy Core syntax as follows:
625
+
626
+ ```python
627
+ dynamic_test_table_1 = DynamicTable(
628
+ "dynamic_MyUser",
629
+ metadata,
630
+ Column("id", Integer),
631
+ Column("name", String),
632
+ target_lag=(1, TimeUnit.HOURS), # Additionally, you can use SnowflakeKeyword.DOWNSTREAM
633
+ warehouse='test_wh',
634
+ refresh_mode=SnowflakeKeyword.FULL,
635
+ as_query="SELECT id, name from MyUser;"
636
+ )
637
+ ```
638
+
639
+ Alternatively, you can define a table without columns using the SQLAlchemy `select()` construct:
640
+
641
+ ```python
642
+ dynamic_test_table_1 = DynamicTable(
643
+ "dynamic_MyUser",
644
+ metadata,
645
+ target_lag=(1, TimeUnit.HOURS),
646
+ warehouse='test_wh',
647
+ refresh_mode=SnowflakeKeyword.FULL,
648
+ as_query=select(MyUser.id, MyUser.name)
649
+ )
650
+ ```
651
+
652
+ ### Notes
653
+
654
+ - Defining a primary key in a Dynamic Table is not supported, meaning declarative tables don’t support Dynamic Tables.
655
+ - When using the `as_query` parameter with a string, you must explicitly define the columns. However, if you use the SQLAlchemy `select()` construct, you don’t need to explicitly define the columns.
656
+ - Direct data insertion into Dynamic Tables is not supported.
657
+
658
+
511
659
  ## Support
512
660
 
513
661
  Feel free to file an issue or submit a PR here for general cases. For official support, contact Snowflake support at:
@@ -8,6 +8,11 @@
8
8
 
9
9
  Snowflake SQLAlchemy runs on the top of the Snowflake Connector for Python as a [dialect](http://docs.sqlalchemy.org/en/latest/dialects/) to bridge a Snowflake database and SQLAlchemy applications.
10
10
 
11
+
12
+ | :exclamation: | For production-affecting or urgent issues related to the connector, please [create a case with Snowflake Support](https://community.snowflake.com/s/article/How-To-Submit-a-Support-Case-in-Snowflake-Lodge). |
13
+ |---------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
14
+
15
+
11
16
  ## Prerequisites
12
17
 
13
18
  ### Snowflake Connector for Python
@@ -204,7 +209,7 @@ finally:
204
209
 
205
210
  # Best
206
211
  try:
207
- with engine.connext() as connection:
212
+ with engine.connect() as connection:
208
213
  connection.execute(text(<SQL>))
209
214
  # or
210
215
  connection.exec_driver_sql(<SQL>)
@@ -225,11 +230,43 @@ t = Table('mytable', metadata,
225
230
 
226
231
  ### Object Name Case Handling
227
232
 
228
- Snowflake stores all case-insensitive object names in uppercase text. In contrast, SQLAlchemy considers all lowercase object names to be case-insensitive. Snowflake SQLAlchemy converts the object name case during schema-level communication, i.e. during table and index reflection. If you use uppercase object names, SQLAlchemy assumes they are case-sensitive and encloses the names with quotes. This behavior will cause mismatches agaisnt data dictionary data received from Snowflake, so unless identifier names have been truly created as case sensitive using quotes, e.g., `"TestDb"`, all lowercase names should be used on the SQLAlchemy side.
233
+ Snowflake stores all case-insensitive object names in uppercase text. In contrast, SQLAlchemy considers all lowercase object names to be case-insensitive. Snowflake SQLAlchemy converts the object name case during schema-level communication, i.e. during table and index reflection. If you use uppercase object names, SQLAlchemy assumes they are case-sensitive and encloses the names with quotes. This behavior will cause mismatches against data dictionary data received from Snowflake, so unless identifier names have been truly created as case sensitive using quotes, e.g., `"TestDb"`, all lowercase names should be used on the SQLAlchemy side.
229
234
 
230
235
  ### Index Support
231
236
 
232
- Snowflake does not utilize indexes, so neither does Snowflake SQLAlchemy.
237
+ Indexes are supported only for Hybrid Tables in Snowflake SQLAlchemy. For more details on limitations and use cases, refer to the [Create Index documentation](https://docs.snowflake.com/en/sql-reference/constraints-indexes.html). You can create an index using the following methods:
238
+
239
+ #### Single Column Index
240
+
241
+ You can create a single column index by setting the `index=True` parameter on the column or by explicitly defining an `Index` object.
242
+
243
+ ```python
244
+ hybrid_test_table_1 = HybridTable(
245
+ "table_name",
246
+ metadata,
247
+ Column("column1", Integer, primary_key=True),
248
+ Column("column2", String, index=True),
249
+ Index("index_1", "column1", "column2")
250
+ )
251
+
252
+ metadata.create_all(engine_testaccount)
253
+ ```
254
+
255
+ #### Multi-Column Index
256
+
257
+ For multi-column indexes, you define the `Index` object specifying the columns that should be indexed.
258
+
259
+ ```python
260
+ hybrid_test_table_1 = HybridTable(
261
+ "table_name",
262
+ metadata,
263
+ Column("column1", Integer, primary_key=True),
264
+ Column("column2", String),
265
+ Index("index_1", "column1", "column2")
266
+ )
267
+
268
+ metadata.create_all(engine_testaccount)
269
+ ```
233
270
 
234
271
  ### Numpy Data Type Support
235
272
 
@@ -456,6 +493,117 @@ copy_into = CopyIntoStorage(from_=users,
456
493
  connection.execute(copy_into)
457
494
  ```
458
495
 
496
+ ### Iceberg Table with Snowflake Catalog support
497
+
498
+ Snowflake SQLAlchemy supports Iceberg Tables with the Snowflake Catalog, along with various related parameters. For detailed information about Iceberg Tables, refer to the Snowflake [CREATE ICEBERG](https://docs.snowflake.com/en/sql-reference/sql/create-iceberg-table-snowflake) documentation.
499
+
500
+ To create an Iceberg Table using Snowflake SQLAlchemy, you can define the table using the SQLAlchemy Core syntax as follows:
501
+
502
+ ```python
503
+ table = IcebergTable(
504
+ "myuser",
505
+ metadata,
506
+ Column("id", Integer, primary_key=True),
507
+ Column("name", String),
508
+ external_volume=external_volume_name,
509
+ base_location="my_iceberg_table",
510
+ as_query="SELECT * FROM table"
511
+ )
512
+ ```
513
+
514
+ Alternatively, you can define the table using a declarative approach:
515
+
516
+ ```python
517
+ class MyUser(Base):
518
+ __tablename__ = "myuser"
519
+
520
+ @classmethod
521
+ def __table_cls__(cls, name, metadata, *arg, **kw):
522
+ return IcebergTable(name, metadata, *arg, **kw)
523
+
524
+ __table_args__ = {
525
+ "external_volume": "my_external_volume",
526
+ "base_location": "my_iceberg_table",
527
+ "as_query": "SELECT * FROM table",
528
+ }
529
+
530
+ id = Column(Integer, primary_key=True)
531
+ name = Column(String)
532
+ ```
533
+
534
+ ### Hybrid Table support
535
+
536
+ Snowflake SQLAlchemy supports Hybrid Tables with indexes. For detailed information, refer to the Snowflake [CREATE HYBRID TABLE](https://docs.snowflake.com/en/sql-reference/sql/create-hybrid-table) documentation.
537
+
538
+ To create a Hybrid Table and add an index, you can use the SQLAlchemy Core syntax as follows:
539
+
540
+ ```python
541
+ table = HybridTable(
542
+ "myuser",
543
+ metadata,
544
+ Column("id", Integer, primary_key=True),
545
+ Column("name", String),
546
+ Index("idx_name", "name")
547
+ )
548
+ ```
549
+
550
+ Alternatively, you can define the table using the declarative approach:
551
+
552
+ ```python
553
+ class MyUser(Base):
554
+ __tablename__ = "myuser"
555
+
556
+ @classmethod
557
+ def __table_cls__(cls, name, metadata, *arg, **kw):
558
+ return HybridTable(name, metadata, *arg, **kw)
559
+
560
+ __table_args__ = (
561
+ Index("idx_name", "name"),
562
+ )
563
+
564
+ id = Column(Integer, primary_key=True)
565
+ name = Column(String)
566
+ ```
567
+
568
+ ### Dynamic Tables support
569
+
570
+ Snowflake SQLAlchemy supports Dynamic Tables. For detailed information, refer to the Snowflake [CREATE DYNAMIC TABLE](https://docs.snowflake.com/en/sql-reference/sql/create-dynamic-table) documentation.
571
+
572
+ To create a Dynamic Table, you can use the SQLAlchemy Core syntax as follows:
573
+
574
+ ```python
575
+ dynamic_test_table_1 = DynamicTable(
576
+ "dynamic_MyUser",
577
+ metadata,
578
+ Column("id", Integer),
579
+ Column("name", String),
580
+ target_lag=(1, TimeUnit.HOURS), # Additionally, you can use SnowflakeKeyword.DOWNSTREAM
581
+ warehouse='test_wh',
582
+ refresh_mode=SnowflakeKeyword.FULL,
583
+ as_query="SELECT id, name from MyUser;"
584
+ )
585
+ ```
586
+
587
+ Alternatively, you can define a table without columns using the SQLAlchemy `select()` construct:
588
+
589
+ ```python
590
+ dynamic_test_table_1 = DynamicTable(
591
+ "dynamic_MyUser",
592
+ metadata,
593
+ target_lag=(1, TimeUnit.HOURS),
594
+ warehouse='test_wh',
595
+ refresh_mode=SnowflakeKeyword.FULL,
596
+ as_query=select(MyUser.id, MyUser.name)
597
+ )
598
+ ```
599
+
600
+ ### Notes
601
+
602
+ - Defining a primary key in a Dynamic Table is not supported, meaning declarative tables don’t support Dynamic Tables.
603
+ - When using the `as_query` parameter with a string, you must explicitly define the columns. However, if you use the SQLAlchemy `select()` construct, you don’t need to explicitly define the columns.
604
+ - Direct data insertion into Dynamic Tables is not supported.
605
+
606
+
459
607
  ## Support
460
608
 
461
609
  Feel free to file an issue or submit a PR here for general cases. For official support, contact Snowflake support at:
@@ -16,7 +16,8 @@ from sqlalchemy.orm.context import _MapperEntity
16
16
  from sqlalchemy.schema import Sequence, Table
17
17
  from sqlalchemy.sql import compiler, expression, functions
18
18
  from sqlalchemy.sql.base import CompileState
19
- from sqlalchemy.sql.elements import quoted_name
19
+ from sqlalchemy.sql.elements import BindParameter, quoted_name
20
+ from sqlalchemy.sql.expression import Executable
20
21
  from sqlalchemy.sql.selectable import Lateral, SelectState
21
22
 
22
23
  from snowflake.sqlalchemy._constants import DIALECT_NAME
@@ -563,9 +564,8 @@ class SnowflakeCompiler(compiler.SQLCompiler):
563
564
  if isinstance(copy_into.into, Table)
564
565
  else copy_into.into._compiler_dispatch(self, **kw)
565
566
  )
566
- from_ = None
567
567
  if isinstance(copy_into.from_, Table):
568
- from_ = copy_into.from_
568
+ from_ = copy_into.from_.name
569
569
  # this is intended to catch AWSBucket and AzureContainer
570
570
  elif (
571
571
  isinstance(copy_into.from_, AWSBucket)
@@ -576,6 +576,21 @@ class SnowflakeCompiler(compiler.SQLCompiler):
576
576
  # everything else (selects, etc.)
577
577
  else:
578
578
  from_ = f"({copy_into.from_._compiler_dispatch(self, **kw)})"
579
+
580
+ partition_by_value = None
581
+ if isinstance(copy_into.partition_by, (BindParameter, Executable)):
582
+ partition_by_value = copy_into.partition_by.compile(
583
+ compile_kwargs={"literal_binds": True}
584
+ )
585
+ elif copy_into.partition_by is not None:
586
+ partition_by_value = copy_into.partition_by
587
+
588
+ partition_by = (
589
+ f"PARTITION BY {partition_by_value}"
590
+ if partition_by_value is not None and partition_by_value != ""
591
+ else ""
592
+ )
593
+
579
594
  credentials, encryption = "", ""
580
595
  if isinstance(into, tuple):
581
596
  into, credentials, encryption = into
@@ -586,8 +601,7 @@ class SnowflakeCompiler(compiler.SQLCompiler):
586
601
  options_list.sort(key=operator.itemgetter(0))
587
602
  options = (
588
603
  (
589
- " "
590
- + " ".join(
604
+ " ".join(
591
605
  [
592
606
  "{} = {}".format(
593
607
  n,
@@ -608,7 +622,7 @@ class SnowflakeCompiler(compiler.SQLCompiler):
608
622
  options += f" {credentials}"
609
623
  if encryption:
610
624
  options += f" {encryption}"
611
- return f"COPY INTO {into} FROM {from_} {formatter}{options}"
625
+ return f"COPY INTO {into} FROM {' '.join([from_, partition_by, formatter, options])}"
612
626
 
613
627
  def visit_copy_formatter(self, formatter, **kw):
614
628
  options_list = list(formatter.options.items())
@@ -115,18 +115,23 @@ class CopyInto(UpdateBase):
115
115
  __visit_name__ = "copy_into"
116
116
  _bind = None
117
117
 
118
- def __init__(self, from_, into, formatter=None):
118
+ def __init__(self, from_, into, partition_by=None, formatter=None):
119
119
  self.from_ = from_
120
120
  self.into = into
121
121
  self.formatter = formatter
122
122
  self.copy_options = {}
123
+ self.partition_by = partition_by
123
124
 
124
125
  def __repr__(self):
125
126
  """
126
127
  repr for debugging / logging purposes only. For compilation logic, see
127
128
  the corresponding visitor in base.py
128
129
  """
129
- return f"COPY INTO {self.into} FROM {repr(self.from_)} {repr(self.formatter)} ({self.copy_options})"
130
+ val = f"COPY INTO {self.into} FROM {repr(self.from_)}"
131
+ if self.partition_by is not None:
132
+ val += f" PARTITION BY {self.partition_by}"
133
+
134
+ return val + f" {repr(self.formatter)} ({self.copy_options})"
130
135
 
131
136
  def bind(self):
132
137
  return None
@@ -39,6 +39,8 @@ from .custom_types import (
39
39
  _CUSTOM_Float,
40
40
  _CUSTOM_Time,
41
41
  )
42
+ from .parser.custom_type_parser import * # noqa
43
+ from .parser.custom_type_parser import _CUSTOM_DECIMAL # noqa
42
44
  from .parser.custom_type_parser import ischema_names, parse_type
43
45
  from .sql.custom_schema.custom_table_prefix import CustomTablePrefix
44
46
  from .util import (
@@ -3,4 +3,4 @@
3
3
  #
4
4
  # Update this for the versions
5
5
  # Don't change the forth version number from None
6
- VERSION = "1.7.0"
6
+ VERSION = "1.7.1"
@@ -4,7 +4,7 @@
4
4
 
5
5
  import pytest
6
6
  from sqlalchemy import Column, Integer, MetaData, Sequence, String, Table
7
- from sqlalchemy.sql import select, text
7
+ from sqlalchemy.sql import functions, select, text
8
8
 
9
9
  from snowflake.sqlalchemy import (
10
10
  AWSBucket,
@@ -58,8 +58,8 @@ def test_copy_into_location(engine_testaccount, sql_compiler):
58
58
  )
59
59
  assert (
60
60
  sql_compiler(copy_stmt_1)
61
- == "COPY INTO 's3://backup' FROM python_tests_foods FILE_FORMAT=(TYPE=csv "
62
- "ESCAPE=None NULL_IF=('null', 'Null') RECORD_DELIMITER='|') ENCRYPTION="
61
+ == "COPY INTO 's3://backup' FROM python_tests_foods FILE_FORMAT=(TYPE=csv "
62
+ "ESCAPE=None NULL_IF=('null', 'Null') RECORD_DELIMITER='|') ENCRYPTION="
63
63
  "(KMS_KEY_ID='1234abcd-12ab-34cd-56ef-1234567890ab' TYPE='AWS_SSE_KMS')"
64
64
  )
65
65
  copy_stmt_2 = CopyIntoStorage(
@@ -73,8 +73,8 @@ def test_copy_into_location(engine_testaccount, sql_compiler):
73
73
  sql_compiler(copy_stmt_2)
74
74
  == "COPY INTO 's3://backup' FROM (SELECT python_tests_foods.id, "
75
75
  "python_tests_foods.name, python_tests_foods.quantity FROM python_tests_foods "
76
- "WHERE python_tests_foods.id = 1) FILE_FORMAT=(TYPE=json COMPRESSION='zstd' "
77
- "FILE_EXTENSION='json') CREDENTIALS=(AWS_ROLE='some_iam_role') "
76
+ "WHERE python_tests_foods.id = 1) FILE_FORMAT=(TYPE=json COMPRESSION='zstd' "
77
+ "FILE_EXTENSION='json') CREDENTIALS=(AWS_ROLE='some_iam_role') "
78
78
  "ENCRYPTION=(TYPE='AWS_SSE_S3')"
79
79
  )
80
80
  copy_stmt_3 = CopyIntoStorage(
@@ -87,7 +87,7 @@ def test_copy_into_location(engine_testaccount, sql_compiler):
87
87
  assert (
88
88
  sql_compiler(copy_stmt_3)
89
89
  == "COPY INTO 'azure://snowflake.blob.core.windows.net/snowpile/backup' "
90
- "FROM python_tests_foods FILE_FORMAT=(TYPE=parquet SNAPPY_COMPRESSION=true) "
90
+ "FROM python_tests_foods FILE_FORMAT=(TYPE=parquet SNAPPY_COMPRESSION=true) "
91
91
  "CREDENTIALS=(AZURE_SAS_TOKEN='token')"
92
92
  )
93
93
 
@@ -95,7 +95,7 @@ def test_copy_into_location(engine_testaccount, sql_compiler):
95
95
  assert (
96
96
  sql_compiler(copy_stmt_3)
97
97
  == "COPY INTO 'azure://snowflake.blob.core.windows.net/snowpile/backup' "
98
- "FROM python_tests_foods FILE_FORMAT=(TYPE=parquet SNAPPY_COMPRESSION=true) "
98
+ "FROM python_tests_foods FILE_FORMAT=(TYPE=parquet SNAPPY_COMPRESSION=true) "
99
99
  "MAX_FILE_SIZE = 50000000 "
100
100
  "CREDENTIALS=(AZURE_SAS_TOKEN='token')"
101
101
  )
@@ -112,8 +112,8 @@ def test_copy_into_location(engine_testaccount, sql_compiler):
112
112
  )
113
113
  assert (
114
114
  sql_compiler(copy_stmt_4)
115
- == "COPY INTO python_tests_foods FROM 's3://backup' FILE_FORMAT=(TYPE=csv "
116
- "ESCAPE=None NULL_IF=('null', 'Null') RECORD_DELIMITER='|') ENCRYPTION="
115
+ == "COPY INTO python_tests_foods FROM 's3://backup' FILE_FORMAT=(TYPE=csv "
116
+ "ESCAPE=None NULL_IF=('null', 'Null') RECORD_DELIMITER='|') ENCRYPTION="
117
117
  "(KMS_KEY_ID='1234abcd-12ab-34cd-56ef-1234567890ab' TYPE='AWS_SSE_KMS')"
118
118
  )
119
119
 
@@ -126,8 +126,8 @@ def test_copy_into_location(engine_testaccount, sql_compiler):
126
126
  )
127
127
  assert (
128
128
  sql_compiler(copy_stmt_5)
129
- == "COPY INTO python_tests_foods FROM 's3://backup' FILE_FORMAT=(TYPE=csv "
130
- "FIELD_DELIMITER=',') ENCRYPTION="
129
+ == "COPY INTO python_tests_foods FROM 's3://backup' FILE_FORMAT=(TYPE=csv "
130
+ "FIELD_DELIMITER=',') ENCRYPTION="
131
131
  "(KMS_KEY_ID='1234abcd-12ab-34cd-56ef-1234567890ab' TYPE='AWS_SSE_KMS')"
132
132
  )
133
133
 
@@ -138,7 +138,7 @@ def test_copy_into_location(engine_testaccount, sql_compiler):
138
138
  )
139
139
  assert (
140
140
  sql_compiler(copy_stmt_6)
141
- == "COPY INTO @stage_name FROM python_tests_foods FILE_FORMAT=(TYPE=csv)"
141
+ == "COPY INTO @stage_name FROM python_tests_foods FILE_FORMAT=(TYPE=csv) "
142
142
  )
143
143
 
144
144
  copy_stmt_7 = CopyIntoStorage(
@@ -148,7 +148,38 @@ def test_copy_into_location(engine_testaccount, sql_compiler):
148
148
  )
149
149
  assert (
150
150
  sql_compiler(copy_stmt_7)
151
- == "COPY INTO @name.stage_name/prefix/file FROM python_tests_foods FILE_FORMAT=(TYPE=csv)"
151
+ == "COPY INTO @name.stage_name/prefix/file FROM python_tests_foods FILE_FORMAT=(TYPE=csv) "
152
+ )
153
+
154
+ copy_stmt_8 = CopyIntoStorage(
155
+ from_=food_items,
156
+ into=ExternalStage(name="stage_name"),
157
+ partition_by=text("('YEAR=' || year)"),
158
+ )
159
+ assert (
160
+ sql_compiler(copy_stmt_8)
161
+ == "COPY INTO @stage_name FROM python_tests_foods PARTITION BY ('YEAR=' || year) "
162
+ )
163
+
164
+ copy_stmt_9 = CopyIntoStorage(
165
+ from_=food_items,
166
+ into=ExternalStage(name="stage_name"),
167
+ partition_by=functions.concat(
168
+ text("'YEAR='"), text(food_items.columns["name"].name)
169
+ ),
170
+ )
171
+ assert (
172
+ sql_compiler(copy_stmt_9)
173
+ == "COPY INTO @stage_name FROM python_tests_foods PARTITION BY concat('YEAR=', name) "
174
+ )
175
+
176
+ copy_stmt_10 = CopyIntoStorage(
177
+ from_=food_items,
178
+ into=ExternalStage(name="stage_name"),
179
+ partition_by="",
180
+ )
181
+ assert (
182
+ sql_compiler(copy_stmt_10) == "COPY INTO @stage_name FROM python_tests_foods "
152
183
  )
153
184
 
154
185
  # NOTE Other than expect known compiled text, submit it to RegressionTests environment and expect them to fail, but
@@ -231,7 +262,7 @@ def test_copy_into_storage_csv_extended(sql_compiler):
231
262
  result = sql_compiler(copy_into)
232
263
  expected = (
233
264
  r"COPY INTO TEST_IMPORT "
234
- r"FROM @ML_POC.PUBLIC.AZURE_STAGE/testdata "
265
+ r"FROM @ML_POC.PUBLIC.AZURE_STAGE/testdata "
235
266
  r"FILE_FORMAT=(TYPE=csv COMPRESSION='auto' DATE_FORMAT='AUTO' "
236
267
  r"ERROR_ON_COLUMN_COUNT_MISMATCH=True ESCAPE=None "
237
268
  r"ESCAPE_UNENCLOSED_FIELD='\134' FIELD_DELIMITER=',' "
@@ -288,7 +319,7 @@ def test_copy_into_storage_parquet_named_format(sql_compiler):
288
319
  expected = (
289
320
  "COPY INTO TEST_IMPORT "
290
321
  "FROM (SELECT $1:COL1::number, $1:COL2::varchar "
291
- "FROM @ML_POC.PUBLIC.AZURE_STAGE/testdata/out.parquet) "
322
+ "FROM @ML_POC.PUBLIC.AZURE_STAGE/testdata/out.parquet) "
292
323
  "FILE_FORMAT=(format_name = parquet_file_format) force = TRUE"
293
324
  )
294
325
  assert result == expected
@@ -350,7 +381,7 @@ def test_copy_into_storage_parquet_files(sql_compiler):
350
381
  "COPY INTO TEST_IMPORT "
351
382
  "FROM (SELECT $1:COL1::number, $1:COL2::varchar "
352
383
  "FROM @ML_POC.PUBLIC.AZURE_STAGE/testdata/out.parquet "
353
- "(file_format => parquet_file_format)) FILES = ('foo.txt','bar.txt') "
384
+ "(file_format => parquet_file_format)) FILES = ('foo.txt','bar.txt') "
354
385
  "FORCE = true"
355
386
  )
356
387
  assert result == expected
@@ -412,6 +443,6 @@ def test_copy_into_storage_parquet_pattern(sql_compiler):
412
443
  "COPY INTO TEST_IMPORT "
413
444
  "FROM (SELECT $1:COL1::number, $1:COL2::varchar "
414
445
  "FROM @ML_POC.PUBLIC.AZURE_STAGE/testdata/out.parquet "
415
- "(file_format => parquet_file_format)) FORCE = true PATTERN = '.*csv'"
446
+ "(file_format => parquet_file_format)) FORCE = true PATTERN = '.*csv'"
416
447
  )
417
448
  assert result == expected
@@ -0,0 +1,64 @@
1
+ #
2
+ # Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved.
3
+ #
4
+
5
+ import importlib
6
+ import inspect
7
+
8
+ import pytest
9
+
10
+
11
+ def get_classes_from_module(module_name):
12
+ """Returns a set of class names from a given module."""
13
+ try:
14
+ module = importlib.import_module(module_name)
15
+ members = inspect.getmembers(module)
16
+ return {name for name, obj in members if inspect.isclass(obj)}
17
+
18
+ except ImportError:
19
+ print(f"Module '{module_name}' could not be imported.")
20
+ return set()
21
+
22
+
23
+ def test_types_in_snowdialect():
24
+ classes_a = get_classes_from_module(
25
+ "snowflake.sqlalchemy.parser.custom_type_parser"
26
+ )
27
+ classes_b = get_classes_from_module("snowflake.sqlalchemy.snowdialect")
28
+ assert classes_a.issubset(classes_b), str(classes_a - classes_b)
29
+
30
+
31
+ @pytest.mark.parametrize(
32
+ "type_class_name",
33
+ [
34
+ "BIGINT",
35
+ "BINARY",
36
+ "BOOLEAN",
37
+ "CHAR",
38
+ "DATE",
39
+ "DATETIME",
40
+ "DECIMAL",
41
+ "FLOAT",
42
+ "INTEGER",
43
+ "REAL",
44
+ "SMALLINT",
45
+ "TIME",
46
+ "TIMESTAMP",
47
+ "VARCHAR",
48
+ "NullType",
49
+ "_CUSTOM_DECIMAL",
50
+ "ARRAY",
51
+ "DOUBLE",
52
+ "GEOGRAPHY",
53
+ "GEOMETRY",
54
+ "MAP",
55
+ "OBJECT",
56
+ "TIMESTAMP_LTZ",
57
+ "TIMESTAMP_NTZ",
58
+ "TIMESTAMP_TZ",
59
+ "VARIANT",
60
+ ],
61
+ )
62
+ def test_snowflake_data_types_instance(type_class_name):
63
+ classes_b = get_classes_from_module("snowflake.sqlalchemy.snowdialect")
64
+ assert type_class_name in classes_b, type_class_name