From: Kadir Can Ozden <101993364+bysiber@users.noreply.github.com> Date: Thu, 9 Jul 2026 15:43:58 +0000 (-0400) Subject: Use _effective_decimal_return_scale in Numeric.result_processor X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=53df8f959b52f304e7d5dc969a834c7386ee5393;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Use _effective_decimal_return_scale in Numeric.result_processor Fixed an issue in :class:`.Numeric` where the :paramref:`.Numeric.decimal_return_scale` parameter was ignored when the DBAPI does not support native decimal objects (i.e. ``dialect.supports_native_decimal`` is ``False``). In this path the result processor was computing the conversion scale from :paramref:`.Numeric.scale` directly, bypassing :paramref:`.Numeric.decimal_return_scale` entirely. The behavior now matches :class:`.Float`, which already used the correct ``_effective_decimal_return_scale`` property. Pull request courtesy Kadir Can Ozden. Fixes: #13424 Closes: #13137 Co-authored-by: Mike Bayer Pull-request: https://github.com/sqlalchemy/sqlalchemy/pull/13137 Pull-request-sha: 9d5dbc80a4d053b46454c87d56d36b51c672a2dc Change-Id: Ib671aa9db19c64fe69d95a47a4b8420b96e918b9 (cherry picked from commit 117851648a21cf6cb12430df33913f200a090100) --- diff --git a/doc/build/changelog/unreleased_20/13424.rst b/doc/build/changelog/unreleased_20/13424.rst new file mode 100644 index 0000000000..970839d10e --- /dev/null +++ b/doc/build/changelog/unreleased_20/13424.rst @@ -0,0 +1,14 @@ +.. change:: + :tags: bug, sql + :tickets: 13424 + + Fixed an issue in :class:`.Numeric` where the + :paramref:`.Numeric.decimal_return_scale` parameter was ignored when the + DBAPI does not support native decimal objects (i.e. + ``dialect.supports_native_decimal`` is ``False``). In this path the result + processor was computing the conversion scale from + :paramref:`.Numeric.scale` directly, bypassing + :paramref:`.Numeric.decimal_return_scale` entirely. The behavior now + matches :class:`.Float`, which already used the correct + ``_effective_decimal_return_scale`` property. Pull request courtesy Kadir + Can Ozden. diff --git a/lib/sqlalchemy/sql/sqltypes.py b/lib/sqlalchemy/sql/sqltypes.py index 98fa8890f9..9feb29bce8 100644 --- a/lib/sqlalchemy/sql/sqltypes.py +++ b/lib/sqlalchemy/sql/sqltypes.py @@ -563,11 +563,7 @@ class Numeric(HasExpressionLookup, TypeEngine[_N]): # we're a "numeric", DBAPI returns floats, convert. return processors.to_decimal_processor_factory( decimal.Decimal, - ( - self.scale - if self.scale is not None - else self._default_decimal_return_scale - ), + self._effective_decimal_return_scale, ) else: if dialect.supports_native_decimal: diff --git a/test/sql/test_types.py b/test/sql/test_types.py index cd34ed6af3..bab39eee7c 100644 --- a/test/sql/test_types.py +++ b/test/sql/test_types.py @@ -4170,6 +4170,105 @@ class NumericRawSQLTest(fixtures.TestBase): eq_(val, 46.583) +class NumericDecimalReturnScaleTest(fixtures.TestBase): + """Test that Numeric and Float honour decimal_return_scale when the DBAPI + does not return native Decimal objects (supports_native_decimal=False). + + issue #13424 + + """ + + # TODO: re-add when supports_native_decimal is fixed + # __sparse_driver_backend__ = True + + numeric_cases = testing.combinations( + # Numeric: decimal_return_scale overrides scale + ( + Numeric(10, 2, decimal_return_scale=5), + 1.23456789, + decimal.Decimal("1.23457"), + ), + # Numeric: falls back to scale when decimal_return_scale not set + (Numeric(10, 2), 1.23456789, decimal.Decimal("1.23")), + # Numeric: falls back to _default_decimal_return_scale (10) + # when neither scale nor decimal_return_scale is set + ( + Numeric(asdecimal=True), + 1.23456789012345, + decimal.Decimal("1.2345678901"), + ), + # Float: decimal_return_scale (pre-existing behavior, for contrast); + # cases 1 and 4 returning the same value asserts consistency + ( + Float(decimal_return_scale=5, asdecimal=True), + 1.23456789, + decimal.Decimal("1.23457"), + ), + ( + Float(asdecimal=True), + 1.23456789012345, + decimal.Decimal("1.2345678901"), + ), + id_="nnn", + argnames="typ,value,expected", + ) + + @numeric_cases + def test_result_processor_decimal_return_scale(self, typ, value, expected): + """result_processor honours decimal_return_scale.""" + proc = typ.result_processor( + mock.Mock(supports_native_decimal=False), None + ) + eq_(proc(value), expected) + + # TODO: make this a more general requirement, however for the moment + # the supports_native_decimal flag seems to not be correctly set for + # some dialects such as psycopg + @testing.only_on("sqlite") + @testing.combinations( + ( + Numeric(10, 5, decimal_return_scale=3), + decimal.Decimal("1.12345"), + decimal.Decimal("1.123"), + ), + ( + Float(decimal_return_scale=3, asdecimal=True), + 1.12345, + decimal.Decimal("1.123"), + ), + id_="nnn", + argnames="typ,value,expected", + ) + def test_decimal_return_scale_roundtrip( + self, connection, metadata, typ, value, expected + ): + """Numeric and Float both honour decimal_return_scale + end-to-end. + + Only runs on SQLite where the DBAPI truly returns floats for + NUMERIC/FLOAT columns. Other backends like psycopg return + native Decimal objects despite supports_native_decimal=False, + which bypasses the result processor being tested here. + """ + + t = Table( + "t", + metadata, + Column( + "numeric_val", + typ, + ), + ) + t.create(connection) + + connection.execute( + t.insert(), + {"numeric_val": value}, + ) + + eq_(connection.scalar(select(t.c.numeric_val)), expected) + + class IntervalTest(fixtures.TablesTest, AssertsExecutionResults): __sparse_driver_backend__ = True