Skip to content

Commit 1eff019

Browse files
ai: apply changes for #1006 (2 review threads)
Addresses: - #4418612993 at src/databricks/sql/parameters/native.py:773 - #4418612995 at src/databricks/sql/parameters/native.py:663 Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>
1 parent 25634d4 commit 1eff019

3 files changed

Lines changed: 45 additions & 8 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Release History
22

33
# Unreleased
4-
- **Behaviour change:** `DecimalParameter` now binds `DECIMAL(precision, scale)`, matching SQL: `precision` is the total number of digits and `scale` the number of fractional digits (earlier docs described them the other way round). Calls written for the old convention (`scale > precision`, e.g. `scale=10, precision=2`) still bind `DECIMAL(10,2)` but emit a `FutureWarning`; swap your arguments, as this will raise `ValueError` in a future release. A declared `scale` equal to or less than `precision` is now interpreted with the SQL meaning, and a declared `scale` smaller than the value's own scale is widened (growing the precision by the same amount) so fractional digits are not lost (e.g. `DecimalParameter(Decimal("123.4567"), precision=10, scale=2)` binds `DECIMAL(12,4)`). The declared integer room (`precision - scale`) is not widened, so a value with more integer digits is still rejected by the server as out of range. Widening is capped at the 38-digit DECIMAL maximum; a value needing more digits than that still has its excess fractional digits rounded. The constructor now also raises `ValueError` for inputs it previously accepted: a `precision` outside 1..38 (including `precision=0`, e.g. `scale=0, precision=0` or `scale=5, precision=0`, which used to fall back to an inferred type), a negative `scale`, and `bool` values for `scale` or `precision`.
4+
- **Behaviour change:** `DecimalParameter` now binds `DECIMAL(precision, scale)`, matching SQL: `precision` is the total number of digits and `scale` the number of fractional digits (earlier docs described them the other way round). Calls written for the old convention (`scale > precision`, e.g. `scale=10, precision=2`) still bind `DECIMAL(10,2)` but emit a `FutureWarning`; swap your arguments, as this will raise `ValueError` in a future release. A declared `scale` equal to or less than `precision` is now interpreted with the SQL meaning, and a declared `scale` smaller than the value's own scale is widened (growing the precision by the same amount) so fractional digits are not lost (e.g. `DecimalParameter(Decimal("123.4567"), precision=10, scale=2)` binds `DECIMAL(12,4)`). The declared integer room (`precision - scale`) is not widened, so a value with more integer digits is still rejected by the server as out of range. Widening is capped at the 38-digit DECIMAL maximum; a value needing more digits than that still has its excess fractional digits rounded. The constructor now also raises `ValueError` for inputs it previously accepted: a `precision` outside 0..38, a negative `scale`, and `bool` values for `scale` or `precision`. `precision=0` (e.g. `scale=5, precision=0`) still falls back to an inferred type as before, but now emits a `FutureWarning`; it will raise `ValueError` in a future release.
55
- Preserve the originating session when re-fetching or concurrently polling async statements on the kernel backend.
66

77
# 4.8.0 (2026-10-24)

‎src/databricks/sql/parameters/native.py‎

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,24 @@ def __init__(
648648
"as integers"
649649
)
650650

