Skip to content

Commit e0ade9c

Browse files
authored
SNOW-638841: ints returned as Decimals when upgrading from version 1.3.4 to 1.4.0 (#335)
1 parent 50a8b4e commit e0ade9c

File tree

5 files changed

+149
-26
lines changed

5 files changed

+149
-26
lines changed

DESCRIPTION.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ Source code is also available at:
1010
# Release Notes
1111

1212
- v1.4.1(Unreleased)
13+
1314
- snowflake-sqlalchemy is now SQLAlchemy 2.0 compatible.
1415
- Fixed a bug that `DATE` should not be removed from `SnowflakeDialect.ischema_names`.
16+
- Fixed a breaking change introduced in release 1.4.0 that changed the behavior of processing numeric values returned from service.
1517

1618
- v1.4.0(July 20, 2022)
1719

src/snowflake/sqlalchemy/custom_types.py

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
# Copyright (c) 2012-2022 Snowflake Computing Inc. All rights reserved.
33
#
44
import datetime
5-
import decimal
65
import re
76

87
import sqlalchemy.types as sqltypes
@@ -144,18 +143,3 @@ class _CUSTOM_DECIMAL(SnowflakeType, sqltypes.DECIMAL):
144143
@util.memoized_property
145144
def _type_affinity(self):
146145
return sqltypes.INTEGER if self.scale == 0 else sqltypes.DECIMAL
147-
148-
149-
class _CUSTOM_Numeric(SnowflakeType, sqltypes.Numeric):
150-
def result_processor(self, dialect, coltype):
151-
if self.asdecimal:
152-
153-
def process(value):
154-
if value:
155-
return decimal.Decimal(value)
156-
else:
157-
return None
158-
159-
return process
160-
else:
161-
return _process_float

src/snowflake/sqlalchemy/requirements.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,3 +274,10 @@ def json_type(self):
274274
def implements_get_lastrowid(self):
275275
# TODO: need connector lastrowid support, check SNOW-11155
276276
return exclusions.closed()
277+
278+
@property
279+
def implicit_decimal_binds(self):
280+
# Supporting this would require behavior breaking change to implicitly convert str to Decimal when binding
281+
# parameters in string forms of decimal values.
282+
# Check https://snowflakecomputing.atlassian.net/browse/SNOW-640134 for details on breaking changes discussion.
283+
return exclusions.closed()

src/snowflake/sqlalchemy/snowdialect.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
Date,
3535
DateTime,
3636
Float,
37-
Numeric,
3837
Time,
3938
)
4039

@@ -60,7 +59,6 @@
6059
_CUSTOM_Date,
6160
_CUSTOM_DateTime,
6261
_CUSTOM_Float,
63-
_CUSTOM_Numeric,
6462
_CUSTOM_Time,
6563
)
6664
from .util import _sort_columns_by_sequences
@@ -70,7 +68,6 @@
7068
DateTime: _CUSTOM_DateTime,
7169
Time: _CUSTOM_Time,
7270
Float: _CUSTOM_Float,
73-
Numeric: _CUSTOM_Numeric,
7471
}
7572

