snowflake-sqlalchemy 1.5.3__tar.gz → 1.6.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 (70) hide show
  1. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/.pre-commit-config.yaml +7 -0
  2. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/DESCRIPTION.md +15 -2
  3. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/PKG-INFO +23 -12
  4. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/README.md +21 -10
  5. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/ci/build.sh +7 -5
  6. snowflake_sqlalchemy-1.6.1/ci/test_linux.sh +25 -0
  7. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/pyproject.toml +18 -3
  8. snowflake_sqlalchemy-1.6.1/snyk/requirements.txt +2 -0
  9. snowflake_sqlalchemy-1.6.1/snyk/requiremtnts.txt +2 -0
  10. snowflake_sqlalchemy-1.6.1/snyk/update_requirements.py +17 -0
  11. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/src/snowflake/sqlalchemy/base.py +26 -23
  12. snowflake_sqlalchemy-1.6.1/src/snowflake/sqlalchemy/compat.py +36 -0
  13. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/src/snowflake/sqlalchemy/custom_commands.py +2 -1
  14. snowflake_sqlalchemy-1.6.1/src/snowflake/sqlalchemy/functions.py +16 -0
  15. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/src/snowflake/sqlalchemy/requirements.py +16 -0
  16. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/src/snowflake/sqlalchemy/snowdialect.py +37 -30
  17. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/src/snowflake/sqlalchemy/util.py +10 -2
  18. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/src/snowflake/sqlalchemy/version.py +1 -1
  19. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/tests/conftest.py +9 -23
  20. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/tests/sqlalchemy_test_suite/conftest.py +7 -0
  21. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/tests/sqlalchemy_test_suite/test_suite.py +4 -0
  22. snowflake_sqlalchemy-1.6.1/tests/sqlalchemy_test_suite/test_suite_20.py +205 -0
  23. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/tests/test_compiler.py +1 -1
  24. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/tests/test_core.py +32 -53
  25. snowflake_sqlalchemy-1.6.1/tests/test_custom_functions.py +25 -0
  26. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/tests/test_orm.py +22 -20
  27. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/tests/test_pandas.py +6 -5
  28. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/tests/test_qmark.py +2 -2
  29. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/tests/test_sequence.py +26 -0
  30. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/tox.ini +4 -8
  31. snowflake_sqlalchemy-1.5.3/ci/test_linux.sh +0 -25
  32. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/.gitignore +0 -0
  33. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/.gitmodules +0 -0
  34. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/LICENSE.txt +0 -0
  35. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/MANIFEST.in +0 -0
  36. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/ci/build_docker.sh +0 -0
  37. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/ci/docker/sqlalchemy_build/Dockerfile +0 -0
  38. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/ci/docker/sqlalchemy_build/scripts/entrypoint.sh +0 -0
  39. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/ci/set_base_image.sh +0 -0
  40. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/ci/test.sh +0 -0
  41. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/ci/test_docker.sh +0 -0
  42. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/license_header.txt +0 -0
  43. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/setup.cfg +0 -0
  44. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/src/snowflake/sqlalchemy/__init__.py +0 -0
  45. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/src/snowflake/sqlalchemy/_constants.py +0 -0
  46. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/src/snowflake/sqlalchemy/custom_types.py +0 -0
  47. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/src/snowflake/sqlalchemy/provision.py +0 -0
  48. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/tested_requirements/requirements_310.reqs +0 -0
  49. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/tested_requirements/requirements_37.reqs +0 -0
  50. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/tested_requirements/requirements_38.reqs +0 -0
  51. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/tested_requirements/requirements_39.reqs +0 -0
  52. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/tests/README.rst +0 -0
  53. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/tests/__init__.py +0 -0
  54. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/tests/data/users.txt +0 -0
  55. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/tests/sqlalchemy_test_suite/README.md +0 -0
  56. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/tests/sqlalchemy_test_suite/__init__.py +0 -0
  57. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/tests/test_copy.py +0 -0
  58. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/tests/test_create.py +0 -0
  59. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/tests/test_custom_types.py +0 -0
  60. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/tests/test_geography.py +0 -0
  61. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/tests/test_geometry.py +0 -0
  62. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/tests/test_multivalues_insert.py +0 -0
  63. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/tests/test_quote.py +0 -0
  64. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/tests/test_semi_structured_datatypes.py +0 -0
  65. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/tests/test_timestamp.py +0 -0
  66. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/tests/test_unit_core.py +0 -0
  67. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/tests/test_unit_cte.py +0 -0
  68. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/tests/test_unit_types.py +0 -0
  69. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/tests/test_unit_url.py +0 -0
  70. {snowflake_sqlalchemy-1.5.3 → snowflake_sqlalchemy-1.6.1}/tests/util.py +0 -0
