Skip to content

Commit 8c170bf

Browse files
author
peco-engineer-bot[bot]
committed
[coverage] Conformance findings: PARAMQUERY-021 (#901)
Signed-off-by: peco-engineer-bot[bot] <3815206+peco-engineer-bot[bot]@users.noreply.github.com>
1 parent 85b69ea commit 8c170bf

3 files changed

Lines changed: 64 additions & 4 deletions

File tree

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

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -615,9 +615,10 @@ def __init__(
615615
616616
The `name` argument to this function would be `my_param`.
617617
:scale:
618-
The maximum precision (total number of digits) of the number between 1 and 38.
618+
The number of digits to the right of the decimal point. If the value itself
619+
has more fractional digits, the value's scale is used so no digits are lost.
619620
:precision:
620-
The number of digits to the right of the decimal point.
621+
The maximum precision (total number of digits) of the number between 1 and 38.
621622
"""
622623
self.value: decimal.Decimal = value
623624
self.name = name
@@ -638,11 +639,37 @@ def valid_scale_and_precision(self):
638639
return False
639640

640641
def _cast_expr(self):
641-
if self.scale and self.precision:
642-
return self.CAST_EXPR.format(self.scale, self.precision)
642+
if self.scale is not None and self.precision is not None:
643+
precision, scale = self._effective_precision_and_scale(self.precision, self.scale)
644+
# SQL DECIMAL type syntax is DECIMAL(precision, scale)
645+
return self.CAST_EXPR.format(precision, scale)
643646
else:
644647
return self.calculate_decimal_cast_string(self.value)
645648

649+
MAX_PRECISION = 38
650+
651+
def _effective_precision_and_scale(self, precision: int, scale: int):
652+
"""Widen a declared DECIMAL(precision, scale) so the bound value never loses
653+
fractional digits: the value's own scale is authoritative over an
654+
under-declared target scale. Integer-digit room from the declared type is kept.
655+
"""
656+
try:
657+
sign, digits, exponent = self.value.as_tuple()
658+
except AttributeError:
659+
return precision, scale
660+
if not isinstance(exponent, int):
661+
# NaN / Infinity: leave the declared type as-is
662+
return precision, scale
663+
664+
value_scale = max(-exponent, 0)
665+
value_int_digits = max(len(digits) + exponent, 0)
666+
667+
effective_scale = max(scale, value_scale)
668+
int_digits = max(precision - scale, value_int_digits)
669+
effective_precision = min(int_digits + effective_scale, self.MAX_PRECISION)
670+
effective_scale = min(effective_scale, effective_precision)
671+
return effective_precision, effective_scale
672+
646673
def calculate_decimal_cast_string(self, input: decimal.Decimal) -> str:
647674
"""Returns the smallest SQL cast argument that can contain the passed decimal
648675

‎tests/e2e/test_parameterized_queries.py‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,26 @@ def test_native_recursive_complex_type(
510510
)
511511
assert self._recursive_compare(result.col, data)
512512

513+
@pytest.mark.parametrize("extra_params", [{}, {"use_sea": True}])
514+
def test_decimal_explicit_precision_scale_order(self, extra_params):
515+
"""PARAMQUERY-021 (a): a declared DECIMAL(10,2) target (precision=10, scale=2)
516+
must be sent as DECIMAL(10,2), not DECIMAL(2,10), and the value round-trips."""
517+
param = DecimalParameter(Decimal("123.45"), scale=2, precision=10)
518+
with self.cursor(extra_params) as cursor:
519+
result = cursor.execute("SELECT ? AS v", [param]).fetchone()
520+
521+
assert result.v == Decimal("123.45")
522+
523+
@pytest.mark.parametrize("extra_params", [{}, {"use_sea": True}])
524+
def test_decimal_target_type_never_truncates_bound_scale(self, extra_params):
525+
"""PARAMQUERY-021 (c): the value's own scale (4) is authoritative over an
526+
under-declared target scale (2): all four fractional digits must survive."""
527+
param = DecimalParameter(Decimal("123.4567"), scale=2, precision=10)
528+
with self.cursor(extra_params) as cursor:
529+
result = cursor.execute("SELECT ? AS v", [param]).fetchone()
530+
531+
assert result.v == Decimal("123.4567")
532+
513533

514534
class TestInlineParameterSyntax(PySQLPytestTestCase):
515535
"""The inline parameter approach uses pyformat markers"""

‎tests/unit/test_parameters.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,19 @@ def test_calculate_decimal_cast_string(value, expected):
107107
assert p._cast_expr() == expected
108108

109109

110+
@pytest.mark.parametrize(
111+
"value,scale,precision,expected",
112+
(
113+
(Decimal("123.45"), 2, 10, "DECIMAL(10,2)"),
114+
(Decimal("123"), 0, 10, "DECIMAL(10,0)"),
115+
),
116+
)
117+
def test_decimal_explicit_precision_scale_cast_string(value, scale, precision, expected):
118+
"""PARAMQUERY-021: SQL DECIMAL type is DECIMAL(precision, scale)."""
119+
p = DecimalParameter(value, scale=scale, precision=precision)
120+
assert p._cast_expr() == expected
121+
122+
110123
class Primitive(Enum):
111124
"""These are the inferrable types. This Enum is used for parametrized tests."""
112125

0 commit comments

Comments
 (0)