Skip to content

Commit 9036107

Browse files
ai: apply changes for #1006 (1 review thread)
Addresses: - #4418637929 at src/databricks/sql/parameters/native.py:740 Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>
1 parent 1eff019 commit 9036107

3 files changed

Lines changed: 86 additions & 19 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 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.
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. `scale=0` (previously also treated as unset) is now honoured as `DECIMAL(precision, 0)`, except that when the value has more integer digits than `precision` (which earlier releases accepted via the inferred type) it falls back to the inferred type with a `FutureWarning`; this 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: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -608,7 +608,8 @@ def __init__(
608608
the value has more fractional digits than `scale`, the bound scale (and so the
609609
precision) is widened to fit them (capped at precision 38) so they are not lost;
610610
the declared integer room (`precision - scale`) is kept, so a value with more
611-
integer digits than that is still rejected by the server as out of range.
611+
integer digits than that is still rejected by the server as out of range
612+
(except, for backward compatibility, with `scale=0`: see the CHANGELOG).
612613
Round the `Decimal` yourself (e.g. with `Decimal.quantize`) before binding if
613614
you need it narrowed to the declared scale.
614615
@@ -688,6 +689,31 @@ def __init__(
688689
)
689690
)
690691

692+
if self.scale == 0 and self.precision is not None:
693+
# Earlier releases also treated scale=0 as falsy, i.e. "unset", and
694+
# inferred the type from the value. Honouring DECIMAL(precision, 0) is
695+
# only a regression when the value has more integer digits than
696+
# `precision` (the server would now reject a call that used to work), so
697+
# keep the inferred type for exactly that case, with a warning.
698+
normalized = self._normalized_value_digits()
699+
if normalized is not None:
700+
digits, exponent = normalized
701+
if max(len(digits) + exponent, 0) > self.precision:
702+
warnings.warn(
703+
"DecimalParameter received scale=0, precision={} for a value "
704+
"with more than {} integer digits; ignoring scale and "
705+
"precision and inferring the DECIMAL type from the value, as "
706+
"earlier releases did. Declare a precision large enough for "
707+
"the value, or pass both as None to infer the type. This "
708+
"will raise ValueError in a future release.".format(
709+
self.precision, self.precision
710+
),
711+
FutureWarning,
712+
stacklevel=2,
713+
)
714+
self.scale = None
715+
self.precision = None
716+
691717
if (
692718
self.scale is not None
693719
and self.precision is not None
@@ -761,29 +787,21 @@ def _cast_expr(self):
761787

762788
MAX_PRECISION = 38
763789

764-
def _effective_precision_and_scale(self, precision: int, scale: int):
765-
"""Widen a declared DECIMAL(precision, scale) so the value's fractional digits
766-
fit: the value's own scale is authoritative over an under-declared target
767-
scale. The declared integer room (``precision - scale``) is never widened, so
768-
a value with more integer digits is still rejected by the server as out of
769-
range, as in earlier releases.
770-
771-
Exception: the total is capped at MAX_PRECISION (38) digits. If the value
772-
needs more than that (e.g. a scale over 38), the cast is clamped and the
773-
server rounds away the fractional digits that do not fit.
774-
"""
790+
def _normalized_value_digits(self):
791+
"""Return ``(digits, exponent)`` of the value with insignificant trailing
792+
fractional zeros stripped, or None if the value is not a finite number."""
775793
value = self.value
776794
if not isinstance(value, decimal.Decimal):
777795
# The value is sent to the server as str(value), so inspect that same
778796
# textual form; this makes int / float / str values widen like Decimals.
779797
try:
780798
value = decimal.Decimal(str(value))
781799
except (decimal.InvalidOperation, ValueError, TypeError):
782-
return precision, scale
800+
return None
783801
sign, digits, exponent = value.as_tuple()
784802
if not isinstance(exponent, int):
785-
# NaN / Infinity: leave the declared type as-is
786-
return precision, scale
803+
# NaN / Infinity
804+
return None
787805

788806
# Trailing fractional zeros carry no information, so ignore them: otherwise
789807
# Decimal("1.50") and Decimal("1.5") would bind different types for the same
@@ -794,6 +812,23 @@ def _effective_precision_and_scale(self, precision: int, scale: int):
794812
if digits == (0,):
795813
# Zero (e.g. Decimal("0.000")) has no significant fractional digits
796814
exponent = max(exponent, 0)
815+
return digits, exponent
816+
817+
def _effective_precision_and_scale(self, precision: int, scale: int):
818+
"""Widen a declared DECIMAL(precision, scale) so the value's fractional digits
819+
fit: the value's own scale is authoritative over an under-declared target
820+
scale. The declared integer room (``precision - scale``) is never widened, so
821+
a value with more integer digits is still rejected by the server as out of
822+
range, as in earlier releases.
823+
824+
Exception: the total is capped at MAX_PRECISION (38) digits. If the value
825+
needs more than that (e.g. a scale over 38), the cast is clamped and the
826+
server rounds away the fractional digits that do not fit.
827+
"""
828+
normalized = self._normalized_value_digits()
829+
if normalized is None:
830+
return precision, scale
831+
digits, exponent = normalized
797832

798833
value_scale = max(-exponent, 0)
799834
declared_int_digits = precision - scale

‎tests/unit/test_parameters.py‎

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,11 @@ def test_calculate_decimal_cast_string(value, expected):
123123
# Value has more integer digits than declared precision - scale allows:
124124
# integer room is not widened, so the server rejects it as out of range
125125
(Decimal("123456789012.5"), 2, 10, "DECIMAL(10,2)"),
126-
# Positive exponent: 1E+3 needs 4 integer digits; declared room (2) is kept
127-
(Decimal("1E+3"), 0, 2, "DECIMAL(2,0)"),
126+
# Positive exponent: 1E+3 needs 4 integer digits, which DECIMAL(5,0) fits
128127
(Decimal("1E+3"), 0, 5, "DECIMAL(5,0)"),
128+
(Decimal("1E+3"), 0, 4, "DECIMAL(4,0)"),
129+
# scale=0 with a fractional value that fits the integer room: honoured, widened
130+
(Decimal("123.45"), 0, 3, "DECIMAL(5,2)"),
129131
# Widened precision is capped at 38
130132
(Decimal("0.123456789012345678901234567890123456"), 2, 10, "DECIMAL(38,36)"),
131133
# Widened scale is capped at the (capped) precision
@@ -144,7 +146,7 @@ def test_calculate_decimal_cast_string(value, expected):
144146
# Non-Decimal values are inspected via str(value), which is what is sent
145147
(1.5, 2, 10, "DECIMAL(10,2)"),
146148
(12.3456, 2, 10, "DECIMAL(12,4)"),
147-
(123456789012, 0, 10, "DECIMAL(10,0)"),
149+
(1234567890, 0, 10, "DECIMAL(10,0)"),
148150
("123.4567", 2, 10, "DECIMAL(12,4)"),
149151
# Unparseable values fall back to the declared type
150152
("not-a-number", 2, 10, "DECIMAL(10,2)"),
@@ -261,6 +263,36 @@ def test_decimal_precision_zero_falls_back_to_inferred_type_with_warning(scale):
261263
assert p._cast_expr() == "DECIMAL(6,2)"
262264

263265

266+
@pytest.mark.parametrize(
267+
"value,precision,expected",
268+
(
269+
(Decimal("123.45"), 2, "DECIMAL(5,2)"),
270+
(Decimal("1E+3"), 2, "DECIMAL(4,0)"),
271+
(123456789012, 10, "DECIMAL(12,0)"),
272+
),
273+
)
274+
def test_decimal_scale_zero_integer_overflow_falls_back_to_inferred_type(
275+
value, precision, expected
276+
):
277+
"""scale=0 used to mean "unset" (type inferred). When honouring DECIMAL(precision,0)
278+
would overflow the value's integer digits (a call that used to succeed), keep the
279+
inferred type, with a FutureWarning."""
280+
with pytest.warns(FutureWarning, match="scale=0"):
281+
p = DecimalParameter(value, scale=0, precision=precision)
282+
assert p.scale is None and p.precision is None
283+
assert p._cast_expr() == expected
284+
285+
286+
@pytest.mark.parametrize(
287+
"value,precision", ((Decimal("123.45"), 3), (Decimal("0.5"), 1), (Decimal("NaN"), 1))
288+
)
289+
def test_decimal_scale_zero_that_fits_is_honoured_without_warning(value, precision):
290+
with warnings.catch_warnings():
291+
warnings.simplefilter("error", FutureWarning)
292+
p = DecimalParameter(value, scale=0, precision=precision)
293+
assert (p.scale, p.precision) == (0, precision)
294+
295+
264296
class Primitive(Enum):
265297
"""These are the inferrable types. This Enum is used for parametrized tests."""
266298

0 commit comments

Comments
 (0)