@@ -44,3 +44,10 @@ repos:
44
44
  - id: flake8
45
45
  additional_dependencies:
46
46
  - flake8-bugbear
47
+ - repo: local
48
+ hooks:
49
+ - id: requirements-update
50
+ name: "Update dependencies from pyproject.toml to snyk/requirements.txt"
51
+ language: system
52
+ entry: python snyk/update_requirements.py
53
+ files: ^pyproject.toml$
@@ -9,9 +9,22 @@ Source code is also available at:
9
9
 
10
10
  # Release Notes
11
11
 
12
- - v1.5.3(Unrelased)
12
+ - v1.6.1(July 9, 2024)
13
13
 
14
- - Limit SQLAlchemy to < 2.0.0 before releasing version compatible with 2.0
14
+ - Update internal project workflow with pypi publishing
15
+
16
+ - v1.6.0(July 8, 2024)
17
+
18
+ - support for installing with SQLAlchemy 2.0.x
19
+ - use `hatch` & `uv` for managing project virtual environments
20
+
21
+ - v1.5.4
22
+
23
+ - Add ability to set ORDER / NOORDER sequence on columns with IDENTITY
24
+
25
+ - v1.5.3(April 16, 2024)
26
+
27
+ - Limit SQLAlchemy to < 2.0.0 before releasing version compatible with 2.0
15
28
 
16
29
  - v1.5.2(April 11, 2024)
17
30
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: snowflake-sqlalchemy
3
- Version: 1.5.3
3
+ Version: 1.6.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
@@ -36,7 +36,7 @@ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
36
36
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
37
37
  Requires-Python: >=3.8
38
38
  Requires-Dist: snowflake-connector-python<4.0.0
39
- Requires-Dist: sqlalchemy<2.0.0,>=1.4.19
39
+ Requires-Dist: sqlalchemy>=1.4.19
40
40
  Provides-Extra: development
41
41
  Requires-Dist: mock; extra == 'development'
42
42
  Requires-Dist: numpy; extra == 'development'
@@ -153,6 +153,7 @@ containing special characters need to be URL encoded to be parsed correctly. Thi
153
153
  characters could lead to authentication failure.
154
154
 
155
155
  The encoding for the password can be generated using `urllib.parse`:
156
+
156
157
  ```python
157
158
  import urllib.parse
158
159
  urllib.parse.quote("kx@% jj5/g")
@@ -163,6 +164,7 @@ urllib.parse.quote("kx@% jj5/g")
163
164
 
164
165
  To create an engine with the proper encodings, either manually constructing the url string by formatting
165
166
  or taking advantage of the `snowflake.sqlalchemy.URL` helper method:
167
+
166
168
  ```python
167
169
  import urllib.parse
168
170
  from snowflake.sqlalchemy import URL
@@ -243,14 +245,23 @@ engine = create_engine(...)
243
245
  engine.execute(<SQL>)
244
246
  engine.dispose()
245
247
 
246
- # Do this.
248
+ # Better.
247
249
  engine = create_engine(...)
248
250
  connection = engine.connect()
249
251
  try:
250
- connection.execute(<SQL>)
252
+ connection.execute(text(<SQL>))
251
253
  finally:
252
254
  connection.close()
253
255
  engine.dispose()
