]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
remove "native decimal" warning
authorMike Bayer <mike_mp@zzzcomputing.com>
Mon, 8 Nov 2021 17:20:23 +0000 (12:20 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Wed, 17 Nov 2021 23:21:48 +0000 (18:21 -0500)
Removed the warning that emits from the :class:`_types.Numeric` type about
DBAPIs not supporting Decimal values natively. This warning was oriented
towards SQLite, which does not have any real way without additional
extensions or workarounds of handling precision numeric values more than 15
significant digits as it only uses floating point math to represent
numbers. As this is a known and documented limitation in SQLite itself, and
not a quirk of the pysqlite driver, there's no need for SQLAlchemy to warn
for this. The change does not otherwise modify how precision numerics are
handled. Values can continue to be handled as ``Decimal()`` or ``float()``
as configured with the :class:`_types.Numeric`, :class:`_types.Float` , and
related datatypes, just without the ability to maintain precision beyond 15
significant digits when using SQLite, unless alternate representations such
as strings are used.

Fixes: #7299
Change-Id: Ic570f8107177dec3ddbe94c7b43f40057b03276a

doc/build/changelog/unreleased_20/7299.rst [new file with mode: 0644]
lib/sqlalchemy/sql/sqltypes.py
lib/sqlalchemy/testing/requirements.py
lib/sqlalchemy/testing/suite/test_types.py
test/perf/orm2010.py
test/requirements.py
test/sql/test_types.py

diff --git a/doc/build/changelog/unreleased_20/7299.rst b/doc/build/changelog/unreleased_20/7299.rst
new file mode 100644 (file)
index 0000000..af2955e
--- /dev/null
@@ -0,0 +1,17 @@
+.. change::
+    :tags: bug, sqlite
+    :tickets: 7299
+
+    Removed the warning that emits from the :class:`_types.Numeric` type about
+    DBAPIs not supporting Decimal values natively. This warning was oriented
+    towards SQLite, which does not have any real way without additional
+    extensions or workarounds of handling precision numeric values more than 15
+    significant digits as it only uses floating point math to represent
+    numbers. As this is a known and documented limitation in SQLite itself, and
+    not a quirk of the pysqlite driver, there's no need for SQLAlchemy to warn
+    for this. The change does not otherwise modify how precision numerics are
+    handled. Values can continue to be handled as ``Decimal()`` or ``float()``
+    as configured with the :class:`_types.Numeric`, :class:`_types.Float` , and
+    related datatypes, just without the ability to maintain precision beyond 15
+    significant digits when using SQLite, unless alternate representations such
+    as strings are used.
\ No newline at end of file
index 55994607288d1dcfcb92cdcf4fc9d482bfb4ee7c..52033f5865d61fc7bc484451a22ca44752cae6d1 100644 (file)
@@ -490,16 +490,6 @@ class Numeric(_LookupExpressionAdapter, TypeEngine):
                 # we're a "numeric", DBAPI will give us Decimal directly
                 return None
             else:
-                util.warn(
-                    "Dialect %s+%s does *not* support Decimal "
-                    "objects natively, and SQLAlchemy must "
-                    "convert from floating point - rounding "
-                    "errors and other issues may occur. Please "
-                    "consider storing Decimal numbers as strings "
-                    "or integers on this platform for lossless "
-                    "storage." % (dialect.name, dialect.driver)
-                )
-
                 # we're a "numeric", DBAPI returns floats, convert.
                 return processors.to_decimal_processor_factory(
                     decimal.Decimal,
index 4cc431bb7683db8533eac084fe62d5cb89df2e9b..56df452a54cb0fd55ba6c7a749a73efc9aab7f89 100644 (file)
@@ -956,6 +956,21 @@ class SuiteRequirements(Requirements):
 
         return exclusions.open()
 
+    @property
+    def numeric_received_as_decimal_untyped(self):
+        """target backend will return result columns that are explicitly
+        against NUMERIC or similar precision-numeric datatypes (not including
+        FLOAT or INT types) as Python Decimal objects, and not as floats
+        or ints, including when no SQLAlchemy-side typing information is
+        associated with the statement (e.g. such as a raw SQL string).
+
+        This should be enabled if either the DBAPI itself returns Decimal
+        objects, or if the dialect has set up DBAPI-specific return type
+        handlers such that Decimal objects come back automatically.
+
+        """
+        return exclusions.open()
+
     @property
     def nested_aggregates(self):
         """target database can select an aggregate from a subquery that's
index 7438b8bc89313ef268c670eb5ec3a4fcd681b6ac..aa796df76c0e812e5ac2a2d6c0dc708c917b4ead 100644 (file)
@@ -35,6 +35,7 @@ from ... import testing
 from ... import Text
 from ... import Time
 from ... import TIMESTAMP
+from ... import type_coerce
 from ... import TypeDecorator
 from ... import Unicode
 from ... import UnicodeText
@@ -524,9 +525,6 @@ class NumericTest(_LiteralRoundTripFixture, fixtures.TestBase):
 
     @testing.fixture
     def do_numeric_test(self, metadata, connection):
-        @testing.emits_warning(
-            r".*does \*not\* support Decimal objects natively"
-        )
         def run(type_, input_, output, filter_=None, check_scale=False):
             t = Table("t", metadata, Column("x", type_))
             t.create(connection)
@@ -541,9 +539,30 @@ class NumericTest(_LiteralRoundTripFixture, fixtures.TestBase):
             if check_scale:
                 eq_([str(x) for x in result], [str(x) for x in output])
 
+            connection.execute(t.delete())
+
+            # test that this is actually a number!
+            # note we have tiny scale here as we have tests with very
+            # small scale Numeric types.  PostgreSQL will raise an error
+            # if you use values outside the available scale.
+            if type_.asdecimal:
+                test_value = decimal.Decimal("2.9")
+                add_value = decimal.Decimal("37.12")
+            else:
+                test_value = 2.9
+                add_value = 37.12
+
+            connection.execute(t.insert(), {"x": test_value})
+            assert_we_are_a_number = connection.scalar(
+                select(type_coerce(t.c.x + add_value, type_))
+            )
+            eq_(
+                round(assert_we_are_a_number, 3),
+                round(test_value + add_value, 3),
+            )
+
         return run
 
-    @testing.emits_warning(r".*does \*not\* support Decimal objects natively")
     def test_render_literal_numeric(self, literal_round_trip):
         literal_round_trip(
             Numeric(precision=8, scale=4),
@@ -551,7 +570,6 @@ class NumericTest(_LiteralRoundTripFixture, fixtures.TestBase):
             [decimal.Decimal("15.7563")],
         )
 
-    @testing.emits_warning(r".*does \*not\* support Decimal objects natively")
     def test_render_literal_numeric_asfloat(self, literal_round_trip):
         literal_round_trip(
             Numeric(precision=8, scale=4, asdecimal=False),
@@ -637,14 +655,12 @@ class NumericTest(_LiteralRoundTripFixture, fixtures.TestBase):
     # to render CAST unconditionally since this is kind of an edge case.
 
     @testing.requires.implicit_decimal_binds
-    @testing.emits_warning(r".*does \*not\* support Decimal objects natively")
     def test_decimal_coerce_round_trip(self, connection):
         expr = decimal.Decimal("15.7563")
 
         val = connection.scalar(select(literal(expr)))
         eq_(val, expr)
 
-    @testing.emits_warning(r".*does \*not\* support Decimal objects natively")
     def test_decimal_coerce_round_trip_w_cast(self, connection):
         expr = decimal.Decimal("15.7563")
 
@@ -984,7 +1000,6 @@ class JSONTest(_LiteralRoundTripFixture, fixtures.TablesTest):
         return datatype, compare_value, p_s
 
     @_index_fixtures(False)
-    @testing.emits_warning(r".*does \*not\* support Decimal objects natively")
     def test_index_typed_access(self, datatype, value):
         data_table = self.tables.data_table
         data_element = {"key1": value}
@@ -1007,7 +1022,6 @@ class JSONTest(_LiteralRoundTripFixture, fixtures.TablesTest):
             is_(type(roundtrip), type(compare_value))
 
     @_index_fixtures(True)
-    @testing.emits_warning(r".*does \*not\* support Decimal objects natively")
     def test_index_typed_comparison(self, datatype, value):
         data_table = self.tables.data_table
         data_element = {"key1": value}
@@ -1032,7 +1046,6 @@ class JSONTest(_LiteralRoundTripFixture, fixtures.TablesTest):
             eq_(row, (compare_value,))
 
     @_index_fixtures(True)
-    @testing.emits_warning(r".*does \*not\* support Decimal objects natively")
     def test_path_typed_comparison(self, datatype, value):
         data_table = self.tables.data_table
         data_element = {"key1": {"subkey1": value}}
index a96ba23d79389f3f8693e185d35a0f16d87bfbf7..de467cfd737e96f4689334012ee3e360ee1edf15 100644 (file)
@@ -1,7 +1,6 @@
 from decimal import Decimal
 import os
 import random
-import warnings
 
 from sqlalchemy import __version__
 from sqlalchemy import Column
@@ -10,23 +9,11 @@ from sqlalchemy import ForeignKey
 from sqlalchemy import Integer
 from sqlalchemy import Numeric
 from sqlalchemy import String
-from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.orm import declarative_base
 from sqlalchemy.orm import relationship
 from sqlalchemy.orm import Session
 
 
-warnings.filterwarnings("ignore", r".*Decimal objects natively")  # noqa
-
-# speed up cdecimal if available
-try:
-    import cdecimal
-    import sys
-
-    sys.modules["decimal"] = cdecimal
-except ImportError:
-    pass
-
-
 Base = declarative_base()
 
 
index 28a283b12359577a3a27b3ed1e597ffda97cfa34..e6f49f0aea9a8605c5d672641928f11967a9d849 100644 (file)
@@ -1138,16 +1138,9 @@ class DefaultRequirements(SuiteRequirements):
 
         """
 
-        def broken_cx_oracle(config):
-            return (
-                against(config, "oracle+cx_oracle")
-                and config.db.dialect.cx_oracle_ver <= (6, 0, 2)
-                and config.db.dialect.cx_oracle_ver > (6,)
-            )
-
         return fails_if(
             [
-                ("sqlite", None, None, "TODO"),
+                ("sqlite", None, None, "SQLite numeric limitation"),
             ]
         )
 
@@ -1173,6 +1166,13 @@ class DefaultRequirements(SuiteRequirements):
             ]
         )
 
