--- /dev/null
+.. 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.
+
.. autoclass:: Over
:members:
+.. autoclass:: SQLColumnExpression
+
.. autoclass:: TextClause
:members:
.. autoclass:: RelationshipProperty
:members:
+.. autoclass:: SQLORMExpression
+
.. autoclass:: Synonym
.. autoclass:: SynonymProperty
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
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
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
@inspection._self_inspects
class QueryableAttribute(
- roles.ExpressionElementRole[_T],
_DeclarativeMapped[_T],
+ SQLORMExpression[_T],
interfaces.InspectionAttr,
interfaces.PropComparator[_T],
roles.JoinTargetRole,
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
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)
__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.
__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.
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
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
@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
:attr:`.TypeEngine.comparator_factory`
"""
-
__slots__ = "prop", "_parententity", "_adapt_to_entity"
__visit_name__ = "orm_prop_comparator"
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
class MappedColumn(
DDLConstraintColumnRole,
- SQLCoreOperations[_T],
_IntrospectsAnnotations,
_MapsColumns[_T],
_DeclarativeMapped[_T],
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
...
+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]")
roles.DMLColumnRole,
roles.DDLConstraintColumnRole,
roles.DDLExpressionRole,
- SQLCoreOperations[_T],
+ SQLColumnExpression[_T],
DQLDMLClauseElement,
):
"""Represent a column-oriented SQL expression suitable for usage in the
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
--- /dev/null
+"""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")