diff --git a/.gitignore b/.gitignore index be2b6924..38390050 100644 --- a/.gitignore +++ b/.gitignore @@ -102,7 +102,6 @@ generated_version.py # Editor specific .idea/ .vscode/ -*.code-workspace # WhiteSource Scan wss-*agent.config diff --git a/DESCRIPTION.md b/DESCRIPTION.md index ec44db95..941a888c 100644 --- a/DESCRIPTION.md +++ b/DESCRIPTION.md @@ -9,6 +9,10 @@ Source code is also available at: # Unreleased Notes # Release Notes +- v1.8.0(TBD) + - Add support for `DECFLOAT` + - BREAKING CHANGE: The minimum supported version for the `snowflake-python-connector` has been increased to v3.14.1 + - v1.7.6(July 10, 2025) - Fix get_multi_indexes issue, wrong assign of returned indexes when processing multiple indexes in a table diff --git a/junit.xml b/junit.xml new file mode 100644 index 00000000..778449c6 --- /dev/null +++ b/junit.xml @@ -0,0 +1,5 @@ +tests/E2E/test_select_decfloat_type.py:39: in test_select_decfloat_native_connector + assert isinstance(ret[0], Decimal), f"Return type is {type(ret[0])} {snowflake.connector.__version__}" +E AssertionError: Return type is <class 'str'> 3.16.0 +E assert False +E + where False = isinstance('123456789.12345678', Decimal) diff --git a/pyproject.toml b/pyproject.toml index b22dc293..c89b9f8f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ dynamic = ["version"] description = "Snowflake SQLAlchemy Dialect" readme = "README.md" license = "Apache-2.0" -requires-python = ">=3.8" +requires-python = ">=3.9" authors = [ { name = "Snowflake Inc.", email = "triage-snowpark-python-api-dl@snowflake.com" }, ] @@ -25,7 +25,6 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -38,7 +37,7 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", ] -dependencies = ["SQLAlchemy>=1.4.19", "snowflake-connector-python<4.0.0"] +dependencies = ["SQLAlchemy>=1.4.19", "snowflake-connector-python>=3.14.1,<4.0.0"] [tool.hatch.version] path = "src/snowflake/sqlalchemy/version.py" @@ -79,13 +78,13 @@ path = ".venv" type = "virtual" extra-dependencies = ["SQLAlchemy>=1.4.19,<2.1.0"] features = ["development", "pandas"] -python = "3.8" +python = "3.9" installer = "uv" [tool.hatch.envs.sa14] extra-dependencies = ["SQLAlchemy>=1.4.19,<2.0.0"] features = ["development", "pandas"] -python = "3.8" +python = "3.9" [tool.hatch.envs.sa14.scripts] test-dialect = "pytest --ignore_v20_test -ra -vvv --tb=short --cov snowflake.sqlalchemy --cov-append --junitxml ./junit.xml --ignore=tests/sqlalchemy_test_suite tests/" @@ -105,7 +104,7 @@ gh-cache-sum = "python -VV | sha256sum | cut -d' ' -f1" check-import = "python -c 'import snowflake.sqlalchemy; print(snowflake.sqlalchemy.__version__)'" [[tool.hatch.envs.release.matrix]] -python = ["3.8", "3.9", "3.10", "3.11", "3.12"] +python = ["3.9", "3.10", "3.11", "3.12"] features = ["development", "pandas"] [tool.hatch.envs.release.scripts] diff --git a/snowflake-sqlalchemy.code-workspace b/snowflake-sqlalchemy.code-workspace new file mode 100644 index 00000000..e04997fd --- /dev/null +++ b/snowflake-sqlalchemy.code-workspace @@ -0,0 +1,28 @@ +{ + "folders": [ + { + "path": "." + }, + ], + "settings": { + "python.defaultInterpreterPath": ".venv/bin/python", + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, + "debugpy.debugJustMyCode": false, + "python.testing.pytestArgs": [ + "tests", + "-ra", + "-vvv", + "--tb=short", + "--junitxml=./junit.xml", + "--ignore=tests/sqlalchemy_test_suite" + ] + }, + "extensions": { + "recommendations": [ + "ms-python.python", + "ms-python.vscode-pylance", + "ms-python.debugpy", + ] + } +} diff --git a/snyk/requirements.txt b/snyk/requirements.txt index 0166d751..24484126 100644 --- a/snyk/requirements.txt +++ b/snyk/requirements.txt @@ -1,2 +1,2 @@ SQLAlchemy>=1.4.19 -snowflake-connector-python<4.0.0 +snowflake-connector-python>=3.14.1,<4.0.0 diff --git a/src/snowflake/sqlalchemy/__init__.py b/src/snowflake/sqlalchemy/__init__.py index 7d795b2a..c8cdb694 100644 --- a/src/snowflake/sqlalchemy/__init__.py +++ b/src/snowflake/sqlalchemy/__init__.py @@ -46,6 +46,7 @@ BYTEINT, CHARACTER, DEC, + DECFLOAT, DOUBLE, FIXED, GEOGRAPHY, @@ -121,6 +122,7 @@ "VARBINARY", "VARIANT", "MAP", + "DECFLOAT", ) _custom_commands = ( diff --git a/src/snowflake/sqlalchemy/base.py b/src/snowflake/sqlalchemy/base.py index d664853f..19669d8a 100644 --- a/src/snowflake/sqlalchemy/base.py +++ b/src/snowflake/sqlalchemy/base.py @@ -7,10 +7,11 @@ import re import string import warnings -from typing import List +from typing import Any, List from sqlalchemy import exc as sa_exc from sqlalchemy import inspect, sql +from sqlalchemy import types as sqltypes from sqlalchemy import util as sa_util from sqlalchemy.engine import default from sqlalchemy.orm import context @@ -1181,6 +1182,12 @@ def visit_GEOGRAPHY(self, type_, **kw): def visit_GEOMETRY(self, type_, **kw): return "GEOMETRY" + def visit_DECFLOAT(self, type_: sqltypes.DECIMAL[Any], **kw: Any) -> str: + if type_.precision is None: + return "DECFLOAT" + else: + return f"DECFLOAT({type_.precision})" + construct_arguments = [(Table, {"clusterby": None})] diff --git a/src/snowflake/sqlalchemy/custom_types.py b/src/snowflake/sqlalchemy/custom_types.py index c742b740..2cffa0be 100644 --- a/src/snowflake/sqlalchemy/custom_types.py +++ b/src/snowflake/sqlalchemy/custom_types.py @@ -115,6 +115,10 @@ class GEOMETRY(SnowflakeType): __visit_name__ = "GEOMETRY" +class DECFLOAT(SnowflakeType, sqltypes.DECIMAL): + __visit_name__ = "DECFLOAT" + + class _CUSTOM_Date(SnowflakeType, sqltypes.Date): def literal_processor(self, dialect): def process(value): diff --git a/src/snowflake/sqlalchemy/parser/custom_type_parser.py b/src/snowflake/sqlalchemy/parser/custom_type_parser.py index 09cb6ab8..592529cb 100644 --- a/src/snowflake/sqlalchemy/parser/custom_type_parser.py +++ b/src/snowflake/sqlalchemy/parser/custom_type_parser.py @@ -25,6 +25,7 @@ from ..custom_types import ( _CUSTOM_DECIMAL, ARRAY, + DECFLOAT, DOUBLE, GEOGRAPHY, GEOMETRY, @@ -47,6 +48,7 @@ "DATETIME": DATETIME, "DEC": DECIMAL, "DECIMAL": DECIMAL, + "DECFLOAT": DECFLOAT, "DOUBLE": DOUBLE, "FIXED": DECIMAL, "FLOAT": FLOAT, # Snowflake FLOAT datatype doesn't have parameters diff --git a/tests/E2E/__snapshots__/test_select_decfloat_type.ambr b/tests/E2E/__snapshots__/test_select_decfloat_type.ambr new file mode 100644 index 00000000..6176f0f1 --- /dev/null +++ b/tests/E2E/__snapshots__/test_select_decfloat_type.ambr @@ -0,0 +1,21 @@ +# serializer version: 1 +# name: test_create_table_with_decfloat + ''' + + CREATE TABLE mock_table ( + id DECFLOAT(38) + ) + + + ''' +# --- +# name: test_create_table_with_decfloat_without_precision + ''' + + CREATE TABLE mock_table ( + id DECFLOAT + ) + + + ''' +# --- diff --git a/tests/E2E/test_select_decfloat_type.py b/tests/E2E/test_select_decfloat_type.py new file mode 100644 index 00000000..fdd41922 --- /dev/null +++ b/tests/E2E/test_select_decfloat_type.py @@ -0,0 +1,29 @@ +# +# Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved. + +from decimal import Decimal + +from sqlalchemy import cast, func, literal, select + +from snowflake.sqlalchemy import DECFLOAT + + +def test_select_decfloat(engine_testaccount): + select_stmt = select(cast(literal("123.45"), DECFLOAT)) + + with engine_testaccount.connect() as connection: + result = connection.execute(select_stmt) + value = result.scalar_one() + + assert value == Decimal("123.45") + + +def test_select_decfloat_sum(engine_testaccount): + expr = cast(literal("123.45"), DECFLOAT) + cast(literal("100.55"), DECFLOAT) + select_stmt = select(func.sum(expr)) + + with engine_testaccount.connect() as connection: + result = connection.execute(select_stmt) + value = result.scalar_one() + + assert value == Decimal("224.00") diff --git a/tests/__snapshots__/test_core.ambr b/tests/__snapshots__/test_core.ambr index 7a4e0f99..ac41833e 100644 --- a/tests/__snapshots__/test_core.ambr +++ b/tests/__snapshots__/test_core.ambr @@ -2,3 +2,23 @@ # name: test_compile_table_with_cluster_by_with_expression 'CREATE TABLE clustered_user (\t"Id" INTEGER NOT NULL AUTOINCREMENT, \tname VARCHAR, \tPRIMARY KEY ("Id")) CLUSTER BY ("Id", name, "Id" > 5)' # --- +# name: test_create_table_with_decfloat + ''' + + CREATE TABLE mock_table ( + id DECFLOAT(38) + ) + + + ''' +# --- +# name: test_create_table_with_decfloat_without_precision + ''' + + CREATE TABLE mock_table ( + id DECFLOAT + ) + + + ''' +# --- diff --git a/tests/test_core.py b/tests/test_core.py index a25342ac..a26dd53d 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -44,7 +44,7 @@ import snowflake.connector.errors import snowflake.sqlalchemy.snowdialect from snowflake.connector import Error, ProgrammingError, connect -from snowflake.sqlalchemy import URL, MergeInto, dialect +from snowflake.sqlalchemy import DECFLOAT, URL, MergeInto, dialect, snowdialect from snowflake.sqlalchemy._constants import ( APPLICATION_NAME, SNOWFLAKE_SQLALCHEMY_VERSION, @@ -1806,6 +1806,40 @@ def test_normalize_and_denormalize_empty_string_column_name(engine_testaccount): ) +def test_create_table_with_decfloat_without_precision(snapshot): + metadata = MetaData() + + mock_table = Table( + "mock_table", + metadata, + Column("id", DECFLOAT), + ) + + compiled_sql = str(CreateTable(mock_table).compile(dialect=snowdialect.dialect())) + + column = mock_table.c.id + assert isinstance(column.type, DECFLOAT) + assert column.type.precision is None + assert compiled_sql == snapshot + + +def test_create_table_with_decfloat(snapshot): + metadata = MetaData() + + mock_table = Table( + "mock_table", + metadata, + Column("id", DECFLOAT(38)), + ) + + compiled_sql = str(CreateTable(mock_table).compile(dialect=snowdialect.dialect())) + + column = mock_table.c.id + assert isinstance(column.type, DECFLOAT) + assert column.type.precision == 38 + assert compiled_sql == snapshot + + def test_snowflake_sqlalchemy_as_valid_client_type(): engine = create_engine( URL(**CONNECTION_PARAMETERS), diff --git a/tests/test_unit_structured_types.py b/tests/test_unit_structured_types.py index 472ce2e6..cf2c2dc3 100644 --- a/tests/test_unit_structured_types.py +++ b/tests/test_unit_structured_types.py @@ -33,6 +33,8 @@ def test_extract_parameters(): ("DATETIME(3)", "DATETIME"), ("DECIMAL(10, 2)", "DECIMAL(10, 2)"), ("DEC(10, 2)", "DECIMAL(10, 2)"), + ("DECFLOAT(38)", "DECFLOAT(38)"), + ("DECFLOAT", "DECFLOAT"), ("DOUBLE", "FLOAT"), ("FLOAT", "FLOAT"), ("FIXED(10, 2)", "DECIMAL(10, 2)"),