From 04726260c591efbc154814fe5f15b172fccb862d Mon Sep 17 00:00:00 2001 From: Madan Kumar Date: Sun, 20 Sep 2026 14:34:33 +0530 Subject: [PATCH] Fix DECIMAL parameter cast size for exponent-notation and negative values DecimalParameter.calculate_decimal_cast_string inferred DECIMAL(precision, scale) by string-splitting str(value) on ".", which miscounts two forms that str(Decimal) legitimately produces: - exponent notation (Decimal("1500").normalize() -> "1.5E+3"): the "E+3" was counted as fractional digits, giving DECIMAL(5,4) for 1500, which overflows (an error in ANSI mode, NULL in legacy mode). - a leading minus sign: counted as an extra integer digit, so a negative 38-digit value produced DECIMAL(39,0), exceeding Databricks' max DECIMAL precision of 38 and failing the cast. Derive precision/scale from Decimal.as_tuple() (digits + exponent), ignoring sign and display format. Existing cast-string tests are unchanged; added regression cases for exponent notation and negative values. Signed-off-by: Madan Kumar --- src/databricks/sql/parameters/native.py | 27 +++++++++++-------------- tests/unit/test_parameters.py | 6 ++++++ 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/databricks/sql/parameters/native.py b/src/databricks/sql/parameters/native.py index d0d811940..57aeee729 100644 --- a/src/databricks/sql/parameters/native.py +++ b/src/databricks/sql/parameters/native.py @@ -651,23 +651,20 @@ def calculate_decimal_cast_string(self, input: decimal.Decimal) -> str: Output: DECIMAL(8,4) """ - string_decimal = str(input) - - if string_decimal.startswith("0."): - # This decimal is less than 1 - overall = after = len(string_decimal) - 2 - elif "." not in string_decimal: - # This decimal has no fractional component - overall = len(string_decimal) - after = 0 + # Derive precision/scale from the exact numeric value rather than str(), + # whose sign and exponent notation (e.g. "1.5E+3", "-12.34") would be + # miscounted as extra digits. + _, digits, exponent = input.as_tuple() + if exponent >= 0: + # Integer value: `digits` followed by `exponent` trailing zeros. + scale = 0 + precision = len(digits) + exponent else: - # This decimal has both whole and fractional parts - parts = string_decimal.split(".") - parts_lengths = [len(i) for i in parts] - before, after = parts_lengths[:2] - overall = before + after + scale = -exponent + # A value < 1 still needs `scale` digits of precision. + precision = max(len(digits), scale) - return self.CAST_EXPR.format(overall, after) + return self.CAST_EXPR.format(precision, scale) def dbsql_parameter_from_int(value: int, name: Optional[str] = None): diff --git a/tests/unit/test_parameters.py b/tests/unit/test_parameters.py index 0588eb499..89d14bad9 100644 --- a/tests/unit/test_parameters.py +++ b/tests/unit/test_parameters.py @@ -100,6 +100,12 @@ def test_parameters_enabled(self, test_input, expected): (Decimal("123456789.123456789"), "DECIMAL(18,9)"), (Decimal("12345678912345678912345678912345678912"), "DECIMAL(38,0)"), (Decimal("1234.56"), "DECIMAL(6,2)"), + # Exponent notation (e.g. from Decimal.normalize()) and negative values + # must be sized from the numeric value, not str(). + (Decimal("1500").normalize(), "DECIMAL(4,0)"), + (Decimal("1e5"), "DECIMAL(6,0)"), + (Decimal("-" + "9" * 38), "DECIMAL(38,0)"), + (Decimal("-12.34"), "DECIMAL(4,2)"), ), ) def test_calculate_decimal_cast_string(value, expected):