256
+
257
+ # Best
258
+ try:
259
+ with engine.connext() as connection:
260
+ connection.execute(text(<SQL>))
261
+ # or
262
+ connection.exec_driver_sql(<SQL>)
263
+ finally:
264
+ engine.dispose()
254
265
  ```
255
266
 
256
267
  ### Auto-increment Behavior
@@ -294,14 +305,14 @@ engine = create_engine(URL(
294
305
 
295
306
  specific_date = np.datetime64('2016-03-04T12:03:05.123456789Z')
296
307
 
297
- connection = engine.connect()
298
- connection.execute(
299
- "CREATE OR REPLACE TABLE ts_tbl(c1 TIMESTAMP_NTZ)")
300
- connection.execute(
301
- "INSERT INTO ts_tbl(c1) values(%s)", (specific_date,)
302
- )
303
- df = pd.read_sql_query("SELECT * FROM ts_tbl", engine)
304
- assert df.c1.values[0] == specific_date
308
+ with engine.connect() as connection:
309
+ connection.exec_driver_sql(
310
+ "CREATE OR REPLACE TABLE ts_tbl(c1 TIMESTAMP_NTZ)")
311
+ connection.exec_driver_sql(
312
+ "INSERT INTO ts_tbl(c1) values(%s)", (specific_date,)
313
+ )
314
+ df = pd.read_sql_query("SELECT * FROM ts_tbl", connection)
315
+ assert df.c1.values[0] == specific_date
305
316
  ```
306
317
 
307
318
  The following `NumPy` data types are supported:
@@ -101,6 +101,7 @@ containing special characters need to be URL encoded to be parsed correctly. Thi
101
101
  characters could lead to authentication failure.
102
102
 
103
103
  The encoding for the password can be generated using `urllib.parse`:
104
+
104
105
  ```python
105
106
  import urllib.parse
106
107
  urllib.parse.quote("kx@% jj5/g")
@@ -111,6 +112,7 @@ urllib.parse.quote("kx@% jj5/g")
111
112
 
112
113
  To create an engine with the proper encodings, either manually constructing the url string by formatting
113
114
  or taking advantage of the `snowflake.sqlalchemy.URL` helper method:
115
+
114
116
  ```python
115
117
  import urllib.parse
116
118
  from snowflake.sqlalchemy import URL
@@ -191,14 +193,23 @@ engine = create_engine(...)
191
193
  engine.execute(<SQL>)
192
194
  engine.dispose()
193
195
 
194
- # Do this.
196
+ # Better.
195
197
  engine = create_engine(...)
196
198
  connection = engine.connect()
197
199
  try:
198
- connection.execute(<SQL>)
200
+ connection.execute(text(<SQL>))
199
201
  finally:
200
202
  connection.close()
201
203
  engine.dispose()
204
+
205
+ # Best
206
+ try:
207
+ with engine.connext() as connection:
208
+ connection.execute(text(<SQL>))
209
+ # or
210
+ connection.exec_driver_sql(<SQL>)
211
+ finally:
212
+ engine.dispose()
202
213
  ```
203
214
 
204
215
  ### Auto-increment Behavior
@@ -242,14 +253,14 @@ engine = create_engine(URL(
242
253
 
243
254
  specific_date = np.datetime64('2016-03-04T12:03:05.123456789Z')
244
255
 
245
- connection = engine.connect()
246
- connection.execute(
247
- "CREATE OR REPLACE TABLE ts_tbl(c1 TIMESTAMP_NTZ)")
248
- connection.execute(
249
- "INSERT INTO ts_tbl(c1) values(%s)", (specific_date,)
250
- )
251
- df = pd.read_sql_query("SELECT * FROM ts_tbl", engine)
252
- assert df.c1.values[0] == specific_date
256
+ with engine.connect() as connection:
257
+ connection.exec_driver_sql(
258
+ "CREATE OR REPLACE TABLE ts_tbl(c1 TIMESTAMP_NTZ)")
259
+ connection.exec_driver_sql(
260
+ "INSERT INTO ts_tbl(c1) values(%s)", (specific_date,)
261
+ )
262
+ df = pd.read_sql_query("SELECT * FROM ts_tbl", connection)
263
+ assert df.c1.values[0] == specific_date
253
264
  ```
254
265
 
255
266
  The following `NumPy` data types are supported:
@@ -3,7 +3,7 @@
3
3
  # Build snowflake-sqlalchemy
4
4
  set -o pipefail
5
5
 
6
- PYTHON="python3.7"
6
+ PYTHON="python3.8"
7
7
  THIS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
8
  SQLALCHEMY_DIR="$(dirname "${THIS_DIR}")"
9
9
  DIST_DIR="${SQLALCHEMY_DIR}/dist"
@@ -11,14 +11,16 @@ DIST_DIR="${SQLALCHEMY_DIR}/dist"
11
11
  cd "$SQLALCHEMY_DIR"