+    @property
+    def numeric_received_as_decimal_untyped(self):
+        return fails_on(
+            "sqlite",
+            "sqlite doesn't return Decimal objects without special handlers",
+        )
+
     @property
     def infinity_floats(self):
         return fails_on_everything_except(
index a15d163e04144ea5490d8e3a7a927c45803b7f09..7f850d00b83a45b8b21c67a96ea64374d2881b3b 100644 (file)
@@ -90,7 +90,6 @@ from sqlalchemy.testing.schema import Column
 from sqlalchemy.testing.schema import pep435_enum
 from sqlalchemy.testing.schema import Table
 from sqlalchemy.testing.util import picklers
-from sqlalchemy.testing.util import round_decimal
 
 
 def _all_dialect_modules():
@@ -3458,7 +3457,7 @@ class NumericRawSQLTest(fixtures.TestBase):
         metadata.create_all(connection)
         connection.execute(t.insert(), dict(val=data))
 
-    @testing.fails_on("sqlite", "Doesn't provide Decimal results natively")
+    @testing.requires.numeric_received_as_decimal_untyped
     @testing.provide_metadata
     def test_decimal_fp(self, connection):
         metadata = self.metadata
@@ -3469,7 +3468,7 @@ class NumericRawSQLTest(fixtures.TestBase):
         assert isinstance(val, decimal.Decimal)
         eq_(val, decimal.Decimal("45.5"))
 
-    @testing.fails_on("sqlite", "Doesn't provide Decimal results natively")
+    @testing.requires.numeric_received_as_decimal_untyped
     @testing.provide_metadata
     def test_decimal_int(self, connection):
         metadata = self.metadata
@@ -3495,11 +3494,7 @@ class NumericRawSQLTest(fixtures.TestBase):
         val = connection.exec_driver_sql("select val from t").scalar()
         assert isinstance(val, float)
 
-        # some DBAPIs have unusual float handling
-        if testing.against("oracle+cx_oracle"):
-            eq_(round_decimal(val, 3), 46.583)
-        else:
-            eq_(val, 46.583)
+        eq_(val, 46.583)
 
 
 class IntervalTest(fixtures.TablesTest, AssertsExecutionResults):