7673
ischema_names = {

tests/test_core.py

Lines changed: 140 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
#
22
# Copyright (c) 2012-2022 Snowflake Computing Inc. All rights reserved.
33
#
4-
4+
import decimal
5+
import json
56
import os
67
import random
78
import re
89
import string
910
import time
11+
from datetime import date, datetime
1012
from unittest.mock import patch
1113

1214
import pytest
15+
import pytz
1316
from sqlalchemy import (
1417
REAL,
1518
Boolean,
@@ -44,6 +47,9 @@
4447

4548
THIS_DIR = os.path.dirname(os.path.realpath(__file__))
4649

50+
PST_TZ = "America/Los_Angeles"
51+
JST_TZ = "Asia/Tokyo"
52+
4753

4854
def _create_users_addresses_tables(
4955
engine_testaccount, metadata, fk=None, pk=None, uq=None
@@ -1609,16 +1615,14 @@ def test_empty_comments(engine_testaccount):
16091615
def test_column_type_schema(engine_testaccount):
16101616
with engine_testaccount.connect() as conn:
16111617
table_name = random_string(5)
1618+
# column type FIXED not supported, triggers SQL compilation error: Unsupported data type 'FIXED'.
16121619
conn.exec_driver_sql(
16131620
f"""\
16141621
CREATE TEMP TABLE {table_name} (
16151622
C1 BIGINT, C2 BINARY, C3 BOOLEAN, C4 CHAR, C5 CHARACTER, C6 DATE, C7 DATETIME, C8 DEC,
1616-
C9 DECIMAL, C10 DOUBLE,
1617-
-- C11 FIXED, # SQL compilation error: Unsupported data type 'FIXED'.
1618-
C12 FLOAT, C13 INT, C14 INTEGER, C15 NUMBER, C16 REAL, C17 BYTEINT, C18 SMALLINT,
1619-
C19 STRING, C20 TEXT, C21 TIME, C22 TIMESTAMP, C23 TIMESTAMP_TZ, C24 TIMESTAMP_LTZ,
1620-
C25 TIMESTAMP_NTZ, C26 TINYINT, C27 VARBINARY, C28 VARCHAR, C29 VARIANT,
1621-
C30 OBJECT, C31 ARRAY, C32 GEOGRAPHY
1623+
C9 DECIMAL, C10 DOUBLE, C11 FLOAT, C12 INT, C13 INTEGER, C14 NUMBER, C15 REAL, C16 BYTEINT,
1624+
C17 SMALLINT, C18 STRING, C19 TEXT, C20 TIME, C21 TIMESTAMP, C22 TIMESTAMP_TZ, C23 TIMESTAMP_LTZ,
1625+
C24 TIMESTAMP_NTZ, C25 TINYINT, C26 VARBINARY, C27 VARCHAR, C28 VARIANT, C29 OBJECT, C30 ARRAY, C31 GEOGRAPHY
16221626
)
16231627
"""
16241628
)
@@ -1630,3 +1634,132 @@ def test_column_type_schema(engine_testaccount):
16301634
assert (
16311635
len(columns) == len(ischema_names_baseline) - 1
16321636
) # -1 because FIXED is not supported
1637+
1638+
1639+
def test_result_type_and_value(engine_testaccount):
1640+
with engine_testaccount.connect() as conn:
1641+
table_name = random_string(5)
1642+
conn.exec_driver_sql(
1643+
f"""\
1644+
CREATE TEMP TABLE {table_name} (
1645+
C1 BIGINT, C2 BINARY, C3 BOOLEAN, C4 CHAR, C5 CHARACTER, C6 DATE, C7 DATETIME, C8 DEC(12,3),
1646+
C9 DECIMAL(12,3), C10 DOUBLE, C11 FLOAT, C12 INT, C13 INTEGER, C14 NUMBER, C15 REAL, C16 BYTEINT,
1647+
C17 SMALLINT, C18 STRING, C19 TEXT, C20 TIME, C21 TIMESTAMP, C22 TIMESTAMP_TZ, C23 TIMESTAMP_LTZ,
1648+
C24 TIMESTAMP_NTZ, C25 TINYINT, C26 VARBINARY, C27 VARCHAR, C28 VARIANT, C29 OBJECT, C30 ARRAY, C31 GEOGRAPHY
1649+
)
1650+
"""
1651+
)
1652+
table_reflected = Table(
1653+
table_name, MetaData(), autoload=True, autoload_with=conn
1654+
)
1655+
current_date = date.today()
1656+
current_utctime = datetime.utcnow()
1657+
current_localtime = pytz.utc.localize(current_utctime, is_dst=False).astimezone(
1658+
pytz.timezone(PST_TZ)
1659+
)
1660+
current_localtime_without_tz = datetime.now()
1661+
current_localtime_with_other_tz = pytz.utc.localize(
1662+
current_localtime_without_tz, is_dst=False
1663+
).astimezone(pytz.timezone(JST_TZ))
1664+
TIME_VALUE = current_utctime.time()
1665+
DECIMAL_VALUE = decimal.Decimal("123456789.123")
1666+
MAX_INT_VALUE = 99999999999999999999999999999999999999
1667+
MIN_INT_VALUE = -99999999999999999999999999999999999999
1668+
FLOAT_VALUE = 123456789.123
1669+
STRING_VALUE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
1670+
BINARY_VALUE = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
1671+
CHAR_VALUE = "A"
1672+
GEOGRAPHY_VALUE = "POINT(-122.35 37.55)"
1673+
GEOGRAPHY_RESULT_VALUE = '{"coordinates": [-122.35,37.55],"type": "Point"}'
1674+
1675+
ins = table_reflected.insert().values(
1676+
c1=MAX_INT_VALUE, # BIGINT
1677+
c2=BINARY_VALUE, # BINARY
1678+
c3=True, # BOOLEAN
1679+
c4=CHAR_VALUE, # CHAR
1680+
c5=CHAR_VALUE, # CHARACTER
1681+
c6=current_date, # DATE
1682+
c7=current_localtime_without_tz, # DATETIME
1683+
c8=DECIMAL_VALUE, # DEC(12,3)
1684+
c9=DECIMAL_VALUE, # DECIMAL(12,3)
1685+
c10=FLOAT_VALUE, # DOUBLE
1686+
c11=FLOAT_VALUE, # FLOAT
1687+
c12=MIN_INT_VALUE, # INT
1688+
c13=MAX_INT_VALUE, # INTEGER
1689+
c14=MIN_INT_VALUE, # NUMBER
1690+
c15=FLOAT_VALUE, # REAL
1691+
c16=MAX_INT_VALUE, # BYTEINT
1692+
c17=MIN_INT_VALUE, # SMALLINT
1693+
c18=STRING_VALUE, # STRING
1694+
c19=STRING_VALUE, # TEXT
1695+
c20=TIME_VALUE, # TIME
1696+
c21=current_utctime, # TIMESTAMP
1697+
c22=current_localtime_with_other_tz, # TIMESTAMP_TZ
1698+
c23=current_localtime, # TIMESTAMP_LTZ
1699+
c24=current_utctime, # TIMESTAMP_NTZ
1700+
c25=MAX_INT_VALUE, # TINYINT
1701+
c26=BINARY_VALUE, # VARBINARY
1702+
c27=STRING_VALUE, # VARCHAR
1703+
c28=None, # VARIANT, currently snowflake-sqlalchemy/connector does not support binding variant
1704+
c29=None, # OBJECT, currently snowflake-sqlalchemy/connector does not support binding variant
1705+
c30=None, # ARRAY, currently snowflake-sqlalchemy/connector does not support binding variant
1706+
c31=GEOGRAPHY_VALUE, # GEOGRAPHY
1707+
)
1708+
conn.execute(ins)
1709+
1710+
results = conn.execute(select(table_reflected)).fetchall()
1711+
assert len(results) == 1
1712+
result = results[0]
1713+
assert (
1714+
result[0] == MAX_INT_VALUE
1715+
and result[1] == BINARY_VALUE
1716+
and result[2] is True
1717+
and result[3] == CHAR_VALUE
1718+
and result[4] == CHAR_VALUE
1719+
and result[5] == current_date
1720+
and result[6] == current_localtime_without_tz
1721+
and result[7] == DECIMAL_VALUE
1722+
and result[8] == DECIMAL_VALUE
1723+
and result[9] == FLOAT_VALUE
1724+
and result[10] == FLOAT_VALUE
1725+
and result[11] == MIN_INT_VALUE
1726+
and result[12] == MAX_INT_VALUE
1727+
and result[13] == MIN_INT_VALUE
1728+
and result[14] == FLOAT_VALUE
1729+
and result[15] == MAX_INT_VALUE
1730+
and result[16] == MIN_INT_VALUE
1731+
and result[17] == STRING_VALUE
1732+
and result[18] == STRING_VALUE
1733+
and result[19] == TIME_VALUE
1734+
and result[20] == current_utctime
1735+
and result[21] == current_localtime_with_other_tz
1736+
and result[22] == current_localtime
1737+
and result[23] == current_utctime
1738+
and result[24] == MAX_INT_VALUE
1739+
and result[25] == BINARY_VALUE
1740+
and result[26] == STRING_VALUE
1741+
and result[27] is None
1742+
and result[28] is None
1743+
and result[29] is None
1744+
and json.loads(result[30]) == json.loads(GEOGRAPHY_RESULT_VALUE)
1745+
)
1746+
1747+
sql = f"""
1748+
INSERT INTO {table_name}(c28, c29, c30)
1749+
SELECT PARSE_JSON('{{"vk1":100, "vk2":200, "vk3":300}}'),
1750+
OBJECT_CONSTRUCT('vk1', 100, 'vk2', 200, 'vk3', 300),
1751+
PARSE_JSON('[
1752+
{{"k":1, "v":"str1"}},
1753+
{{"k":2, "v":"str2"}},
1754+
{{"k":3, "v":"str3"}}]'
1755+
)"""
1756+
conn.exec_driver_sql(sql)
1757+
results = conn.execute(select(table_reflected)).fetchall()
1758+
assert len(results) == 2
1759+
data = json.loads(results[-1][27])
1760+
assert json.loads(results[-1][28]) == data
1761+
assert data["vk1"] == 100
1762+
assert data["vk3"] == 300
1763+
assert data is not None
1764+
data = json.loads(results[-1][29])
1765+
assert data[1]["k"] == 2

0 commit comments

Comments
 (0)