651+
if self.precision == 0:
652+
# Earlier releases treated precision=0 as falsy, i.e. "unset", and inferred
653+
# the type from the value. Keep that behaviour for now (swapping e.g.
654+
# scale=5, precision=0 into DECIMAL(5,0) would silently round the value),
655+
# but warn so callers migrate before this becomes an error, like the
656+
# other legacy-convention calls below.
657+
warnings.warn(
658+
"DecimalParameter received precision=0 (scale={}); ignoring scale and "
659+
"precision and inferring the DECIMAL type from the value, as earlier "
660+
"releases did. `precision` must be between 1 and {}; pass both as None "
661+
"to infer the type. This will raise ValueError in a future "
662+
"release.".format(self.scale, self.MAX_PRECISION),
663+
FutureWarning,
664+
stacklevel=2,
665+
)
666+
self.scale = None
667+
self.precision = None
668+
651669
if self.scale is not None and self.precision is not None:
652670
# Range-check the raw arguments *before* the legacy swap below, so a call
653671
# that is simply invalid (rather than written for the legacy convention)
@@ -657,9 +675,6 @@ def __init__(
657675
raise ValueError(
658676
"DecimalParameter scale must be >= 0, got {}".format(self.scale)
659677
)
660-
# precision=0 is rejected here too (not swapped): earlier releases
661-
# treated it as unset and inferred the type, so swapping e.g.
662-
# scale=5, precision=0 into DECIMAL(5,0) would silently round the value.
663678
if not 1 <= self.precision <= self.MAX_PRECISION:
664679
raise ValueError(
665680
"DecimalParameter precision must be between 1 and {}, got {}".format(
@@ -770,6 +785,16 @@ def _effective_precision_and_scale(self, precision: int, scale: int):
770785
# NaN / Infinity: leave the declared type as-is
771786
return precision, scale
772787

788+
# Trailing fractional zeros carry no information, so ignore them: otherwise
789+
# Decimal("1.50") and Decimal("1.5") would bind different types for the same
790+
# declared parameter. (Stripping keeps len(digits) + exponent unchanged.)
791+
while exponent < 0 and len(digits) > 1 and digits[-1] == 0:
792+
digits = digits[:-1]
793+
exponent += 1
794+
if digits == (0,):
795+
# Zero (e.g. Decimal("0.000")) has no significant fractional digits
796+
exponent = max(exponent, 0)
797+
773798
value_scale = max(-exponent, 0)
774799
declared_int_digits = precision - scale
775800
# Integer room is never widened past the declared one: an overflowing value

‎tests/unit/test_parameters.py‎

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,11 @@ def test_calculate_decimal_cast_string(value, expected):
115115
(Decimal("123"), 0, 10, "DECIMAL(10,0)"),
116116
# Value scale exceeds declared scale: scale widens, declared int room (8) is kept
117117
(Decimal("123.4567"), 2, 10, "DECIMAL(12,4)"),
118+
# Trailing fractional zeros are not significant and do not widen the scale
119+
(Decimal("123.4500"), 2, 10, "DECIMAL(10,2)"),
120+
(Decimal("123.4560"), 2, 10, "DECIMAL(11,3)"),
121+
(Decimal("0.000"), 2, 10, "DECIMAL(10,2)"),
122+
(Decimal("100"), 0, 10, "DECIMAL(10,0)"),
118123
# Value has more integer digits than declared precision - scale allows:
119124
# integer room is not widened, so the server rejects it as out of range
120125
(Decimal("123456789012.5"), 2, 10, "DECIMAL(10,2)"),
@@ -211,7 +216,6 @@ def test_decimal_correct_order_with_more_fractional_digits_is_widened():
211216
"scale,precision,match",
212217
(
213218
(2, 50, "precision must be between 1 and 38"),
214-
(0, 0, "precision must be between 1 and 38"),
215219
(-1, 10, "scale must be >= 0"),
216220
),
217221
)
@@ -235,9 +239,6 @@ def test_decimal_bool_scale_or_precision_raises(scale, precision):
235239
(
236240
(50, 2, "scale must be between 0 and 38, got 50"),
237241
(5, -1, "precision must be between 1 and 38, got -1"),
238-
# precision=0 used to mean "unset" (type inferred); it must not be swapped
239-
# into DECIMAL(5,0), which would round the value.
240-
(5, 0, "precision must be between 1 and 38, got 0"),
241242
),
242243
)
243244
def test_decimal_out_of_range_raw_args_raise_before_legacy_swap(scale, precision, match):
@@ -249,6 +250,17 @@ def test_decimal_out_of_range_raw_args_raise_before_legacy_swap(scale, precision
249250
DecimalParameter(Decimal("1.5"), scale=scale, precision=precision)
250251

251252

253+
@pytest.mark.parametrize("scale", (0, 5, 10))
254+
def test_decimal_precision_zero_falls_back_to_inferred_type_with_warning(scale):
255+
"""precision=0 used to mean "unset" (type inferred). Keep that, with a
256+
FutureWarning, rather than raising or swapping into DECIMAL(scale,0), which
257+
would round the value."""
258+
with pytest.warns(FutureWarning, match="precision=0"):
259+
p = DecimalParameter(Decimal("1234.56"), scale=scale, precision=0)
260+
assert p.scale is None and p.precision is None
261+
assert p._cast_expr() == "DECIMAL(6,2)"
262+
263+
252264
class Primitive(Enum):
253265
"""These are the inferrable types. This Enum is used for parametrized tests."""
254266

0 commit comments

Comments
 (0)