12
12
  # Clean up previously built DIST_DIR
13
13
  if [ -d "${DIST_DIR}" ]; then
14
- echo "[WARN] ${DIST_DIR} already existing, deleting it..."
15
- rm -rf "${DIST_DIR}"
14
+ echo "[WARN] ${DIST_DIR} already existing, deleting it..."
15
+ rm -rf "${DIST_DIR}"
16
16
  fi
17
17
 
18
18
  # Constants and setup
19
+ export PATH=$PATH:$HOME/.local/bin
19
20
 
20
21
  echo "[Info] Building snowflake-sqlalchemy with $PYTHON"
21
22
  # Clean up possible build artifacts
22
23
  rm -rf build generated_version.py
23
- ${PYTHON} -m pip install --upgrade pip setuptools wheel build
24
- ${PYTHON} -m build --outdir ${DIST_DIR} .
24
+ export UV_NO_CACHE=true
25
+ ${PYTHON} -m pip install uv hatch
26
+ ${PYTHON} -m hatch build
@@ -0,0 +1,25 @@
1
+ #!/bin/bash -e
2
+ #
3
+ # Test Snowflake SQLAlchemy in Linux
4
+ # NOTES:
5
+ # - Versions to be tested should be passed in as the first argument, e.g: "3.7 3.8". If omitted 3.7-3.11 will be assumed.
6
+ # - This script assumes that ../dist/repaired_wheels has the wheel(s) built for all versions to be tested
7
+ # - This is the script that test_docker.sh runs inside of the docker container
8
+
9
+ PYTHON_VERSIONS="${1:-3.8 3.9 3.10 3.11}"
10
+ THIS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
+ SQLALCHEMY_DIR="$(dirname "${THIS_DIR}")"
12
+
13
+ # Install one copy of tox
14
+ python3 -m pip install -U tox
15
+
16
+ # Run tests
17
+ cd $SQLALCHEMY_DIR
18
+ for PYTHON_VERSION in ${PYTHON_VERSIONS}; do
19
+ echo "[Info] Testing with ${PYTHON_VERSION}"
20
+ SHORT_VERSION=$(python3 -c "print('${PYTHON_VERSION}'.replace('.', ''))")
21
+ SQLALCHEMY_WHL=$(ls $SQLALCHEMY_DIR/dist/snowflake_sqlalchemy-*-py3-none-any.whl | sort -r | head -n 1)
22
+ TEST_ENVLIST=fix_lint,py${SHORT_VERSION}-ci,py${SHORT_VERSION}-coverage
23
+ echo "[Info] Running tox for ${TEST_ENVLIST}"
24
+ python3 -m tox -e ${TEST_ENVLIST} --installpkg ${SQLALCHEMY_WHL}
25
+ done
@@ -38,7 +38,7 @@ classifiers = [
38
38
  "Topic :: Software Development :: Libraries :: Application Frameworks",
39
39
  "Topic :: Software Development :: Libraries :: Python Modules",
40
40
  ]
41
- dependencies = ["snowflake-connector-python<4.0.0", "SQLAlchemy>=1.4.19,<2.0.0"]
41
+ dependencies = ["SQLAlchemy>=1.4.19", "snowflake-connector-python<4.0.0"]
42
42
 
43
43
  [tool.hatch.version]
44
44
  path = "src/snowflake/sqlalchemy/version.py"
@@ -73,6 +73,13 @@ exclude = ["/.github"]
73
73
  packages = ["src/snowflake"]
74
74
 
75
75
  [tool.hatch.envs.default]
76
+ extra-dependencies = ["SQLAlchemy>=1.4.19,<2.1.0"]
77
+ features = ["development", "pandas"]
78
+ python = "3.8"
79
+ installer = "uv"
80
+
81
+ [tool.hatch.envs.sa14]
82
+ extra-dependencies = ["SQLAlchemy>=1.4.19,<2.0.0"]
76
83
  features = ["development", "pandas"]
77
84
  python = "3.8"
78
85
 
@@ -82,10 +89,18 @@ SQLACHEMY_WARN_20 = "1"
82
89
 
83
90
  [tool.hatch.envs.default.scripts]
84
91
  check = "pre-commit run --all-files"
