Skip to content

Commit 3ea508b

Browse files
committed
SNOW-2910128: enable_decdloat in dialect
1 parent 7abe5f1 commit 3ea508b

File tree

5 files changed

+338
-10
lines changed

5 files changed

+338
-10
lines changed

README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,57 @@ The following `NumPy` data types are supported:
303303
- numpy.float64
304304
- numpy.datatime64
305305

306+
### DECFLOAT Data Type Support
307+
308+
Snowflake SQLAlchemy supports the `DECFLOAT` data type, which provides decimal floating-point with up to 38 significant digits. For more information, see the [Snowflake DECFLOAT documentation](https://docs.snowflake.com/en/sql-reference/data-types-numeric#decfloat).
309+
310+
```python
311+
from sqlalchemy import Column, Integer, MetaData, Table
312+
from snowflake.sqlalchemy import DECFLOAT
313+
314+
metadata = MetaData()
315+
t = Table('my_table', metadata,
316+
Column('id', Integer, primary_key=True),
317+
Column('value', DECFLOAT()),
318+
)
319+
metadata.create_all(engine)
320+
```
321+
322+
#### DECFLOAT Precision
323+
324+
The Snowflake Python connector uses Python's `decimal` module context when converting `DECFLOAT` values to Python `Decimal` objects. Python's default decimal context precision is 28 digits, which can truncate `DECFLOAT` values that use up to 38 digits.
325+
326+
To preserve full 38-digit precision, add `enable_decfloat=True` to the connection URL:
327+
328+
```python
329+
from sqlalchemy import create_engine
330+
331+
engine = create_engine(
332+
'snowflake://testuser1:0123456@abc123/testdb/public?warehouse=testwh&enable_decfloat=True'
333+
)
334+
```
335+
336+
Or using the `snowflake.sqlalchemy.URL` helper:
337+
338+
```python
339+
from snowflake.sqlalchemy import URL
340+
from sqlalchemy import create_engine
341+
342+
engine = create_engine(URL(
343+
account = 'abc123',
344+
user = 'testuser1',
345+
password = '0123456',
346+
database = 'testdb',
347+
schema = 'public',
348+
warehouse = 'testwh',
349+
enable_decfloat = True,
350+
))
351+
```
352+
353+
**Note**: `DECFLOAT` does not support special values (`inf`, `-inf`, `NaN`) unlike `FLOAT`.
354+
355+
**Why is `enable_decfloat` not enabled by default?** Enabling it sets `decimal.getcontext().prec = 38`, which modifies Python's thread-local decimal context and affects all `Decimal` operations in that thread, not just database queries. To avoid unexpected side effects on application code, the dialect emits a warning when `DECFLOAT` values are retrieved without full precision enabled, guiding users to opt-in explicitly.
356+
306357
### Cache Column Metadata
307358

308359
SQLAlchemy provides [the runtime inspection API](http://docs.sqlalchemy.org/en/latest/core/inspection.html) to get the runtime information about the various objects. One of the common use case is get all tables and their column metadata in a schema in order to construct a schema catalog. For example, [alembic](http://alembic.zzzcomputing.com/) on top of SQLAlchemy manages database schema migrations. A pseudo code flow is as follows:

src/snowflake/sqlalchemy/custom_types.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
#
22
# Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved.
33
#
4+
import decimal
5+
import warnings
46
from typing import Optional, Tuple, Union
57

68
import sqlalchemy.types as sqltypes
79
import sqlalchemy.util as util
810
from sqlalchemy.types import TypeEngine
911

12+
DECFLOAT_PRECISION = 38
13+
1014
TEXT = sqltypes.VARCHAR
1115
CHARACTER = sqltypes.CHAR
1216
DEC = sqltypes.DECIMAL
@@ -126,9 +130,47 @@ class DECFLOAT(SnowflakeType):
126130
- Cannot be stored in VARIANT, OBJECT, or ARRAY
127131
- Not supported in Iceberg or Hybrid tables
128132
- Does NOT support special values (inf, -inf, NaN) unlike FLOAT
133+
134+
Precision: The Snowflake Python connector uses Python's decimal context
135+
when converting DECFLOAT to Decimal. Default context precision is 28 digits,
136+
which truncates values. For full 38-digit precision, use the dialect parameter::
137+
138+
engine = create_engine('snowflake://...?enable_decfloat=True')
139+
140+
Or set manually::
141+
142+
import decimal
143+
decimal.getcontext().prec = 38
129144
"""
130145

131146
__visit_name__ = "DECFLOAT"
147+
_warned_precision = False
148+
149+
def result_processor(self, dialect, coltype):
150+
"""Check decimal context precision and warn if it may truncate DECFLOAT values."""
151+
# Check if dialect has enable_decfloat configured
152+
decfloat_enabled = getattr(dialect, "_enable_decfloat", False)
153+
154+
def process(value):
155+
if value is not None and not DECFLOAT._warned_precision:
156+
# Skip warning if dialect has DECFLOAT support enabled
157+
if decfloat_enabled:
158+
return value
159+
160+
current_prec = decimal.getcontext().prec
161+
if current_prec < DECFLOAT_PRECISION:
162+
warnings.warn(
163+
f"Python decimal context precision ({current_prec}) is less than "
164+
f"DECFLOAT precision ({DECFLOAT_PRECISION}). Values may be truncated. "
165+
f"Set enable_decfloat=True in connection URL or "
166+
f"decimal.getcontext().prec = {DECFLOAT_PRECISION} for full precision.",
167+
UserWarning,
168+
stacklevel=2,
169+
)
170+
DECFLOAT._warned_precision = True
171+
return value
172+
173+
return process
132174

133175

134176
class _CUSTOM_Date(SnowflakeType, sqltypes.Date):

src/snowflake/sqlalchemy/snowdialect.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#
22
# Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved.
33
#
4+
import decimal
45
import operator
56
from collections import defaultdict
67
from enum import Enum
@@ -39,6 +40,7 @@
3940
SnowflakeTypeCompiler,
4041
)
4142
from .custom_types import (
43+
DECFLOAT_PRECISION,
4244
StructuredType,
4345
_CUSTOM_Date,
4446
_CUSTOM_DateTime,
@@ -163,12 +165,14 @@ def __init__(
163165
self,
164166
force_div_is_floordiv: bool = True,
165167
isolation_level: Optional[str] = SnowflakeIsolationLevel.READ_COMMITTED.value,
168+
enable_decfloat: bool = False,
166169
**kwargs: Any,
167170
):
168171
super().__init__(isolation_level=isolation_level, **kwargs)
169172
self.force_div_is_floordiv = force_div_is_floordiv
170173
self.div_is_floordiv = force_div_is_floordiv
171174
self.name_utils = _NameUtils(self.identifier_preparer)
175+
self._enable_decfloat = enable_decfloat
172176

173177
def initialize(self, connection):
174178
super().initialize(connection)
@@ -238,6 +242,11 @@ def create_connect_args(self, url: URL):
238242
parse_url_boolean(cache_column_metadata) if cache_column_metadata else False
239243
)
240244

245+
# Handle enable_decfloat URL parameter
246+
enable_decfloat = query.pop("enable_decfloat", None)
247+
if enable_decfloat is not None:
248+
self._enable_decfloat = parse_url_boolean(enable_decfloat)
249+
241250
# URL sets the query parameter values as strings, we need to cast to expected types when necessary
242251
for name, value in query.items():
243252
opts[name] = self.parse_query_param_type(name, value)
@@ -910,6 +919,10 @@ def connect(self, *cargs, **cparams):
910919
if _ENABLE_SQLALCHEMY_AS_APPLICATION_NAME:
911920
cparams = _update_connection_application_name(**cparams)
912921

922+
# Set decimal precision for full DECFLOAT support (38 digits)
923+
if self._enable_decfloat:
924+
decimal.getcontext().prec = DECFLOAT_PRECISION
925+
913926
connection = super().connect(*cargs, **cparams)
914927
self._log_new_connection_event(connection)
915928

tests/README.rst

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,51 @@ Run the test:
4949
.. code-block:: bash
5050
5151
py.test test
52+
53+
54+
DECFLOAT Precision
55+
================================================================================
56+
57+
Snowflake's DECFLOAT type supports up to 38 significant digits. However, Python's
58+
default decimal context precision is 28 digits, which can truncate values when
59+
reading from the database.
60+
61+
To preserve full 38-digit precision, add ``enable_decfloat=True`` to the connection URL:
62+
63+
.. code-block:: python
64+
65+
from decimal import Decimal
66+
from sqlalchemy import create_engine, Column, Integer, MetaData, Table, select
67+
from snowflake.sqlalchemy import DECFLOAT
68+
69+
# Use enable_decfloat=True in the connection URL for full precision
70+
engine = create_engine(
71+
'snowflake://user:password@account/database/schema?enable_decfloat=True'
72+
)
73+
metadata = MetaData()
74+
75+
prices = Table(
76+
'prices', metadata,
77+
Column('id', Integer, primary_key=True),
78+
Column('value', DECFLOAT()),
79+
)
80+
metadata.create_all(engine)
81+
82+
with engine.connect() as conn:
83+
# Insert a value with 38 significant digits
84+
conn.execute(prices.insert().values(
85+
id=1,
86+
value=Decimal('12345678901234567890123456789.123456789')
87+
))
88+
conn.commit()
89+
90+
# Query returns full precision with enable_decfloat=True
91+
result = conn.execute(select(prices)).fetchone()
92+
print(result[1]) # 12345678901234567890123456789.123456789
93+
94+
Alternatively, you can set Python's decimal context manually:
95+
96+
.. code-block:: python
97+
98+
import decimal
99+
decimal.getcontext().prec = 38

0 commit comments

Comments
 (0)