--- /dev/null
+.. change::
+ :tags: change, sql
+ :tickets: 5252
+
+ the :class:`.Numeric` and :class:`.Float` SQL types have been separated out
+ so that :class:`.Float` no longer inherits from :class:`.Numeric`; instead,
+ they both extend from a common mixin :class:`.NumericCommon`. This
+ corrects for some architectural shortcomings where numeric and float types
+ are typically separate, and establishes more consistency with
+ :class:`.Integer` also being a distinct type. The change should not have
+ any end-user implications except for code that may be using
+ ``isinstance()`` to test for the :class:`.Numeric` datatype; third party
+ dialects which rely upon specific implementation types for numeric and/or
+ float may also require adjustment to maintain compatibility.
.. autoclass:: Numeric
:members:
+.. autoclass:: NumericCommon
+ :members:
+
.. autoclass:: PickleType
:members:
from .types import NCHAR as NCHAR
from .types import NUMERIC as NUMERIC
from .types import Numeric as Numeric
+from .types import NumericCommon as NumericCommon
from .types import NVARCHAR as NVARCHAR
from .types import PickleType as PickleType
from .types import REAL as REAL
self.process(binary.left, **kw),
self.process(binary.right, **kw),
)
- elif binary.type._type_affinity is sqltypes.Numeric:
+ elif binary.type._type_affinity in (sqltypes.Numeric, sqltypes.Float):
type_expression = "ELSE CAST(JSON_VALUE(%s, %s) AS %s)" % (
self.process(binary.left, **kw),
self.process(binary.right, **kw),
)
coltype = sqltypes.NULLTYPE
else:
- if issubclass(coltype, sqltypes.Numeric):
+ if issubclass(coltype, sqltypes.NumericCommon):
kwargs["precision"] = numericprec
if not issubclass(coltype, sqltypes.Float):
from .types import _FloatType
from .types import _IntegerType
from .types import _MatchType
+from .types import _NumericCommonType
from .types import _NumericType
from .types import _StringType
from .types import BIGINT
colspecs = {
_IntegerType: _IntegerType,
+ _NumericCommonType: _NumericCommonType,
_NumericType: _NumericType,
_FloatType: _FloatType,
sqltypes.Numeric: NUMERIC,
self.process(binary.right, **kw),
)
)
- elif binary.type._type_affinity is sqltypes.Numeric:
+ elif binary.type._type_affinity in (sqltypes.Numeric, sqltypes.Float):
if (
binary.type.scale is not None
and binary.type.precision is not None
)
def _mysql_type(self, type_):
- return isinstance(type_, (_StringType, _NumericType))
+ return isinstance(type_, (_StringType, _NumericCommonType))
def visit_NUMERIC(self, type_, **kw):
if type_.precision is None:
from ...sql import sqltypes
-class _NumericType:
+class _NumericCommonType:
"""Base for MySQL numeric types.
This is the base both for NUMERIC as well as INTEGER, hence
self.zerofill = zerofill
super().__init__(**kw)
+
+class _NumericType(_NumericCommonType, sqltypes.Numeric):
+
def __repr__(self):
return util.generic_repr(
- self, to_inspect=[_NumericType, sqltypes.Numeric]
+ self,
+ to_inspect=[_NumericType, _NumericCommonType, sqltypes.Numeric],
)
-class _FloatType(_NumericType, sqltypes.Float):
+class _FloatType(_NumericCommonType, sqltypes.Float):
+
def __init__(self, precision=None, scale=None, asdecimal=True, **kw):
if isinstance(self, (REAL, DOUBLE)) and (
(precision is None and scale is not None)
def __repr__(self):
return util.generic_repr(
- self, to_inspect=[_FloatType, _NumericType, sqltypes.Float]
+ self, to_inspect=[_FloatType, _NumericCommonType, sqltypes.Float]
)
-class _IntegerType(_NumericType, sqltypes.Integer):
+class _IntegerType(_NumericCommonType, sqltypes.Integer):
def __init__(self, display_width=None, **kw):
self.display_width = display_width
super().__init__(**kw)
def __repr__(self):
return util.generic_repr(
- self, to_inspect=[_IntegerType, _NumericType, sqltypes.Integer]
+ self,
+ to_inspect=[_IntegerType, _NumericCommonType, sqltypes.Integer],
)
return handler
-class _OracleNumeric(sqltypes.Numeric):
+class _OracleNumericCommon(sqltypes.NumericCommon, sqltypes.TypeEngine):
is_number = False
def bind_processor(self, dialect):
return handler
+class _OracleNumeric(_OracleNumericCommon, sqltypes.Numeric):
+ pass
+
+
+class _OracleFloat(_OracleNumericCommon, sqltypes.Float):
+ pass
+
+
class _OracleUUID(sqltypes.Uuid):
def get_dbapi_type(self, dbapi):
return dbapi.STRING
-class _OracleBinaryFloat(_OracleNumeric):
+class _OracleBinaryFloat(_OracleNumericCommon):
def get_dbapi_type(self, dbapi):
return dbapi.NATIVE_FLOAT
pass
-class _OracleNUMBER(_OracleNumeric):
+class _OracleNUMBER(_OracleNumericCommon, sqltypes.Numeric):
is_number = True
arraysize=len_params,
)
elif (
- isinstance(type_impl, _OracleNumeric)
+ isinstance(type_impl, _OracleNumericCommon)
and type_impl.asdecimal
):
out_parameters[name] = self.cursor.var(
{
sqltypes.TIMESTAMP: _CXOracleTIMESTAMP,
sqltypes.Numeric: _OracleNumeric,
- sqltypes.Float: _OracleNumeric,
+ sqltypes.Float: _OracleFloat,
oracle.BINARY_FLOAT: _OracleBINARY_FLOAT,
oracle.BINARY_DOUBLE: _OracleBINARY_DOUBLE,
sqltypes.Integer: _OracleInteger,
_server_side_id = util.counter()
-class _PsycopgNumeric(sqltypes.Numeric):
+class _PsycopgNumericCommon(sqltypes.NumericCommon):
def bind_processor(self, dialect):
return None
)
-class _PsycopgFloat(_PsycopgNumeric):
- __visit_name__ = "float"
+class _PsycopgNumeric(_PsycopgNumericCommon, sqltypes.Numeric):
+ pass
+
+
+class _PsycopgFloat(_PsycopgNumericCommon, sqltypes.Float):
+ pass
class _PsycopgHStore(HSTORE):
return process
-class AsyncpgNumeric(sqltypes.Numeric):
+class _AsyncpgNumericCommon(sqltypes.NumericCommon):
render_bind_cast = True
def bind_processor(self, dialect):
)
-class AsyncpgFloat(AsyncpgNumeric, sqltypes.Float):
- __visit_name__ = "float"
- render_bind_cast = True
+class AsyncpgNumeric(_AsyncpgNumericCommon, sqltypes.Numeric):
+ pass
+
+
+class AsyncpgFloat(_AsyncpgNumericCommon, sqltypes.Float):
+ pass
class AsyncpgREGCLASS(REGCLASS):
render_bind_cast = True
-class _PGNumeric(sqltypes.Numeric):
+class _PGNumericCommon(sqltypes.NumericCommon):
render_bind_cast = True
def result_processor(self, dialect, coltype):
)
-class _PGFloat(_PGNumeric, sqltypes.Float):
- __visit_name__ = "float"
- render_bind_cast = True
+class _PGNumeric(_PGNumericCommon, sqltypes.Numeric):
+ pass
+
+
+class _PGFloat(_PGNumericCommon, sqltypes.Float):
+ pass
class _PGNumericNoBind(_PGNumeric):
(
binary.right.type
if binary.right.type._type_affinity
- is sqltypes.Numeric
+ in (sqltypes.Numeric, sqltypes.Float)
else sqltypes.Numeric()
),
),
from typing import Callable
from typing import cast
from typing import Dict
+from typing import Generic
from typing import List
from typing import Optional
from typing import overload
Date: Date,
Integer: self.__class__,
Numeric: Numeric,
+ Float: Float,
},
operators.mul: {
Interval: Interval,
Integer: self.__class__,
Numeric: Numeric,
+ Float: Float,
+ },
+ operators.truediv: {
+ Integer: Numeric,
+ Numeric: Numeric,
+ Float: Float,
},
- operators.truediv: {Integer: Numeric, Numeric: Numeric},
operators.floordiv: {Integer: self.__class__, Numeric: Numeric},
- operators.sub: {Integer: self.__class__, Numeric: Numeric},
+ operators.sub: {
+ Integer: self.__class__,
+ Numeric: Numeric,
+ Float: Float,
+ },
}
_N = TypeVar("_N", bound=Union[decimal.Decimal, float])
-class Numeric(HasExpressionLookup, TypeEngine[_N]):
+class NumericCommon(HasExpressionLookup, TypeEngineMixin, Generic[_N]):
+ """common mixin for the :class:`.Numeric` and :class:`.Float` types.
+
+
+ .. versionadded:: 2.1
+
+ """
+
+ _default_decimal_return_scale = 10
+
+ if TYPE_CHECKING:
+
+ @util.ro_memoized_property
+ def _type_affinity(self) -> Type[NumericCommon[_N]]: ...
+
+ def __init__(
+ self,
+ *,
+ precision: Optional[int],
+ scale: Optional[int],
+ decimal_return_scale: Optional[int],
+ asdecimal: bool,
+ ):
+ self.precision = precision
+ self.scale = scale
+ self.decimal_return_scale = decimal_return_scale
+ self.asdecimal = asdecimal
+
+ @property
+ def _effective_decimal_return_scale(self):
+ if self.decimal_return_scale is not None:
+ return self.decimal_return_scale
+ elif getattr(self, "scale", None) is not None:
+ return self.scale
+ else:
+ return self._default_decimal_return_scale
+
+ def get_dbapi_type(self, dbapi):
+ return dbapi.NUMBER
+
+ def literal_processor(self, dialect):
+ def process(value):
+ return str(value)
+
+ return process
+
+ @property
+ def python_type(self):
+ if self.asdecimal:
+ return decimal.Decimal
+ else:
+ return float
+
+ def bind_processor(self, dialect):
+ if dialect.supports_native_decimal:
+ return None
+ else:
+ return processors.to_float
+
+ @util.memoized_property
+ def _expression_adaptations(self):
+ return {
+ operators.mul: {
+ Interval: Interval,
+ Numeric: self.__class__,
+ Float: self.__class__,
+ Integer: self.__class__,
+ },
+ operators.truediv: {
+ Numeric: self.__class__,
+ Float: self.__class__,
+ Integer: self.__class__,
+ },
+ operators.add: {
+ Numeric: self.__class__,
+ Float: self.__class__,
+ Integer: self.__class__,
+ },
+ operators.sub: {
+ Numeric: self.__class__,
+ Float: self.__class__,
+ Integer: self.__class__,
+ },
+ }
+
+
+class Numeric(NumericCommon[_N], TypeEngine[_N]):
"""Base for non-integer numeric types, such as
``NUMERIC``, ``FLOAT``, ``DECIMAL``, and other variants.
__visit_name__ = "numeric"
- if TYPE_CHECKING:
-
- @util.ro_memoized_property
- def _type_affinity(self) -> Type[Numeric[_N]]: ...
-
- _default_decimal_return_scale = 10
-
@overload
def __init__(
self: Numeric[decimal.Decimal],
conversion overhead.
"""
- self.precision = precision
- self.scale = scale
- self.decimal_return_scale = decimal_return_scale
- self.asdecimal = asdecimal
-
- @property
- def _effective_decimal_return_scale(self):
- if self.decimal_return_scale is not None:
- return self.decimal_return_scale
- elif getattr(self, "scale", None) is not None:
- return self.scale
- else:
- return self._default_decimal_return_scale
-
- def get_dbapi_type(self, dbapi):
- return dbapi.NUMBER
-
- def literal_processor(self, dialect):
- def process(value):
- return str(value)
-
- return process
+ super().__init__(
+ precision=precision,
+ scale=scale,
+ decimal_return_scale=decimal_return_scale,
+ asdecimal=asdecimal,
+ )
@property
- def python_type(self):
- if self.asdecimal:
- return decimal.Decimal
- else:
- return float
-
- def bind_processor(self, dialect):
- if dialect.supports_native_decimal:
- return None
- else:
- return processors.to_float
+ def _type_affinity(self):
+ return Numeric
def result_processor(self, dialect, coltype):
if self.asdecimal:
else:
return None
- @util.memoized_property
- def _expression_adaptations(self):
- return {
- operators.mul: {
- Interval: Interval,
- Numeric: self.__class__,
- Integer: self.__class__,
- },
- operators.truediv: {
- Numeric: self.__class__,
- Integer: self.__class__,
- },
- operators.add: {Numeric: self.__class__, Integer: self.__class__},
- operators.sub: {Numeric: self.__class__, Integer: self.__class__},
- }
-
-class Float(Numeric[_N]):
+class Float(NumericCommon[_N], TypeEngine[_N]):
"""Type representing floating point types, such as ``FLOAT`` or ``REAL``.
This type returns Python ``float`` objects by default, unless the
as the default for decimal_return_scale, if not otherwise specified.
""" # noqa: E501
- self.precision = precision
- self.asdecimal = asdecimal
- self.decimal_return_scale = decimal_return_scale
+ super().__init__(
+ precision=precision,
+ scale=None,
+ asdecimal=asdecimal,
+ decimal_return_scale=decimal_return_scale,
+ )
+
+ @property
+ def _type_affinity(self):
+ return Float
def result_processor(self, dialect, coltype):
if self.asdecimal:
Time: Time,
},
operators.sub: {Interval: self.__class__},
- operators.mul: {Numeric: self.__class__},
- operators.truediv: {Numeric: self.__class__},
+ operators.mul: {Numeric: self.__class__, Float: self.__class__},
+ operators.truediv: {
+ Numeric: self.__class__,
+ Float: self.__class__,
+ },
}
@util.ro_non_memoized_property
[
sql_types.Integer,
sql_types.Numeric,
+ sql_types.Float,
sql_types.DateTime,
sql_types.Date,
sql_types.Time,
from .sql.sqltypes import NullType as NullType
from .sql.sqltypes import NUMERIC as NUMERIC
from .sql.sqltypes import Numeric as Numeric
+from .sql.sqltypes import NumericCommon as NumericCommon
from .sql.sqltypes import NVARCHAR as NVARCHAR
from .sql.sqltypes import PickleType as PickleType
from .sql.sqltypes import REAL as REAL
from sqlalchemy import DefaultClause
from sqlalchemy import event
from sqlalchemy import exc
+from sqlalchemy import Float
from sqlalchemy import ForeignKey
from sqlalchemy import ForeignKeyConstraint
from sqlalchemy import Index
col = insp.get_columns("t1")[0]
if hasattr(expected, "match"):
assert expected.match(col["default"])
- elif isinstance(datatype_inst, (Integer, Numeric)):
+ elif isinstance(datatype_inst, (Integer, Numeric, Float)):
pattern = re.compile(r"\'?%s\'?" % expected)
assert pattern.match(col["default"])
else:
from sqlalchemy import delete
from sqlalchemy import event
from sqlalchemy import exc
-from sqlalchemy import Float
from sqlalchemy import ForeignKey
from sqlalchemy import inspect
from sqlalchemy import Integer
+from sqlalchemy import Numeric
from sqlalchemy import select
from sqlalchemy import sql
from sqlalchemy import String
metadata,
Column("id", Integer, primary_key=True),
Column("location_id", Integer, ForeignKey(weather_locations.c.id)),
- Column("temperature", Float),
+ Column("temperature", Numeric(asdecimal=False)),
Column("report_time", DateTime, default=datetime.datetime.now),
schema=cls.schema,
)
def fetch_null_from_numeric(self):
return skip_if(("mssql+pyodbc", None, None, "crashes due to bug #351"))
- @property
- def float_is_numeric(self):
- return exclusions.fails_if(["oracle"])
-
@property
def duplicate_key_raises_integrity_error(self):
return exclusions.open()
)
def test_float_illegal_autoinc(self):
- """test that Float is not acceptable if autoincrement=True"""
+ """test that Float is not acceptable if autoincrement=True
+
+ note this changed in 2.1 with #5252 where Numeric/Float were split out
+
+ """
t = Table("t", MetaData(), Column("a", Float, autoincrement=True))
pk = PrimaryKeyConstraint(t.c.a)
t.append_constraint(pk)
with expect_raises_message(
- exc.ArgumentError, "Column type FLOAT with non-zero scale "
+ exc.ArgumentError,
+ "Column type FLOAT on column 't.a' is not compatible "
+ "with autoincrement=True",
):
pk._autoincrement_column,
from sqlalchemy import between
from sqlalchemy import bindparam
from sqlalchemy import exc
+from sqlalchemy import Float
from sqlalchemy import Integer
from sqlalchemy import join
from sqlalchemy import LargeBinary
return testing.combinations(
("integer", Integer),
("boolean", Boolean),
- ("float", Numeric),
+ ("float", Float),
("string", String),
)(fn)