85
- test-dialect = "pytest -ra -vvv --tb=short --cov snowflake.sqlalchemy --cov-append --junitxml ./junit.xml --ignore=tests/sqlalchemy_test_suite"
92
+ test-dialect = "pytest -ra -vvv --tb=short --cov snowflake.sqlalchemy --cov-append --junitxml ./junit.xml --ignore=tests/sqlalchemy_test_suite tests/"
86
93
  test-dialect-compatibility = "pytest -ra -vvv --tb=short --cov snowflake.sqlalchemy --cov-append --junitxml ./junit.xml tests/sqlalchemy_test_suite"
87
- test-run_v20 = "pytest -ra -vvv --tb=short --cov snowflake.sqlalchemy --cov-append --junitxml ./junit.xml --ignore=tests/sqlalchemy_test_suite --run_v20_sqlalchemy"
88
94
  gh-cache-sum = "python -VV | sha256sum | cut -d' ' -f1"
95
+ check-import = "python -c 'import snowflake.sqlalchemy; print(snowflake.sqlalchemy.__version__)'"
96
+
97
+ [[tool.hatch.envs.release.matrix]]
98
+ python = ["3.8", "3.9", "3.10", "3.11", "3.12"]
99
+ features = ["development", "pandas"]
100
+
101
+ [tool.hatch.envs.release.scripts]
102
+ test-dialect = "pytest -ra -vvv --tb=short --ignore=tests/sqlalchemy_test_suite tests/"
103
+ test-compatibility = "pytest -ra -vvv --tb=short tests/sqlalchemy_test_suite tests/"
89
104
 
90
105
  [tool.ruff]
91
106
  line-length = 88
@@ -0,0 +1,2 @@
1
+ SQLAlchemy>=1.4.19
2
+ snowflake-connector-python<4.0.0
@@ -0,0 +1,2 @@
1
+ snowflake-connector-python<4.0.0
2
+ SQLAlchemy>=1.4.19,<2.1.0
@@ -0,0 +1,17 @@
1
+ from pathlib import Path
2
+
3
+ import tomlkit
4
+
5
+
6
+ def sync():
7
+ pyproject = tomlkit.loads(Path("pyproject.toml").read_text())
8
+ snyk_reqiurements = Path("snyk/requirements.txt")
9
+ dependencies = pyproject.get("project", {}).get("dependencies", [])
10
+
11
+ with snyk_reqiurements.open("w") as fh:
12
+ fh.write("\n".join(dependencies))
13
+ fh.write("\n")
14
+
15
+
16
+ if __name__ == "__main__":
17
+ sync()
@@ -13,13 +13,14 @@ from sqlalchemy.engine import default
13
13
  from sqlalchemy.orm import context
14
14
  from sqlalchemy.orm.context import _MapperEntity
15
15
  from sqlalchemy.schema import Sequence, Table
16
- from sqlalchemy.sql import compiler, expression
16
+ from sqlalchemy.sql import compiler, expression, functions
17
17
  from sqlalchemy.sql.base import CompileState
18
18
  from sqlalchemy.sql.elements import quoted_name
19
19
  from sqlalchemy.sql.selectable import Lateral, SelectState
20
- from sqlalchemy.util.compat import string_types
21
20
 
21
+ from .compat import IS_VERSION_20, args_reducer, string_types
22
22
  from .custom_commands import AWSBucket, AzureContainer, ExternalStage
