From: Mike Bayer Date: Mon, 8 Nov 2021 17:20:23 +0000 (-0500) Subject: remove "native decimal" warning X-Git-Tag: rel_2_0_0b1~647^2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=93fad8fb0c5421ad162064e0aa506cb1e70cbf2b;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git remove "native decimal" warning 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 --- diff --git a/doc/build/changelog/unreleased_20/7299.rst b/doc/build/changelog/unreleased_20/7299.rst new file mode 100644 index 0000000000..af2955e218 --- /dev/null +++ b/doc/build/changelog/unreleased_20/7299.rst @@ -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 diff --git a/lib/sqlalchemy/sql/sqltypes.py b/lib/sqlalchemy/sql/sqltypes.py index 5599460728..52033f5865 100644 --- a/lib/sqlalchemy/sql/sqltypes.py +++ b/lib/sqlalchemy/sql/sqltypes.py @@ -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, diff --git a/lib/sqlalchemy/testing/requirements.py b/lib/sqlalchemy/testing/requirements.py index 4cc431bb76..56df452a54 100644 --- a/lib/sqlalchemy/testing/requirements.py +++ b/lib/sqlalchemy/testing/requirements.py @@ -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 diff --git a/lib/sqlalchemy/testing/suite/test_types.py b/lib/sqlalchemy/testing/suite/test_types.py index 7438b8bc89..aa796df76c 100644 --- a/lib/sqlalchemy/testing/suite/test_types.py +++ b/lib/sqlalchemy/testing/suite/test_types.py @@ -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}} diff --git a/test/perf/orm2010.py b/test/perf/orm2010.py index a96ba23d79..de467cfd73 100644 --- a/test/perf/orm2010.py +++ b/test/perf/orm2010.py @@ -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() diff --git a/test/requirements.py b/test/requirements.py index 28a283b123..e6f49f0aea 100644 --- a/test/requirements.py +++ b/test/requirements.py @@ -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( diff --git a/test/sql/test_types.py b/test/sql/test_types.py index a15d163e04..7f850d00b8 100644 --- a/test/sql/test_types.py +++ b/test/sql/test_types.py @@ -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):