--- /dev/null
+.. 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
# 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,
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
from ... import Text
from ... import Time
from ... import TIMESTAMP
+from ... import type_coerce
from ... import TypeDecorator
from ... import Unicode
from ... import UnicodeText
@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)
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),
[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),
# 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")
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}
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}
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}}
from decimal import Decimal
import os
import random
-import warnings
from sqlalchemy import __version__
from sqlalchemy import Column
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()
"""
- 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"),
]
)
]
)
+ @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(
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():
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
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
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):