Skip to content

Commit 2823eed

Browse files
ai: apply changes for #1006 (3 review threads)
Addresses: - #4418440090 at src/databricks/sql/parameters/native.py:715 - #4418440099 at src/databricks/sql/parameters/native.py:702 - #4418440106 at src/databricks/sql/parameters/native.py:704 Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>
1 parent 864acfa commit 2823eed

3 files changed

Lines changed: 53 additions & 9 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 so fractional digits are not lost (up to the 38-digit DECIMAL maximum; a value needing more digits than that still has its excess fractional digits rounded).
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 the bound type is widened to fit the value: a declared `scale` smaller than the value's own scale is widened so fractional digits are not lost, and a declared `precision` whose integer room (`precision - scale`) is smaller than the value's integer digits is also widened, so such values are no longer rejected by the server as out of range (e.g. `DecimalParameter(Decimal("123456789012.5"), precision=10, scale=2)` now binds `DECIMAL(14,2)` instead of failing). If you relied on `precision` as a server-side range check, validate the value client-side. Widening is capped at the 38-digit DECIMAL maximum; a value needing more digits than that still has its excess fractional digits rounded.
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: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,18 @@ def __init__(
664664
)
665665
self.scale, self.precision = self.precision, self.scale
666666

667+
if self.scale is not None and self.precision is not None:
668+
if self.scale < 0:
669+
raise ValueError(
670+
"DecimalParameter scale must be >= 0, got {}".format(self.scale)
671+
)
672+
if not 1 <= self.precision <= self.MAX_PRECISION:
673+
raise ValueError(
674+
"DecimalParameter precision must be between 1 and {}, got {}".format(
675+
self.MAX_PRECISION, self.precision
676+
)
677+
)
678+
667679
def valid_scale_and_precision(self):
668680
if (self.scale is None and self.precision is None) or (
669681
isinstance(self.scale, int) and isinstance(self.precision, int)
@@ -692,18 +704,25 @@ def _cast_expr(self):
692704
MAX_PRECISION = 38
693705

694706
def _effective_precision_and_scale(self, precision: int, scale: int):
695-
"""Widen a declared DECIMAL(precision, scale) so the bound value does not lose
696-
fractional digits: the value's own scale is authoritative over an
697-
under-declared target scale. Integer-digit room from the declared type is kept.
707+
"""Widen a declared DECIMAL(precision, scale) so the bound value fits: the
708+
value's own scale is authoritative over an under-declared target scale, and
709+
the integer-digit room is widened to the value's integer digits when the
710+
declared ``precision - scale`` is too small (so the declared precision is not
711+
enforced as a range check). Extra integer room from the declared type is kept.
698712
699713
Exception: the total is capped at MAX_PRECISION (38) digits. If the value
700714
itself needs more than 38 digits (e.g. a scale over 38), the cast is clamped
701715
and the server rounds away the fractional digits that do not fit.
702716
"""
703-
try:
704-
sign, digits, exponent = self.value.as_tuple()
705-
except AttributeError:
706-
return precision, scale
717+
value = self.value
718+
if not isinstance(value, decimal.Decimal):
719+
# The value is sent to the server as str(value), so inspect that same
720+
# textual form; this makes int / float / str values widen like Decimals.
721+
try:
722+
value = decimal.Decimal(str(value))
723+
except (decimal.InvalidOperation, ValueError, TypeError):
724+
return precision, scale
725+
sign, digits, exponent = value.as_tuple()
707726
if not isinstance(exponent, int):
708727
# NaN / Infinity: leave the declared type as-is
709728
return precision, scale

‎tests/unit/test_parameters.py‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,13 @@ def test_calculate_decimal_cast_string(value, expected):
130130
# NaN / Infinity leave the declared type untouched
131131
(Decimal("NaN"), 2, 10, "DECIMAL(10,2)"),
132132
(Decimal("Infinity"), 2, 10, "DECIMAL(10,2)"),
133-
# Non-Decimal values (no as_tuple) fall back to the declared type
133+
# Non-Decimal values are inspected via str(value), which is what is sent
134134
(1.5, 2, 10, "DECIMAL(10,2)"),
135+
(12.3456, 2, 10, "DECIMAL(12,4)"),
136+
(123456789012, 0, 10, "DECIMAL(12,0)"),
137+
("123.4567", 2, 10, "DECIMAL(12,4)"),
138+
# Unparseable values fall back to the declared type
139+
("not-a-number", 2, 10, "DECIMAL(10,2)"),
135140
),
136141
)
137142
def test_decimal_explicit_precision_scale_cast_string(value, scale, precision, expected):
@@ -164,6 +169,26 @@ def test_decimal_scale_greater_than_precision_legacy_swapped():
164169
assert p._cast_expr() == "DECIMAL(10,2)"
165170

166171

172+
@pytest.mark.parametrize(
173+
"scale,precision,match",
174+
(
175+
(2, 50, "precision must be between 1 and 38"),
176+
(0, 0, "precision must be between 1 and 38"),
177+
(-1, 10, "scale must be >= 0"),
178+
),
179+
)
180+
def test_decimal_invalid_declared_precision_or_scale_raises(scale, precision, match):
181+
with pytest.raises(ValueError, match=match):
182+
DecimalParameter(Decimal("1.5"), scale=scale, precision=precision)
183+
184+
185+
def test_decimal_legacy_swapped_out_of_range_precision_raises():
186+
"""The range check applies after the legacy swap (scale=50 becomes precision=50)."""
187+
with pytest.warns(FutureWarning, match="legacy"):
188+
with pytest.raises(ValueError, match="precision must be between 1 and 38"):
189+
DecimalParameter(Decimal("1.5"), scale=50, precision=2)
190+
191+
167192
class Primitive(Enum):
168193
"""These are the inferrable types. This Enum is used for parametrized tests."""
169194

0 commit comments

Comments
 (0)