23
+ from .functions import flatten
23
24
  from .util import (
24
25
  _find_left_clause_to_join_from,
25
26
  _set_connection_interpolate_empty_sequences,
@@ -324,17 +325,9 @@ class SnowflakeORMSelectCompileState(context.ORMSelectCompileState):
324
325
 
325
326
  return left, replace_from_obj_index, use_entity_index
326
327
 
328
+ @args_reducer(positions_to_drop=(6, 7))
327
329
  def _join_left_to_right(
328
- self,
329
- entities_collection,
330
- left,
331
- right,
332
- onclause,
333
- prop,
334
- create_aliases,
335
- aliased_generation,
336
- outerjoin,
337
- full,
330
+ self, entities_collection, left, right, onclause, prop, outerjoin, full
338
331
  ):
339
332
  """given raw "left", "right", "onclause" parameters consumed from
340
333
  a particular key within _join(), add a real ORMJoin object to
@@ -364,7 +357,7 @@ class SnowflakeORMSelectCompileState(context.ORMSelectCompileState):
364
357
  use_entity_index,
365
358
  ) = self._join_place_explicit_left_side(entities_collection, left)
366
359
 
367
- if left is right and not create_aliases:
360
+ if left is right:
368
361
  raise sa_exc.InvalidRequestError(
369
362
  "Can't construct a join from %s to %s, they "
370
363
  "are the same entity" % (left, right)
@@ -373,9 +366,15 @@ class SnowflakeORMSelectCompileState(context.ORMSelectCompileState):
373
366
  # the right side as given often needs to be adapted. additionally
374
367
  # a lot of things can be wrong with it. handle all that and
375
368
  # get back the new effective "right" side
376
- r_info, right, onclause = self._join_check_and_adapt_right_side(
377
- left, right, onclause, prop, create_aliases, aliased_generation
378
- )
369
+
370
+ if IS_VERSION_20:
371
+ r_info, right, onclause = self._join_check_and_adapt_right_side(
372
+ left, right, onclause, prop
373
+ )
374
+ else:
375
+ r_info, right, onclause = self._join_check_and_adapt_right_side(
376
+ left, right, onclause, prop, False, False
377
+ )
379
378
 
380
379
  if not r_info.is_selectable:
381
380
  extra_criteria = self._get_extra_criteria(r_info)
@@ -966,34 +965,36 @@ class SnowflakeDDLCompiler(compiler.DDLCompiler):
966
965
  )
967
966
 
968
967
  def visit_identity_column(self, identity, **kw):
969
- text = " IDENTITY"
968
+ text = "IDENTITY"
970
969
  if identity.start is not None or identity.increment is not None:
971
970
  start = 1 if identity.start is None else identity.start
972
971
  increment = 1 if identity.increment is None else identity.increment
973
972
  text += f"({start},{increment})"
973
+ if identity.order is not None:
974
+ order = "ORDER" if identity.order else "NOORDER"
975
+ text += f" {order}"
974
976
  return text
975
977
 
976
978
  def get_identity_options(self, identity_options):
977
979
  text = []
978
980
  if identity_options.increment is not None:
979
- text.append(f"INCREMENT BY {identity_options.increment:d}")
981
+ text.append("INCREMENT BY %d" % identity_options.increment)
980
982
  if identity_options.start is not None:
981
- text.append(f"START WITH {identity_options.start:d}")
983
+ text.append("START WITH %d" % identity_options.start)
982
984
  if identity_options.minvalue is not None:
983
- text.append(f"MINVALUE {identity_options.minvalue:d}")
985
+ text.append("MINVALUE %d" % identity_options.minvalue)
984
986
  if identity_options.maxvalue is not None:
985
- text.append(f"MAXVALUE {identity_options.maxvalue:d}")
987
+ text.append("MAXVALUE %d" % identity_options.maxvalue)
986
988
  if identity_options.nominvalue is not None:
987
989
  text.append("NO MINVALUE")
988
990
  if identity_options.nomaxvalue is not None:
989
991
  text.append("NO MAXVALUE")
990
992
  if identity_options.cache is not None:
991
- text.append(f"CACHE {identity_options.cache:d}")
993
+ text.append("CACHE %d" % identity_options.cache)
992
994
  if identity_options.cycle is not None:
993
995
  text.append("CYCLE" if identity_options.cycle else "NO CYCLE")
994
996
  if identity_options.order is not None:
995
997
  text.append("ORDER" if identity_options.order else "NOORDER")
996
-
997
998
  return " ".join(text)
998
999
 
999
1000
 
@@ -1063,3 +1064,5 @@ class SnowflakeTypeCompiler(compiler.GenericTypeCompiler):
1063
1064
 
1064
1065
 
1065
1066
  construct_arguments = [(Table, {"clusterby": None})]
1067
+
1068
+ functions.register_function("flatten", flatten)
@@ -0,0 +1,36 @@
1
+ #
2
+ # Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved.
3
+ from __future__ import annotations
4
+
5
+ import functools
6
+ from typing import Callable
7
+
8
+ from sqlalchemy import __version__ as SA_VERSION
9
+ from sqlalchemy import util
10
+
11
+ string_types = (str,)
12
+ returns_unicode = util.symbol("RETURNS_UNICODE")
13
+
14
+ IS_VERSION_20 = tuple(int(v) for v in SA_VERSION.split(".")) >= (2, 0, 0)
15
+
16
+
17
+ def args_reducer(positions_to_drop: tuple):
18
+ """Removes args at positions provided in tuple positions_to_drop.
19
+
20
+ For example tuple (3, 5) will remove items at third and fifth position.
21
+ Keep in mind that on class methods first postion is cls or self.
22
+ """
23
+
24
+ def fn_wrapper(fn: Callable):
25
+ @functools.wraps(fn)
26
+ def wrapper(*args):
27
+ reduced_args = args
28
+ if not IS_VERSION_20:
29
+ reduced_args = tuple(
30
+ arg for idx, arg in enumerate(args) if idx not in positions_to_drop
31
+ )
32
+ fn(*reduced_args)
33
+
34
+ return wrapper
35
+
36
+ return fn_wrapper
@@ -10,7 +10,8 @@ from sqlalchemy.sql.ddl import DDLElement
10
10
  from sqlalchemy.sql.dml import UpdateBase
11
11
  from sqlalchemy.sql.elements import ClauseElement
12
12
  from sqlalchemy.sql.roles import FromClauseRole
13
- from sqlalchemy.util.compat import string_types
13
+
14
+ from .compat import string_types
14
15
 
15
16
  NoneType = type(None)
16
17
 
@@ -0,0 +1,16 @@
1
+ #
2
+ # Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved.
3
+
4
+ import warnings
5
+
6
+ from sqlalchemy.sql import functions as sqlfunc
7
+
8
+ FLATTEN_WARNING = "For backward compatibility params are not rendered."
9
+
10
+
11
+ class flatten(sqlfunc.GenericFunction):
12
+ name = "flatten"
13
+
14
+ def __init__(self, *args, **kwargs):
15
+ warnings.warn(FLATTEN_WARNING, DeprecationWarning, stacklevel=2)
16
+ super().__init__(*args, **kwargs)
@@ -289,9 +289,25 @@ class Requirements(SuiteRequirements):
289
289
  # Check https://snowflakecomputing.atlassian.net/browse/SNOW-640134 for details on breaking changes discussion.
290
290
  return exclusions.closed()
291
291
 
292
+ @property
293
+ def date_implicit_bound(self):
294
+ # Supporting this would require behavior breaking change to implicitly convert str to timestamp when binding
295
+ # parameters in string forms of timestamp values.
296
+ return exclusions.closed()
297
+
298
+ @property
299
+ def time_implicit_bound(self):
300
+ # Supporting this would require behavior breaking change to implicitly convert str to timestamp when binding
301
+ # parameters in string forms of timestamp values.
302
+ return exclusions.closed()
303
+
292
304
  @property
293
305
  def timestamp_microseconds_implicit_bound(self):
294
306
  # Supporting this would require behavior breaking change to implicitly convert str to timestamp when binding
295
307
  # parameters in string forms of timestamp values.
296
308
  # Check https://snowflakecomputing.atlassian.net/browse/SNOW-640134 for details on breaking changes discussion.
297
309
  return exclusions.closed()
310
+
311
+ @property
312
+ def array_type(self):
313
+ return exclusions.closed()
@@ -5,6 +5,7 @@
5
5
  import operator
6
6
  from collections import defaultdict
7
7
  from functools import reduce
8
+ from typing import Any
8
9
  from urllib.parse import unquote_plus
9
10
 
10
11
  import sqlalchemy.types as sqltypes
@@ -15,7 +16,6 @@ from sqlalchemy.engine import URL, default, reflection
15
16
  from sqlalchemy.schema import Table
16
17
  from sqlalchemy.sql import text
17
18
  from sqlalchemy.sql.elements import quoted_name
18
- from sqlalchemy.sql.sqltypes import String
19
19
  from sqlalchemy.types import (
20
20
  BIGINT,
21
21
  BINARY,
@@ -40,6 +40,7 @@ from sqlalchemy.types import (
40
40
  from snowflake.connector import errors as sf_errors
41
41
  from snowflake.connector.connection import DEFAULT_CONFIGURATION
42
42
  from snowflake.connector.constants import UTF8
43
+ from snowflake.sqlalchemy.compat import returns_unicode
43
44
 
44
45
  from .base import (
45
46
  SnowflakeCompiler,
@@ -63,7 +64,11 @@ from .custom_types import (
63
64
  _CUSTOM_Float,
64
65
  _CUSTOM_Time,
65
66
  )
66
- from .util import _update_connection_application_name, parse_url_boolean
67
+ from .util import (
68
+ _update_connection_application_name,
69
+ parse_url_boolean,
70
+ parse_url_integer,
71
+ )
67
72
 
68
73
  colspecs = {
69
74
  Date: _CUSTOM_Date,
@@ -134,7 +139,7 @@ class SnowflakeDialect(default.DefaultDialect):
134
139
  # unicode strings
135
140
  supports_unicode_statements = True
136
141
  supports_unicode_binds = True
137
- returns_unicode_strings = String.RETURNS_UNICODE
142
+ returns_unicode_strings = returns_unicode
138
143
  description_encoding = None
139
144
 
140
145
  # No lastrowid support. See SNOW-11155
@@ -195,10 +200,34 @@ class SnowflakeDialect(default.DefaultDialect):
195
200
 
196
201
  @classmethod
197
202
  def dbapi(cls):
203
+ return cls.import_dbapi()
204
+
205
+ @classmethod
206
+ def import_dbapi(cls):
198
207
  from snowflake import connector
199
208
 
200
209
  return connector
201
210
 
211
+ @staticmethod
212
+ def parse_query_param_type(name: str, value: Any) -> Any:
213
+ """Cast param value if possible to type defined in connector-python."""
214
+ if not (maybe_type_configuration := DEFAULT_CONFIGURATION.get(name)):
215
+ return value
216
+
217
+ _, expected_type = maybe_type_configuration
218
+ if not isinstance(expected_type, tuple):
219
+ expected_type = (expected_type,)
220
+
221
+ if isinstance(value, expected_type):
222
+ return value
223
+
224
+ elif bool in expected_type:
225
+ return parse_url_boolean(value)
226
+ elif int in expected_type:
227
+ return parse_url_integer(value)
228
+ else:
229
+ return value
230
+
202
231
  def create_connect_args(self, url: URL):
203
232
  opts = url.translate_connect_args(username="user")
204
233
  if "database" in opts:
@@ -235,47 +264,25 @@ class SnowflakeDialect(default.DefaultDialect):
235
264
 
236
265
  # URL sets the query parameter values as strings, we need to cast to expected types when necessary
237
266
  for name, value in query.items():
238
- maybe_type_configuration = DEFAULT_CONFIGURATION.get(name)
239
- if (
240
- not maybe_type_configuration
241
- ): # if the parameter is not found in the type mapping, pass it through as a string
242
- opts[name] = value
243
- continue
244
-
245
- (_, expected_type) = maybe_type_configuration
246
- if not isinstance(expected_type, tuple):
247
- expected_type = (expected_type,)
248
-
249
- if isinstance(
250
- value, expected_type
251
- ): # if the expected type is str, pass it through as a string
252
- opts[name] = value
253
-
254
- elif (
255
- bool in expected_type
256
- ): # if the expected type is bool, parse it and pass as a boolean
257
- opts[name] = parse_url_boolean(value)
258
- else:
259
- # TODO: other types like int are stil passed through as string
260
- # https://github.com/snowflakedb/snowflake-sqlalchemy/issues/447
261
- opts[name] = value
267
+ opts[name] = self.parse_query_param_type(name, value)
262
268
 
263
269
  return ([], opts)
264
270
 
265
- def has_table(self, connection, table_name, schema=None):
271
+ @reflection.cache
272
+ def has_table(self, connection, table_name, schema=None, **kw):
266
273
  """
267
274
  Checks if the table exists
268
275
  """
269
276
  return self._has_object(connection, "TABLE", table_name, schema)
270
277
 
271
- def has_sequence(self, connection, sequence_name, schema=None):
278
+ @reflection.cache
279
+ def has_sequence(self, connection, sequence_name, schema=None, **kw):
272
280
  """
273
281
  Checks if the sequence exists
274
282
  """
275
283
  return self._has_object(connection, "SEQUENCE", sequence_name, schema)
276
284
 
277
285
  def _has_object(self, connection, object_type, object_name, schema=None):
278
-
279
286
  full_name = self._denormalize_quote_join(schema, object_name)
280
287
  try:
281
288
  results = connection.execute(