diff --git a/ibm_db_sa/base.py b/ibm_db_sa/base.py index b0169ce..27bf088 100644 --- a/ibm_db_sa/base.py +++ b/ibm_db_sa/base.py @@ -224,7 +224,9 @@ class BOOLEAN(sa_types.Boolean): __visit_name__ = 'BOOLEAN' -class DOUBLE(sa_types.Numeric): +# DOUBLE is a binary floating-point type and the DBAPI returns float for it, +# so declare it as a Float (asdecimal=False), not a Numeric. +class DOUBLE(sa_types.Float): __visit_name__ = 'DOUBLE' diff --git a/ibm_db_sa/ibm_db.py b/ibm_db_sa/ibm_db.py index 9ddb9ae..22af71c 100644 --- a/ibm_db_sa/ibm_db.py +++ b/ibm_db_sa/ibm_db.py @@ -128,7 +128,11 @@ class DB2Dialect_ibm_db(DB2Dialect): colspecs = util.update_copy( DB2Dialect.colspecs, { - sa_types.Numeric: _IBM_Numeric_ibm_db + sa_types.Numeric: _IBM_Numeric_ibm_db, + # Float subclasses Numeric; without its own entry it would be + # adapted to _IBM_Numeric_ibm_db, which ignores Float's + # asdecimal result conversion. + sa_types.Float: sa_types.Float, } ) diff --git a/test/test_float.py b/test/test_float.py new file mode 100644 index 0000000..f2cf0b1 --- /dev/null +++ b/test/test_float.py @@ -0,0 +1,114 @@ +"""Float result conversion with the ibm_db DBAPI.""" + +from decimal import Decimal + +from sqlalchemy import Column, Float, Integer, MetaData, REAL, Table, inspect, select +from sqlalchemy.testing import fixtures +from sqlalchemy.testing.assertions import eq_ + +from ibm_db_sa.base import DOUBLE +from ibm_db_sa.ibm_db import DB2Dialect_ibm_db + + +VALUES = (1.25, -2.5, 0.0, None) + + +def _expected(value, asdecimal): + if value is None or not asdecimal: + return value + return Decimal(str(value)) + + +class TestFloatResults(fixtures.TestBase): + def _check(self, type_, asdecimal): + dialect = DB2Dialect_ibm_db() + impl = type_.dialect_impl(dialect) + processor = impl.result_processor(dialect, None) + for value in VALUES: + result = processor(value) if processor else value + expected = _expected(value, asdecimal) + eq_(result, expected) + eq_(type(result), type(expected)) + + def test_float_asdecimal(self): + self._check(Float(asdecimal=True), True) + + def test_real_asdecimal(self): + self._check(REAL(asdecimal=True), True) + + def test_float_default(self): + self._check(Float(), False) + + def test_double_is_float(self): + type_ = DOUBLE() + eq_(isinstance(type_, Float), True) + eq_(type_.asdecimal, False) + eq_(type_.python_type, float) + eq_(str(type_.compile(dialect=DB2Dialect_ibm_db())), "DOUBLE") + self._check(type_, False) + + +class TestFloatRoundTrip(fixtures.TestBase): + __only_on__ = "ibm_db_sa+ibm_db_sa" + __backend__ = True + + def _round_trip(self, metadata, connection, asdecimal): + table = Table( + "float_results", + metadata, + Column("id", Integer, primary_key=True, autoincrement=False), + Column("amount", Float(asdecimal=asdecimal)), + ) + table.create(connection) + connection.execute( + table.insert(), + [{"id": i, "amount": value} for i, value in enumerate(VALUES)], + ) + actual = ( + connection.execute(select(table.c.amount).order_by(table.c.id)) + .scalars() + .all() + ) + expected = [_expected(value, asdecimal) for value in VALUES] + eq_(actual, expected) + for value, result in zip(expected, actual): + eq_(type(result), type(value)) + + def test_float_asdecimal_round_trip(self, metadata, connection): + self._round_trip(metadata, connection, True) + + def test_float_round_trip(self, metadata, connection): + self._round_trip(metadata, connection, False) + + def test_reflected_double(self, metadata, connection): + table = Table( + "float_reflect", + metadata, + Column("id", Integer, primary_key=True, autoincrement=False), + Column("amount", DOUBLE()), + ) + table.create(connection) + connection.execute( + table.insert(), + [{"id": i, "amount": value} for i, value in enumerate(VALUES)], + ) + inspector = inspect(connection) + schema = connection.dialect.normalize_name( + connection.exec_driver_sql("VALUES CURRENT SCHEMA").scalar().strip() + ) + columns = inspector.get_columns("float_reflect", schema=schema) + type_ = next(c["type"] for c in columns if c["name"] == "amount") + eq_(isinstance(type_, DOUBLE), True) + eq_(type_.asdecimal, False) + eq_(type_.python_type, float) + reflected = Table( + "float_reflect", MetaData(), schema=schema, autoload_with=connection + ) + actual = ( + connection.execute(select(reflected.c.amount).order_by(reflected.c.id)) + .scalars() + .all() + ) + eq_(actual, list(VALUES)) + for value, result in zip(VALUES, actual): + eq_(type(result), type(value))