]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
add common base class for all SQL col expression objects
authorMike Bayer <mike_mp@zzzcomputing.com>
Sat, 19 Nov 2022 21:42:22 +0000 (16:42 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 21 Nov 2022 16:55:23 +0000 (11:55 -0500)
Added a new type :class:`.SQLColumnExpression` which may be indicated in
user code to represent any SQL column oriented expression, including both
those based on :class:`.ColumnElement` as well as on ORM
:class:`.QueryableAttribute`. This type is a real class, not an alias, so
can also be used as the foundation for other objects.

Fixes: #8847
Change-Id: I3161bdff1c9f447793fce87864e1774a90cd4146

13 files changed:
doc/build/changelog/unreleased_20/8847.rst [new file with mode: 0644]
doc/build/core/sqlelement.rst
doc/build/orm/internals.rst
lib/sqlalchemy/__init__.py
lib/sqlalchemy/orm/__init__.py
lib/sqlalchemy/orm/attributes.py
lib/sqlalchemy/orm/base.py
lib/sqlalchemy/orm/interfaces.py
lib/sqlalchemy/orm/properties.py
lib/sqlalchemy/sql/__init__.py
lib/sqlalchemy/sql/elements.py
lib/sqlalchemy/sql/expression.py
test/ext/mypy/plain_files/common_sql_element.py [new file with mode: 0644]

diff --git a/doc/build/changelog/unreleased_20/8847.rst b/doc/build/changelog/unreleased_20/8847.rst
new file mode 100644 (file)
index 0000000..b3842ac
--- /dev/null
@@ -0,0 +1,11 @@
+.. change::
+    :tags: usecase, typing
+    :tickets: 8847
+
+    Added a new type :class:`.SQLColumnExpression` which may be indicated in
+    user code to represent any SQL column oriented expression, including both
+    those based on :class:`.ColumnElement` as well as on ORM
+    :class:`.QueryableAttribute`. This type is a real class, not an alias, so
+    can also be used as the foundation for other objects.  An additional
+    ORM-specific subclass :class:`.SQLORMExpression` is also included.
+
index 499f26571a8530ede14b4ada5b58459bbcde7e92..be780adb0ed209facd2b08bfb955b9242d0794ce 100644 (file)
@@ -170,6 +170,8 @@ The classes here are generated using the constructors listed at
 .. autoclass:: Over
    :members:
 
+.. autoclass:: SQLColumnExpression
+
 .. autoclass:: TextClause
    :members:
 
index f0ace43a6f625cdb300c2a2b04b6a8c1fca8ce92..9bb7e83a49097eb41f1d962bce4af925ad45aa98 100644 (file)
@@ -79,6 +79,8 @@ sections, are listed here.
 .. autoclass:: RelationshipProperty
   :members:
 
+.. autoclass:: SQLORMExpression
+
 .. autoclass:: Synonym
 
 .. autoclass:: SynonymProperty
index 55ce29310eaa4f6591fb84218e180596836b18db..ccc3a446d5ce91bd279288e6db648acab652c3fc 100644 (file)
@@ -179,6 +179,7 @@ from .sql.expression import Select as Select
 from .sql.expression import select as select
 from .sql.expression import Selectable as Selectable
 from .sql.expression import SelectBase as SelectBase
+from .sql.expression import SQLColumnExpression as SQLColumnExpression
 from .sql.expression import StatementLambdaElement as StatementLambdaElement
 from .sql.expression import Subquery as Subquery
 from .sql.expression import table as table
index 5e21615155b29f66237df7be5ef7adf7d6996d8f..96acce2ff885b4109404f680bcc523a1cc75497f 100644 (file)
@@ -49,6 +49,7 @@ from .base import Mapped as Mapped
 from .base import NotExtension as NotExtension
 from .base import ORMDescriptor as ORMDescriptor
 from .base import PassiveFlag as PassiveFlag
+from .base import SQLORMExpression as SQLORMExpression
 from .base import WriteOnlyMapped as WriteOnlyMapped
 from .context import FromStatement as FromStatement
 from .context import QueryContext as QueryContext
index 2c77111c1d6c16a1b471b435c6ce2204047c62bb..89beedc47ff4f9ce35c3c5548a2b57c24c6b6aef 100644 (file)
@@ -70,6 +70,7 @@ from .base import PASSIVE_RETURN_NO_VALUE
 from .base import PassiveFlag
 from .base import RELATED_OBJECT_OK  # noqa
 from .base import SQL_OK  # noqa
+from .base import SQLORMExpression
 from .base import state_str
 from .. import event
 from .. import exc
@@ -131,8 +132,8 @@ SelfQueryableAttribute = TypeVar(
 
 @inspection._self_inspects
 class QueryableAttribute(
-    roles.ExpressionElementRole[_T],
     _DeclarativeMapped[_T],
+    SQLORMExpression[_T],
     interfaces.InspectionAttr,
     interfaces.PropComparator[_T],
     roles.JoinTargetRole,
index b46c787996e4a729bff6a5e27d8778a029157421..032364ff429daef560289e9ac4a97dc293384ee7 100644 (file)
@@ -31,7 +31,7 @@ from ._typing import insp_is_mapper
 from .. import exc as sa_exc
 from .. import inspection
 from .. import util
-from ..sql import roles
+from ..sql.elements import SQLColumnExpression
 from ..sql.elements import SQLCoreOperations
 from ..util import FastIntFlag
 from ..util.langhelpers import TypingOnly
@@ -52,6 +52,7 @@ if typing.TYPE_CHECKING:
     from ..sql._typing import _ColumnExpressionArgument
     from ..sql._typing import _InfoType
     from ..sql.elements import ColumnElement
+    from ..sql.operators import OperatorType
 
 _T = TypeVar("_T", bound=Any)
 
@@ -740,9 +741,31 @@ class _MappedAnnotationBase(Generic[_T], TypingOnly):
     __slots__ = ()
 
 
+class SQLORMExpression(
+    SQLORMOperations[_T], SQLColumnExpression[_T], TypingOnly
+):
+    """A type that may be used to indicate any ORM-level attribute or
+    object that acts in place of one, in the context of SQL expression
+    construction.
+
+    :class:`.SQLORMExpression` extends from the Core
+    :class:`.SQLColumnExpression` to add additional SQL methods that are ORM
+    specific, such as :meth:`.PropComparator.of_type`, and is part of the bases
+    for :class:`.InstrumentedAttribute`. It may be used in :pep:`484` typing to
+    indicate arguments or return values that should behave as ORM-level
+    attribute expressions.
+
+    .. versionadded:: 2.0.0b4
+
+
+    """
+
+    __slots__ = ()
+
+
 class Mapped(
+    SQLORMExpression[_T],
     ORMDescriptor[_T],
-    roles.TypedColumnsClauseRole[_T],
     _MappedAnnotationBase[_T],
 ):
     """Represent an ORM mapped attribute on a mapped class.
@@ -830,6 +853,22 @@ class _DeclarativeMapped(Mapped[_T], _MappedAttribute[_T]):
 
     __slots__ = ()
 
+    # MappedSQLExpression, Relationship, Composite etc. dont actually do
+    # SQL expression behavior.  yet there is code that compares them with
+    # __eq__(), __ne__(), etc.   Since #8847 made Mapped even more full
+    # featured including ColumnOperators, we need to have those methods
+    # be no-ops for these objects, so return NotImplemented to fall back
+    # to normal comparison behavior.
+    def operate(self, op: OperatorType, *other: Any, **kwargs: Any) -> Any:
+        return NotImplemented
+
+    __sa_operate__ = operate
+
+    def reverse_operate(
+        self, op: OperatorType, other: Any, **kwargs: Any
+    ) -> Any:
+        return NotImplemented
+
 
 class DynamicMapped(_MappedAnnotationBase[_T]):
     """Represent the ORM mapped attribute type for a "dynamic" relationship.
index e61e82126a8b6f6989af4d17b5357d8b72872cbd..ff003f654702d289c88b06838747fe90cdfe7ba5 100644 (file)
@@ -26,6 +26,7 @@ from typing import Callable
 from typing import cast
 from typing import ClassVar
 from typing import Dict
+from typing import Generic
 from typing import Iterator
 from typing import List
 from typing import NamedTuple
@@ -65,6 +66,7 @@ from ..sql import visitors
 from ..sql.base import _NoArg
 from ..sql.base import ExecutableOption
 from ..sql.cache_key import HasCacheKey
+from ..sql.operators import ColumnOperators
 from ..sql.schema import Column
 from ..sql.type_api import TypeEngine
 from ..util.typing import RODescriptorReference
@@ -595,7 +597,7 @@ class MapperProperty(
 
 
 @inspection._self_inspects
-class PropComparator(SQLORMOperations[_T]):
+class PropComparator(SQLORMOperations[_T], Generic[_T], ColumnOperators):
     r"""Defines SQL operations for ORM mapped attributes.
 
     SQLAlchemy allows for operators to
@@ -676,7 +678,6 @@ class PropComparator(SQLORMOperations[_T]):
         :attr:`.TypeEngine.comparator_factory`
 
     """
-
     __slots__ = "prop", "_parententity", "_adapt_to_entity"
 
     __visit_name__ = "orm_prop_comparator"
index c1da267f4dca1ceb8f989daf02856961141b64e3..785a1a098884e7699340f64b92ef6866e17ead18 100644 (file)
@@ -47,7 +47,6 @@ from ..sql import coercions
 from ..sql import roles
 from ..sql import sqltypes
 from ..sql.base import _NoArg
-from ..sql.elements import SQLCoreOperations
 from ..sql.roles import DDLConstraintColumnRole
 from ..sql.schema import Column
 from ..sql.schema import SchemaConst
@@ -500,7 +499,6 @@ class MappedSQLExpression(ColumnProperty[_T], _DeclarativeMapped[_T]):
 
 class MappedColumn(
     DDLConstraintColumnRole,
-    SQLCoreOperations[_T],
     _IntrospectsAnnotations,
     _MapsColumns[_T],
     _DeclarativeMapped[_T],
index 5702d6c250c022e70c1d4aaea0f27964ed6d2d88..8dae9c3f56124ad1c4286310dba29fcf4ac2460c 100644 (file)
@@ -82,6 +82,7 @@ from .expression import Select as Select
 from .expression import select as select
 from .expression import Selectable as Selectable
 from .expression import SelectLabelStyle as SelectLabelStyle
+from .expression import SQLColumnExpression as SQLColumnExpression
 from .expression import StatementLambdaElement as StatementLambdaElement
 from .expression import Subquery as Subquery
 from .expression import table as table
index d9a1a935802b1b453f37fdf0531bd167aff12373..914d2b32634a8d66bc9fa8bfb6d67b5da1d50a77 100644 (file)
@@ -1115,6 +1115,26 @@ class SQLCoreOperations(Generic[_T], ColumnOperators, TypingOnly):
             ...
 
 
+class SQLColumnExpression(
+    SQLCoreOperations[_T], roles.ExpressionElementRole[_T], TypingOnly
+):
+    """A type that may be used to indicate any SQL column element or object
+    that acts in place of one.
+
+    :class:`.SQLColumnExpression` is a base of
+    :class:`.ColumnElement`, as well as within the bases of ORM elements
+    such as :class:`.InstrumentedAttribute`, and may be used in :pep:`484`
+    typing to indicate arguments or return values that should behave
+    as column expressions.
+
+    .. versionadded:: 2.0.0b4
+
+
+    """
+
+    __slots__ = ()
+
+
 _SQO = SQLCoreOperations
 
 SelfColumnElement = TypeVar("SelfColumnElement", bound="ColumnElement[Any]")
@@ -1131,7 +1151,7 @@ class ColumnElement(
     roles.DMLColumnRole,
     roles.DDLConstraintColumnRole,
     roles.DDLExpressionRole,
-    SQLCoreOperations[_T],
+    SQLColumnExpression[_T],
     DQLDMLClauseElement,
 ):
     """Represent a column-oriented SQL expression suitable for usage in the
index d08bbf4eb3fe2585e498a0229feaa94e1ffc44de..2498bfb377353b7800f993f1d9c51819a8905062 100644 (file)
@@ -95,6 +95,7 @@ from .elements import quoted_name as quoted_name
 from .elements import ReleaseSavepointClause as ReleaseSavepointClause
 from .elements import RollbackToSavepointClause as RollbackToSavepointClause
 from .elements import SavepointClause as SavepointClause
+from .elements import SQLColumnExpression as SQLColumnExpression
 from .elements import TextClause as TextClause
 from .elements import True_ as True_
 from .elements import Tuple as Tuple
diff --git a/test/ext/mypy/plain_files/common_sql_element.py b/test/ext/mypy/plain_files/common_sql_element.py
new file mode 100644 (file)
index 0000000..af36c85
--- /dev/null
@@ -0,0 +1,94 @@
+"""tests for #8847
+
+we want to assert that SQLColumnExpression can be used to represent
+all SQL expressions generically, across Core and ORM, without using
+unions.
+
+"""
+
+
+from __future__ import annotations
+
+from sqlalchemy import Column
+from sqlalchemy import Integer
+from sqlalchemy import MetaData
+from sqlalchemy import select
+from sqlalchemy import SQLColumnExpression
+from sqlalchemy import String
+from sqlalchemy import Table
+from sqlalchemy.orm import DeclarativeBase
+from sqlalchemy.orm import Mapped
+from sqlalchemy.orm import mapped_column
+
+
+class Base(DeclarativeBase):
+    pass
+
+
+class User(Base):
+    __tablename__ = "a"
+
+    id: Mapped[int] = mapped_column(primary_key=True)
+    email: Mapped[str]
+
+
+user_table = Table(
+    "user_table", MetaData(), Column("id", Integer), Column("email", String)
+)
+
+
+def receives_str_col_expr(expr: SQLColumnExpression[str]) -> None:
+    pass
+
+
+def receives_bool_col_expr(expr: SQLColumnExpression[bool]) -> None:
+    pass
+
+
+def orm_expr(email: str) -> SQLColumnExpression[bool]:
+    return User.email == email
+
+
+def core_expr(email: str) -> SQLColumnExpression[bool]:
+    email_col: Column[str] = user_table.c.email
+    return email_col == email
+
+
+e1 = orm_expr("hi")
+
+# EXPECTED_TYPE: SQLColumnExpression[bool]
+reveal_type(e1)
+
+stmt = select(e1)
+
+# EXPECTED_TYPE: Select[Tuple[bool]]
+reveal_type(stmt)
+
+stmt = stmt.where(e1)
+
+
+e2 = core_expr("hi")
+
+# EXPECTED_TYPE: SQLColumnExpression[bool]
+reveal_type(e2)
+
+stmt = select(e2)
+
+# EXPECTED_TYPE: Select[Tuple[bool]]
+reveal_type(stmt)
+
+stmt = stmt.where(e2)
+
+
+receives_str_col_expr(User.email)
+receives_str_col_expr(User.email + "some expr")
+receives_str_col_expr(User.email.label("x"))
+receives_str_col_expr(User.email.label("x"))
+
+receives_bool_col_expr(e1)
+receives_bool_col_expr(e1.label("x"))
+receives_bool_col_expr(User.email == "x")
+
+receives_bool_col_expr(e2)
+receives_bool_col_expr(e2.label("x"))
+receives_bool_col_expr(user_table.c.email == "x")