Skip to content

Commit 234ea0b

Browse files
SNOW-2187773 Allow configurable precision for Decimal (#2643)
* SNOW-2187773 Allow configurable precision for Decimal * Added release notes * update ambr * fix tests * ambrs again * one more ambr * move decfloat tests to separate file * added file with Decfloat tests * review fixes
1 parent c6f19d2 commit 234ea0b

File tree

10 files changed

+1373
-31
lines changed

10 files changed

+1373
-31
lines changed

RELEASE-NOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
## Deprecations
2020

2121
## New additions
22+
* Added global option `--decimal-precision` allowing setting arbitrary precision for Python's `Decimal` type.
2223

2324
## Fixes and improvements
2425
* Bumped `snowflake-connector-python==3.17.4`

src/snowflake/cli/api/commands/decorators.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
ConnectionOption,
2828
DatabaseOption,
2929
DebugOption,
30+
DecimalPrecisionOption,
3031
DiagAllowlistPathOption,
3132
DiagLogPathOption,
3233
EnableDiagOption,
@@ -446,6 +447,12 @@ def _evaluate_param_type(
446447
annotation=Optional[bool],
447448
default=EnhancedExitCodesOption,
448449
),
450+
inspect.Parameter(
451+
"decimal_precision",
452+
inspect.Parameter.KEYWORD_ONLY,
453+
annotation=Optional[int],
454+
default=DecimalPrecisionOption,
455+
),
449456
]
450457

451458

src/snowflake/cli/api/commands/flags.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,25 @@ def _diag_log_allowlist_path_callback(path: str):
513513
envvar="SNOWFLAKE_ENHANCED_EXIT_CODES",
514514
)
515515

516+
517+
def _decimal_precision_callback(value: int | None):
518+
"""Callback to set decimal precision globally when provided."""
519+
if value is not None:
520+
from decimal import getcontext
521+
522+
getcontext().prec = value
523+
return value
524+
525+
526+
DecimalPrecisionOption = typer.Option(
527+
None,
528+
"--decimal-precision",
529+
help="Number of decimal places to display for decimal values. Uses Python's default precision if not specified.",
530+
callback=_decimal_precision_callback,
531+
rich_help_panel=_CLI_BEHAVIOUR,
532+
envvar="SNOWFLAKE_DECIMAL_PRECISION",
533+
)
534+
516535
# If IfExistsOption, IfNotExistsOption, or ReplaceOption are used with names other than those in CREATE_MODE_OPTION_NAMES,
517536
# you must also override mutually_exclusive if you want to retain the validation that at most one of these flags is
518537
# passed.

tests/__snapshots__/test_docs_generation_output.ambr

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
--debug
4949
--silent
5050
--enhanced-exit-codes
51+
--decimal-precision <decimal_precision>
5152

5253
Arguments
5354
===============================================================================
@@ -172,5 +173,8 @@
172173
:samp:`--enhanced-exit-codes`
173174
Differentiate exit error codes based on failure type. Default: False.
174175

