]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Use _effective_decimal_return_scale in Numeric.result_processor
authorKadir Can Ozden <101993364+bysiber@users.noreply.github.com>
Thu, 9 Jul 2026 15:43:58 +0000 (11:43 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Thu, 9 Jul 2026 17:24:49 +0000 (13:24 -0400)
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 <mike_mp@zzzcomputing.com>
Pull-request: https://github.com/sqlalchemy/sqlalchemy/pull/13137
Pull-request-sha: 9d5dbc80a4d053b46454c87d56d36b51c672a2dc

Change-Id: Ib671aa9db19c64fe69d95a47a4b8420b96e918b9
(cherry picked from commit 117851648a21cf6cb12430df33913f200a090100)

doc/build/changelog/unreleased_20/13424.rst [new file with mode: 0644]
lib/sqlalchemy/sql/sqltypes.py
test/sql/test_types.py

diff --git a/doc/build/changelog/unreleased_20/13424.rst b/doc/build/changelog/unreleased_20/13424.rst
new file mode 100644 (file)
index 0000000..970839d
--- /dev/null
@@ -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.
index 98fa8890f98a4f6abc6580cea8905f3dc44cc07c..9feb29bce8b945422fdd0abad95885e84dbddd05 100644 (file)
@@ -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:
index cd34ed6af3ca4d0e173d0bd287ba5f509c9e7611..bab39eee7c7709b15be501baea896e65c9b1be20 100644 (file)
@@ -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