176+
:samp:`--decimal-precision {INTEGER}`
177+
Number of decimal places to display for decimal values. Uses Python's default precision if not specified.
178+
175179
'''
176180
# ---

tests/__snapshots__/test_help_messages.ambr

Lines changed: 1152 additions & 0 deletions
Large diffs are not rendered by default.

tests/api/commands/__snapshots__/test_snow_typer.ambr

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,14 @@
123123
| failure type. |
124124
| [env var: |
125125
| SNOWFLAKE_ENHANCED_EX… |
126+
| --decimal-precision INTEGER Number of decimal |
127+
| places to display for |
128+
| decimal values. Uses |
129+
| Python's default |
130+
| precision if not |
131+
| specified. |
132+
| [env var: |
133+
| SNOWFLAKE_DECIMAL_PRE… |
126134
+------------------------------------------------------------------------------+
127135

128136

@@ -158,6 +166,14 @@
158166
| failure type. |
159167
| [env var: |
160168
| SNOWFLAKE_ENHANCED_EX… |
169+
| --decimal-precision INTEGER Number of decimal |
170+
| places to display for |
171+
| decimal values. Uses |
172+
| Python's default |
173+
| precision if not |
174+
| specified. |
175+
| [env var: |
176+
| SNOWFLAKE_DECIMAL_PRE… |
161177
+------------------------------------------------------------------------------+
162178

163179

@@ -215,6 +231,14 @@
215231
| failure type. |
216232
| [env var: |
217233
| SNOWFLAKE_ENHANCED_EX… |
234+
| --decimal-precision INTEGER Number of decimal |
235+
| places to display for |
236+
| decimal values. Uses |
237+
| Python's default |
238+
| precision if not |
239+
| specified. |
240+
| [env var: |
241+
| SNOWFLAKE_DECIMAL_PRE… |
218242
+------------------------------------------------------------------------------+
219243

220244

tests/output/__snapshots__/test_silent_output.ambr

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,14 @@
128128
| failure type. |
129129
| [env var: |
130130
| SNOWFLAKE_ENHANCED_EX… |
131+
| --decimal-precision INTEGER Number of decimal |
132+
| places to display for |
133+
| decimal values. Uses |
134+
| Python's default |
135+
| precision if not |
136+
| specified. |
137+
| [env var: |
138+
| SNOWFLAKE_DECIMAL_PRE… |
131139
+------------------------------------------------------------------------------+
132140

133141

tests/test_common_decorators.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"debug",
2626
"silent",
2727
"enhanced_exit_codes",
28+
"decimal_precision",
2829
]
2930
_KNOWN_SIG_GLOBAL_PARAMETERS_WITH_CONNECTION = [
3031
"connection",

tests_integration/sql/test_sql.py

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -347,34 +347,3 @@ def test_nested_json_backward_compatibility(runner):
347347
assert "profile" in user_obj
348348
assert "Alice" in user_obj
349349
assert "dark" in user_obj
350-
351-
352-
@pytest.mark.integration
353-
@pytest.mark.qa_only
354-
def test_decfloat_values(runner):
355-
"""Test DECFLOAT type with various value types: positive/negative, maximum/minimum, floating point."""
356-
357-
sql = """
358-
SELECT
359-
CAST('123456789012345678901234567890.123456789' AS DECFLOAT) AS positive_value,
360-
CAST('-123456789012345678901234567890.123456789' AS DECFLOAT) AS negative_value,
361-
CAST('3.14159265358979323846264338327950288419' AS DECFLOAT) AS floating_point,
362-
CAST('99999999999999999999999999999999999999e16384' AS DECFLOAT) AS maximum_value,
363-
CAST('-99999999999999999999999999999999999999e16384' AS DECFLOAT) AS minimum_value,
364-
"""
365-
366-
result = runner.invoke_with_connection_json(["sql", "-q", sql])
367-
assert result.exit_code == 0, f"Failed to select DECFLOAT values: {result.output}"
368-
369-
# Verify JSON response contains expected values
370-
assert len(result.json) == 1
371-
row = result.json[0]
372-
373-
# Assert exact values that Snowflake returns for DECFLOAT
374-
assert row["POSITIVE_VALUE"] == "1.234567890123456789012345679E+29"
375-
assert row["NEGATIVE_VALUE"] == "-1.234567890123456789012345679E+29"
376-
assert (
377-
row["FLOATING_POINT"] == "3.141592653589793238462643383"
378-
) # value is rounded up to 28 numbers
379-
assert row["MAXIMUM_VALUE"] == "1.000000000000000000000000000E+16422"
380-
assert row["MINIMUM_VALUE"] == "-1.000000000000000000000000000E+16422"
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# Copyright (c) 2024 Snowflake Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""
16+
Tests for DECFLOAT data type support and decimal precision configuration.
17+
"""
18+
19+
import os
20+
from decimal import getcontext
21+
22+
import pytest
23+
24+
pytestmark = [
25+
pytest.mark.integration,
26+
pytest.mark.qa_only,
27+
]
28+
29+
30+
@pytest.fixture
31+
def reset_decimal_precision():
32+
"""Reset decimal precision before and after each test."""
33+
original_prec = getcontext().prec
34+
yield
35+
getcontext().prec = original_prec
36+
37+
38+
def test_decfloat_values(runner, reset_decimal_precision):
39+
"""Test DECFLOAT type with various value types: positive/negative, maximum/minimum, floating point."""
40+
41+
sql = """
42+
SELECT
43+
CAST('123456789012345678901234567890.123456789' AS DECFLOAT) AS positive_value,
44+
CAST('-123456789012345678901234567890.123456789' AS DECFLOAT) AS negative_value,
45+
CAST('3.14159265358979323846264338327950288419' AS DECFLOAT) AS floating_point,
46+
CAST('99999999999999999999999999999999999999e16384' AS DECFLOAT) AS maximum_value,
47+
CAST('-99999999999999999999999999999999999999e16384' AS DECFLOAT) AS minimum_value,
48+
"""
49+
50+
result = runner.invoke_with_connection_json(["sql", "-q", sql])
51+
assert result.exit_code == 0, f"Failed to select DECFLOAT values: {result.output}"
52+
53+
# Verify JSON response contains expected values
54+
assert len(result.json) == 1
55+
row = result.json[0]
56+
57+
# Assert exact values that Snowflake returns for DECFLOAT
58+
assert row["POSITIVE_VALUE"] == "1.234567890123456789012345679E+29"
59+
assert row["NEGATIVE_VALUE"] == "-1.234567890123456789012345679E+29"
60+
assert (
61+
row["FLOATING_POINT"] == "3.141592653589793238462643383"
62+
) # value is rounded up to 28 numbers
63+
assert row["MAXIMUM_VALUE"] == "1.000000000000000000000000000E+16422"
64+
assert row["MINIMUM_VALUE"] == "-1.000000000000000000000000000E+16422"
65+
66+
67+
def test_decfloat_values_precision_38(runner, reset_decimal_precision):
68+
"""Test DECFLOAT type with precision=38 for higher precision decimal operations."""
69+
70+
sql = """
71+
SELECT
72+
CAST('123456789012345678901234567890.123456789' AS DECFLOAT) AS positive_value,
73+
CAST('-123456789012345678901234567890.123456789' AS DECFLOAT) AS negative_value,
74+
CAST('3.14159265358979323846264338327950288419' AS DECFLOAT) AS floating_point,
75+
CAST('99999999999999999999999999999999999999e16384' AS DECFLOAT) AS maximum_value,
76+
CAST('-99999999999999999999999999999999999999e16384' AS DECFLOAT) AS minimum_value
77+
"""
78+
79+
result = runner.invoke_with_connection_json(
80+
["sql", "-q", sql, "--decimal-precision", "38"]
81+
)
82+
assert (
83+
result.exit_code == 0
84+
), f"Failed to select DECFLOAT values with precision=38: {result.output}"
85+
86+
assert len(result.json) == 1
87+
row = result.json[0]
88+
89+
assert row["POSITIVE_VALUE"] == "123456789012345678901234567890.12345679"
90+
assert row["NEGATIVE_VALUE"] == "-123456789012345678901234567890.12345679"
91+
assert row["FLOATING_POINT"] == "3.1415926535897932384626433832795028842"
92+
assert row["MAXIMUM_VALUE"] == "9.9999999999999999999999999999999999999E+16421"
93+
assert row["MINIMUM_VALUE"] == "-9.9999999999999999999999999999999999999E+16421"
94+
95+
96+
def test_decimal_precision_environment_variable(runner, reset_decimal_precision):
97+
"""Test decimal precision using SNOWFLAKE_DECIMAL_PRECISION environment variable."""
98+
99+
sql = """
100+
SELECT
101+
CAST('1234.56789012345678901234567890' AS DECFLOAT) AS test_value,
102+
CAST('3.14159265358979323846' AS DECFLOAT) AS pi_value
103+
"""
104+
105+
original_env = os.environ.get("SNOWFLAKE_DECIMAL_PRECISION")
106+
os.environ["SNOWFLAKE_DECIMAL_PRECISION"] = "10"
107+
108+
try:
109+
result = runner.invoke_with_connection_json(["sql", "-q", sql])
110+
assert (
111+
result.exit_code == 0
112+
), f"Failed to execute SQL with env var precision: {result.output}"
113+
114+
assert len(result.json) == 1
115+
row = result.json[0]
116+
117+
assert row["TEST_VALUE"] == "1234.567890"
118+
assert row["PI_VALUE"] == "3.141592654"
119+
120+
finally:
121+
if original_env is not None:
122+
os.environ["SNOWFLAKE_DECIMAL_PRECISION"] = original_env
123+
else:
124+
os.environ.pop("SNOWFLAKE_DECIMAL_PRECISION", None)
125+
126+
127+
def test_decimal_precision_param_overrides_env(runner, reset_decimal_precision):
128+
"""Test that CLI parameter takes precedence over environment variable."""
129+
130+
sql = """
131+
SELECT
132+
CAST('1234.56789012345678901234567890' AS DECFLOAT) AS test_value,
133+
CAST('3.14159265358979323846' AS DECFLOAT) AS pi_value
134+
"""
135+
136+
original_env = os.environ.get("SNOWFLAKE_DECIMAL_PRECISION")
137+
os.environ["SNOWFLAKE_DECIMAL_PRECISION"] = "25"
138+
139+
try:
140+
result = runner.invoke_with_connection_json(
141+
["sql", "-q", sql, "--decimal-precision", "5"]
142+
)
143+
assert (
144+
result.exit_code == 0
145+
), f"Failed to execute SQL with param override: {result.output}"
146+
147+
assert len(result.json) == 1
148+
row = result.json[0]
149+
150+
assert row["TEST_VALUE"] == "1234.6"
151+
assert row["PI_VALUE"] == "3.1416"
152+
153+
finally:
154+
if original_env is not None:
155+
os.environ["SNOWFLAKE_DECIMAL_PRECISION"] = original_env
156+
else:
157+
os.environ.pop("SNOWFLAKE_DECIMAL_PRECISION", None)

0 commit comments

Comments
 (0)