# orm/relationships.py # Copyright (C) 2005-2023 the SQLAlchemy authors and contributors # # # This module is part of SQLAlchemy and is released under # the MIT License: https://www.opensource.org/licenses/mit-license.php """Heuristics related to join conditions as used in :func:`_orm.relationship`. Provides the :class:`.JoinCondition` object, which encapsulates SQL annotation and aliasing behavior focused on the `primaryjoin` and `secondaryjoin` aspects of :func:`_orm.relationship`. """ from __future__ import annotations import collections from collections import abc import dataclasses import inspect as _py_inspect import re import typing from typing import Any from typing import Callable from typing import cast from typing import Collection from typing import Dict from typing import Generic from typing import Iterable from typing import Iterator from typing import List from typing import NamedTuple from typing import NoReturn from typing import Optional from typing import Sequence from typing import Set from typing import Tuple from typing import Type from typing import TypeVar from typing import Union import weakref from . import attributes from . import strategy_options from ._typing import insp_is_aliased_class from ._typing import is_has_collection_adapter from .base import _DeclarativeMapped from .base import _is_mapped_class from .base import class_mapper from .base import DynamicMapped from .base import LoaderCallableStatus from .base import PassiveFlag from .base import state_str from .base import WriteOnlyMapped from .interfaces import _AttributeOptions from .interfaces import _IntrospectsAnnotations from .interfaces import MANYTOMANY from .interfaces import MANYTOONE from .interfaces import ONETOMANY from .interfaces import PropComparator from .interfaces import RelationshipDirection from .interfaces import StrategizedProperty from .util import _orm_annotate from .util import _orm_deannotate from .util import CascadeOptions from .. import exc as sa_exc from .. import Exists from .. import log from .. import schema from .. import sql from .. import util from ..inspection import inspect from ..sql import coercions from ..sql import expression from ..sql import operators from ..sql import roles from ..sql import visitors from ..sql._typing import _ColumnExpressionArgument from ..sql._typing import _HasClauseElement from ..sql.elements import ColumnClause from ..sql.elements import ColumnElement from ..sql.util import _deep_deannotate from ..sql.util import _shallow_annotate from ..sql.util import adapt_criterion_to_null from ..sql.util import ClauseAdapter from ..sql.util import join_condition from ..sql.util import selectables_overlap from ..sql.util import visit_binary_product from ..util.typing import de_optionalize_union_types from ..util.typing import Literal from ..util.typing import resolve_name_to_real_class_name if typing.TYPE_CHECKING: from ._typing import _EntityType from ._typing import _ExternalEntityType from ._typing import _IdentityKeyType from ._typing import _InstanceDict from ._typing import _InternalEntityType from ._typing import _O from ._typing import _RegistryType from .base import Mapped from .clsregistry import _class_resolver from .clsregistry import _ModNS from .decl_base import _ClassScanMapperConfig from .dependency import DependencyProcessor from .mapper import Mapper from .query import Query from .session import Session from .state import InstanceState from .strategies import LazyLoader from .util import AliasedClass from .util import AliasedInsp from ..sql._typing import _CoreAdapterProto from ..sql._typing import _EquivalentColumnMap from ..sql._typing import _InfoType from ..sql.annotation import _AnnotationDict from ..sql.elements import BinaryExpression from ..sql.elements import BindParameter from ..sql.elements import ClauseElement from ..sql.schema import Table from ..sql.selectable import FromClause from ..util.typing import _AnnotationScanType from ..util.typing import RODescriptorReference _T = TypeVar("_T", bound=Any) _T1 = TypeVar("_T1", bound=Any) _T2 = TypeVar("_T2", bound=Any) _PT = TypeVar("_PT", bound=Any) _PT2 = TypeVar("_PT2", bound=Any) _RelationshipArgumentType = Union[ str, Type[_T], Callable[[], Type[_T]], "Mapper[_T]", "AliasedClass[_T]", Callable[[], "Mapper[_T]"], Callable[[], "AliasedClass[_T]"], ] _LazyLoadArgumentType = Literal[ "select", "joined", "selectin", "subquery", "raise", "raise_on_sql", "noload", "immediate", "write_only", "dynamic", True, False, None, ] _RelationshipJoinConditionArgument = Union[ str, _ColumnExpressionArgument[bool] ] _RelationshipSecondaryArgument = Union[ "FromClause", str, Callable[[], "FromClause"] ] _ORMOrderByArgument = Union[ Literal[False], str, _ColumnExpressionArgument[Any], Callable[[], Iterable[ColumnElement[Any]]], Iterable[Union[str, _ColumnExpressionArgument[Any]]], ] ORMBackrefArgument = Union[str, Tuple[str, Dict[str, Any]]] _ORMColCollectionElement = Union[ ColumnClause[Any], _HasClauseElement, roles.DMLColumnRole, "Mapped[Any]" ] _ORMColCollectionArgument = Union[ str, Sequence[_ORMColCollectionElement], Callable[[], Sequence[_ORMColCollectionElement]], Callable[[], _ORMColCollectionElement], _ORMColCollectionElement, ] _CEA = TypeVar("_CEA", bound=_ColumnExpressionArgument[Any]) _CE = TypeVar("_CE", bound="ColumnElement[Any]") _ColumnPairIterable = Iterable[Tuple[ColumnElement[Any], ColumnElement[Any]]] _ColumnPairs = Sequence[Tuple[ColumnElement[Any], ColumnElement[Any]]] _MutableColumnPairs = List[Tuple[ColumnElement[Any], ColumnElement[Any]]] def remote(expr: _CEA) -> _CEA: """Annotate a portion of a primaryjoin expression with a 'remote' annotation. See the section :ref:`relationship_custom_foreign` for a description of use. .. seealso:: :ref:`relationship_custom_foreign` :func:`.foreign` """ return _annotate_columns( # type: ignore coercions.expect(roles.ColumnArgumentRole, expr), {"remote": True} ) def foreign(expr: _CEA) -> _CEA: """Annotate a portion of a primaryjoin expression with a 'foreign' annotation. See the section :ref:`relationship_custom_foreign` for a description of use. .. seealso:: :ref:`relationship_custom_foreign` :func:`.remote` """ return _annotate_columns( # type: ignore coercions.expect(roles.ColumnArgumentRole, expr), {"foreign": True} ) @dataclasses.dataclass class _RelationshipArg(Generic[_T1, _T2]): """stores a user-defined parameter value that must be resolved and parsed later at mapper configuration time. """ __slots__ = "name", "argument", "resolved" name: str argument: _T1 resolved: Optional[_T2] def _is_populated(self) -> bool: return self.argument is not None def _resolve_against_registry( self, clsregistry_resolver: Callable[[str, bool], _class_resolver] ) -> None: attr_value = self.argument if isinstance(attr_value, str): self.resolved = clsregistry_resolver( attr_value, self.name == "secondary" )() elif callable(attr_value) and not _is_mapped_class(attr_value): self.resolved = attr_value() else: self.resolved = attr_value class _RelationshipArgs(NamedTuple): """stores user-passed parameters that are resolved at mapper configuration time. """ secondary: _RelationshipArg[ Optional[_RelationshipSecondaryArgument], Optional[FromClause], ] primaryjoin: _RelationshipArg[ Optional[_RelationshipJoinConditionArgument], Optional[ColumnElement[Any]], ] secondaryjoin: _RelationshipArg[ Optional[_RelationshipJoinConditionArgument], Optional[ColumnElement[Any]], ] order_by: _RelationshipArg[ _ORMOrderByArgument, Union[Literal[None, False], Tuple[ColumnElement[Any], ...]], ] foreign_keys: _RelationshipArg[ Optional[_ORMColCollectionArgument], Set[ColumnElement[Any]] ] remote_side: _RelationshipArg[ Optional[_ORMColCollectionArgument], Set[ColumnElement[Any]] ] @log.class_logger class RelationshipProperty( _IntrospectsAnnotations, StrategizedProperty[_T], log.Identified ): """Describes an object property that holds a single item or list of items that correspond to a related database table. Public constructor is the :func:`_orm.relationship` function. .. seealso:: :ref:`relationship_config_toplevel` """ strategy_wildcard_key = strategy_options._RELATIONSHIP_TOKEN inherit_cache = True """:meta private:""" _links_to_entity = True _is_relationship = True _overlaps: Sequence[str] _lazy_strategy: LazyLoader _persistence_only = dict( passive_deletes=False, passive_updates=True, enable_typechecks=True, active_history=False, cascade_backrefs=False, ) _dependency_processor: Optional[DependencyProcessor] = None primaryjoin: ColumnElement[bool] secondaryjoin: Optional[ColumnElement[bool]] secondary: Optional[FromClause] _join_condition: JoinCondition order_by: Union[Literal[False], Tuple[ColumnElement[Any], ...]] _user_defined_foreign_keys: Set[ColumnElement[Any]] _calculated_foreign_keys: Set[ColumnElement[Any]] remote_side: Set[ColumnElement[Any]] local_columns: Set[ColumnElement[Any]] synchronize_pairs: _ColumnPairs secondary_synchronize_pairs: Optional[_ColumnPairs] local_remote_pairs: Optional[_ColumnPairs] direction: RelationshipDirection _init_args: _RelationshipArgs def __init__( self, argument: Optional[_RelationshipArgumentType[_T]] = None, secondary: Optional[_RelationshipSecondaryArgument] = None, *, uselist: Optional[bool] = None, collection_class: Optional[ Union[Type[Collection[Any]], Callable[[], Collection[Any]]] ] = None, primaryjoin: Optional[_RelationshipJoinConditionArgument] = None, secondaryjoin: Optional[_RelationshipJoinConditionArgument] = None, back_populates: Optional[str] = None, order_by: _ORMOrderByArgument = False, backref: Optional[ORMBackrefArgument] = None, overlaps: Optional[str] = None, post_update: bool = False, cascade: str = "save-update, merge", viewonly: bool = False, attribute_options: Optional[_AttributeOptions] = None, lazy: _LazyLoadArgumentType = "select", passive_deletes: Union[Literal["all"], bool] = False, passive_updates: bool = True, active_history: bool = False, enable_typechecks: bool = True, foreign_keys: Optional[_ORMColCollectionArgument] = None, remote_side: Optional[_ORMColCollectionArgument] = None, join_depth: Optional[int] = None, comparator_factory: Optional[ Type[RelationshipProperty.Comparator[Any]] ] = None, single_parent: bool = False, innerjoin: bool = False, distinct_target_key: Optional[bool] = None, load_on_pending: bool = False, query_class: Optional[Type[Query[Any]]] = None, info: Optional[_InfoType] = None, omit_join: Literal[None, False] = None, sync_backref: Optional[bool] = None, doc: Optional[str] = None, bake_queries: Literal[True] = True, cascade_backrefs: Literal[False] = False, _local_remote_pairs: Optional[_ColumnPairs] = None, _legacy_inactive_history_style: bool = False, ): super().__init__(attribute_options=attribute_options) self.uselist = uselist self.argument = argument self._init_args = _RelationshipArgs( _RelationshipArg("secondary", secondary, None), _RelationshipArg("primaryjoin", primaryjoin, None), _RelationshipArg("secondaryjoin", secondaryjoin, None), _RelationshipArg("order_by", order_by, None), _RelationshipArg("foreign_keys", foreign_keys, None), _RelationshipArg("remote_side", remote_side, None), ) self.post_update = post_update self.viewonly = viewonly if viewonly: self._warn_for_persistence_only_flags( passive_deletes=passive_deletes, passive_updates=passive_updates, enable_typechecks=enable_typechecks, active_history=active_history, cascade_backrefs=cascade_backrefs, ) if viewonly and sync_backref: raise sa_exc.ArgumentError( "sync_backref and viewonly cannot both be True" ) self.sync_backref = sync_backref self.lazy = lazy self.single_parent = single_parent self.collection_class = collection_class self.passive_deletes = passive_deletes if cascade_backrefs: raise sa_exc.ArgumentError( "The 'cascade_backrefs' parameter passed to " "relationship() may only be set to False." ) self.passive_updates = passive_updates self.enable_typechecks = enable_typechecks self.query_class = query_class self.innerjoin = innerjoin self.distinct_target_key = distinct_target_key self.doc = doc self.active_history = active_history self._legacy_inactive_history_style = _legacy_inactive_history_style self.join_depth = join_depth if omit_join: util.warn( "setting omit_join to True is not supported; selectin " "loading of this relationship may not work correctly if this " "flag is set explicitly. omit_join optimization is " "automatically detected for conditions under which it is " "supported." ) self.omit_join = omit_join self.local_remote_pairs = _local_remote_pairs self.load_on_pending = load_on_pending self.comparator_factory = ( comparator_factory or RelationshipProperty.Comparator ) util.set_creation_order(self) if info is not None: self.info.update(info) self.strategy_key = (("lazy", self.lazy),) self._reverse_property: Set[RelationshipProperty[Any]] = set() if overlaps: self._overlaps = set(re.split(r"\s*,\s*", overlaps)) # type: ignore # noqa: E501 else: self._overlaps = () # mypy ignoring the @property setter self.cascade = cascade # type: ignore self.back_populates = back_populates if self.back_populates: if backref: raise sa_exc.ArgumentError( "backref and back_populates keyword arguments " "are mutually exclusive" ) self.backref = None else: self.backref = backref def _warn_for_persistence_only_flags(self, **kw: Any) -> None: for k, v in kw.items(): if v != self._persistence_only[k]: # we are warning here rather than warn deprecated as this is a # configuration mistake, and Python shows regular warnings more # aggressively than deprecation warnings by default. Unlike the # case of setting viewonly with cascade, the settings being # warned about here are not actively doing the wrong thing # against viewonly=True, so it is not as urgent to have these # raise an error. util.warn( "Setting %s on relationship() while also " "setting viewonly=True does not make sense, as a " "viewonly=True relationship does not perform persistence " "operations. This configuration may raise an error " "in a future release." % (k,) ) def instrument_class(self, mapper: Mapper[Any]) -> None: attributes.register_descriptor( mapper.class_, self.key, comparator=self.comparator_factory(self, mapper), parententity=mapper, doc=self.doc, ) class Comparator(util.MemoizedSlots, PropComparator[_PT]): """Produce boolean, comparison, and other operators for :class:`.RelationshipProperty` attributes. See the documentation for :class:`.PropComparator` for a brief overview of ORM level operator definition. .. seealso:: :class:`.PropComparator` :class:`.ColumnProperty.Comparator` :class:`.ColumnOperators` :ref:`types_operators` :attr:`.TypeEngine.comparator_factory` """ __slots__ = ( "entity", "mapper", "property", "_of_type", "_extra_criteria", ) prop: RODescriptorReference[RelationshipProperty[_PT]] _of_type: Optional[_EntityType[_PT]] def __init__( self, prop: RelationshipProperty[_PT], parentmapper: _InternalEntityType[Any], adapt_to_entity: Optional[AliasedInsp[Any]] = None, of_type: Optional[_EntityType[_PT]] = None, extra_criteria: Tuple[ColumnElement[bool], ...] = (), ): """Construction of :class:`.RelationshipProperty.Comparator` is internal to the ORM's attribute mechanics. """ self.prop = prop self._parententity = parentmapper self._adapt_to_entity = adapt_to_entity if of_type: self._of_type = of_type else: self._of_type = None self._extra_criteria = extra_criteria def adapt_to_entity( self, adapt_to_entity: AliasedInsp[Any] ) -> RelationshipProperty.Comparator[Any]: return self.__class__( self.prop, self._parententity, adapt_to_entity=adapt_to_entity, of_type=self._of_type, ) entity: _InternalEntityType[_PT] """The target entity referred to by this :class:`.RelationshipProperty.Comparator`. This is either a :class:`_orm.Mapper` or :class:`.AliasedInsp` object. This is the "target" or "remote" side of the :func:`_orm.relationship`. """ mapper: Mapper[_PT] """The target :class:`_orm.Mapper` referred to by this :class:`.RelationshipProperty.Comparator`. This is the "target" or "remote" side of the :func:`_orm.relationship`. """ def _memoized_attr_entity(self) -> _InternalEntityType[_PT]: if self._of_type: return inspect(self._of_type) # type: ignore else: return self.prop.entity def _memoized_attr_mapper(self) -> Mapper[_PT]: return self.entity.mapper def _source_selectable(self) -> FromClause: if self._adapt_to_entity: return self._adapt_to_entity.selectable else: return self.property.parent._with_polymorphic_selectable def __clause_element__(self) -> ColumnElement[bool]: adapt_from = self._source_selectable() if self._of_type: of_type_entity = inspect(self._of_type) else: of_type_entity = None ( pj, sj, source, dest, secondary, target_adapter, ) = self.prop._create_joins( source_selectable=adapt_from, source_polymorphic=True, of_type_entity=of_type_entity, alias_secondary=True, extra_criteria=self._extra_criteria, ) if sj is not None: return pj & sj else: return pj def of_type(self, class_: _EntityType[Any]) -> PropComparator[_PT]: r"""Redefine this object in terms of a polymorphic subclass. See :meth:`.PropComparator.of_type` for an example. """ return RelationshipProperty.Comparator( self.prop, self._parententity, adapt_to_entity=self._adapt_to_entity, of_type=class_, extra_criteria=self._extra_criteria, ) def and_( self, *criteria: _ColumnExpressionArgument[bool] ) -> PropComparator[Any]: """Add AND criteria. See :meth:`.PropComparator.and_` for an example. .. versionadded:: 1.4 """ exprs = tuple( coercions.expect(roles.WhereHavingRole, clause) for clause in util.coerce_generator_arg(criteria) ) return RelationshipProperty.Comparator( self.prop, self._parententity, adapt_to_entity=self._adapt_to_entity, of_type=self._of_type, extra_criteria=self._extra_criteria + exprs, ) def in_(self, other: Any) -> NoReturn: """Produce an IN clause - this is not implemented for :func:`_orm.relationship`-based attributes at this time. """ raise NotImplementedError( "in_() not yet supported for " "relationships. For a simple " "many-to-one, use in_() against " "the set of foreign key values." ) # https://github.com/python/mypy/issues/4266 __hash__ = None # type: ignore def __eq__(self, other: Any) -> ColumnElement[bool]: # type: ignore[override] # noqa: E501 """Implement the ``==`` operator. In a many-to-one context, such as:: MyClass.some_prop == this will typically produce a clause such as:: mytable.related_id == Where ```` is the primary key of the given object. The ``==`` operator provides partial functionality for non- many-to-one comparisons: * Comparisons against collections are not supported. Use :meth:`~.Relationship.Comparator.contains`. * Compared to a scalar one-to-many, will produce a clause that compares the target columns in the parent to the given target. * Compared to a scalar many-to-many, an alias of the association table will be rendered as well, forming a natural join that is part of the main body of the query. This will not work for queries that go beyond simple AND conjunctions of comparisons, such as those which use OR. Use explicit joins, outerjoins, or :meth:`~.Relationship.Comparator.has` for more comprehensive non-many-to-one scalar membership tests. * Comparisons against ``None`` given in a one-to-many or many-to-many context produce a NOT EXISTS clause. """ if other is None or isinstance(other, expression.Null): if self.property.direction in [ONETOMANY, MANYTOMANY]: return ~self._criterion_exists() else: return _orm_annotate( self.property._optimized_compare( None, adapt_source=self.adapter ) ) elif self.property.uselist: raise sa_exc.InvalidRequestError( "Can't compare a collection to an object or collection; " "use contains() to test for membership." ) else: return _orm_annotate( self.property._optimized_compare( other, adapt_source=self.adapter ) ) def _criterion_exists( self, criterion: Optional[_ColumnExpressionArgument[bool]] = None, **kwargs: Any, ) -> Exists: where_criteria = ( coercions.expect(roles.WhereHavingRole, criterion) if criterion is not None else None ) if getattr(self, "_of_type", None): info: Optional[_InternalEntityType[Any]] = inspect( self._of_type ) assert info is not None target_mapper, to_selectable, is_aliased_class = ( info.mapper, info.selectable, info.is_aliased_class, ) if self.property._is_self_referential and not is_aliased_class: to_selectable = to_selectable._anonymous_fromclause() single_crit = target_mapper._single_table_criterion if single_crit is not None: if where_criteria is not None: where_criteria = single_crit & where_criteria else: where_criteria = single_crit else: is_aliased_class = False to_selectable = None if self.adapter: source_selectable = self._source_selectable() else: source_selectable = None ( pj, sj, source, dest, secondary, target_adapter, ) = self.property._create_joins( dest_selectable=to_selectable, source_selectable=source_selectable, ) for k in kwargs: crit = getattr(self.property.mapper.class_, k) == kwargs[k] if where_criteria is None: where_criteria = crit else: where_criteria = where_criteria & crit # annotate the *local* side of the join condition, in the case # of pj + sj this is the full primaryjoin, in the case of just # pj its the local side of the primaryjoin. if sj is not None: j = _orm_annotate(pj) & sj else: j = _orm_annotate(pj, exclude=self.property.remote_side) if ( where_criteria is not None and target_adapter and not is_aliased_class ): # limit this adapter to annotated only? where_criteria = target_adapter.traverse(where_criteria) # only have the "joined left side" of what we # return be subject to Query adaption. The right # side of it is used for an exists() subquery and # should not correlate or otherwise reach out # to anything in the enclosing query. if where_criteria is not None: where_criteria = where_criteria._annotate( {"no_replacement_traverse": True} ) crit = j & sql.True_._ifnone(where_criteria) if secondary is not None: ex = ( sql.exists(1) .where(crit) .select_from(dest, secondary) .correlate_except(dest, secondary) ) else: ex = ( sql.exists(1) .where(crit) .select_from(dest) .correlate_except(dest) ) return ex def any( self, criterion: Optional[_ColumnExpressionArgument[bool]] = None, **kwargs: Any, ) -> ColumnElement[bool]: """Produce an expression that tests a collection against particular criterion, using EXISTS. An expression like:: session.query(MyClass).filter( MyClass.somereference.any(SomeRelated.x==2) ) Will produce a query like:: SELECT * FROM my_table WHERE EXISTS (SELECT 1 FROM related WHERE related.my_id=my_table.id AND related.x=2) Because :meth:`~.Relationship.Comparator.any` uses a correlated subquery, its performance is not nearly as good when compared against large target tables as that of using a join. :meth:`~.Relationship.Comparator.any` is particularly useful for testing for empty collections:: session.query(MyClass).filter( ~MyClass.somereference.any() ) will produce:: SELECT * FROM my_table WHERE NOT (EXISTS (SELECT 1 FROM related WHERE related.my_id=my_table.id)) :meth:`~.Relationship.Comparator.any` is only valid for collections, i.e. a :func:`_orm.relationship` that has ``uselist=True``. For scalar references, use :meth:`~.Relationship.Comparator.has`. """ if not self.property.uselist: raise sa_exc.InvalidRequestError( "'any()' not implemented for scalar " "attributes. Use has()." ) return self._criterion_exists(criterion, **kwargs) def has( self, criterion: Optional[_ColumnExpressionArgument[bool]] = None, **kwargs: Any, ) -> ColumnElement[bool]: """Produce an expression that tests a scalar reference against particular criterion, using EXISTS. An expression like:: session.query(MyClass).filter( MyClass.somereference.has(SomeRelated.x==2) ) Will produce a query like:: SELECT * FROM my_table WHERE EXISTS (SELECT 1 FROM related WHERE related.id==my_table.related_id AND related.x=2) Because :meth:`~.Relationship.Comparator.has` uses a correlated subquery, its performance is not nearly as good when compared against large target tables as that of using a join. :meth:`~.Relationship.Comparator.has` is only valid for scalar references, i.e. a :func:`_orm.relationship` that has ``uselist=False``. For collection references, use :meth:`~.Relationship.Comparator.any`. """ if self.property.uselist: raise sa_exc.InvalidRequestError( "'has()' not implemented for collections. " "Use any()." ) return self._criterion_exists(criterion, **kwargs) def contains( self, other: _ColumnExpressionArgument[Any], **kwargs: Any ) -> ColumnElement[bool]: """Return a simple expression that tests a collection for containment of a particular item. :meth:`~.Relationship.Comparator.contains` is only valid for a collection, i.e. a :func:`_orm.relationship` that implements one-to-many or many-to-many with ``uselist=True``. When used in a simple one-to-many context, an expression like:: MyClass.contains(other) Produces a clause like:: mytable.id == Where ```` is the value of the foreign key attribute on ``other`` which refers to the primary key of its parent object. From this it follows that :meth:`~.Relationship.Comparator.contains` is very useful when used with simple one-to-many operations. For many-to-many operations, the behavior of :meth:`~.Relationship.Comparator.contains` has more caveats. The association table will be rendered in the statement, producing an "implicit" join, that is, includes multiple tables in the FROM clause which are equated in the WHERE clause:: query(MyClass).filter(MyClass.contains(other)) Produces a query like:: SELECT * FROM my_table, my_association_table AS my_association_table_1 WHERE my_table.id = my_association_table_1.parent_id AND my_association_table_1.child_id = Where ```` would be the primary key of ``other``. From the above, it is clear that :meth:`~.Relationship.Comparator.contains` will **not** work with many-to-many collections when used in queries that move beyond simple AND conjunctions, such as multiple :meth:`~.Relationship.Comparator.contains` expressions joined by OR. In such cases subqueries or explicit "outer joins" will need to be used instead. See :meth:`~.Relationship.Comparator.any` for a less-performant alternative using EXISTS, or refer to :meth:`_query.Query.outerjoin` as well as :ref:`orm_queryguide_joins` for more details on constructing outer joins. kwargs may be ignored by this operator but are required for API conformance. """ if not self.prop.uselist: raise sa_exc.InvalidRequestError( "'contains' not implemented for scalar " "attributes. Use ==" ) clause = self.prop._optimized_compare( other, adapt_source=self.adapter ) if self.prop.secondaryjoin is not None: clause.negation_clause = self.__negated_contains_or_equals( other ) return clause def __negated_contains_or_equals( self, other: Any ) -> ColumnElement[bool]: if self.prop.direction == MANYTOONE: state = attributes.instance_state(other) def state_bindparam( local_col: ColumnElement[Any], state: InstanceState[Any], remote_col: ColumnElement[Any], ) -> BindParameter[Any]: dict_ = state.dict return sql.bindparam( local_col.key, type_=local_col.type, unique=True, callable_=self.prop._get_attr_w_warn_on_none( self.prop.mapper, state, dict_, remote_col ), ) def adapt(col: _CE) -> _CE: if self.adapter: return self.adapter(col) else: return col if self.property._use_get: return sql.and_( *[ sql.or_( adapt(x) != state_bindparam(adapt(x), state, y), adapt(x) == None, ) for (x, y) in self.property.local_remote_pairs ] ) criterion = sql.and_( *[ x == y for (x, y) in zip( self.property.mapper.primary_key, self.property.mapper.primary_key_from_instance(other), ) ] ) return ~self._criterion_exists(criterion) def __ne__(self, other: Any) -> ColumnElement[bool]: # type: ignore[override] # noqa: E501 """Implement the ``!=`` operator. In a many-to-one context, such as:: MyClass.some_prop != This will typically produce a clause such as:: mytable.related_id != Where ```` is the primary key of the given object. The ``!=`` operator provides partial functionality for non- many-to-one comparisons: * Comparisons against collections are not supported. Use :meth:`~.Relationship.Comparator.contains` in conjunction with :func:`_expression.not_`. * Compared to a scalar one-to-many, will produce a clause that compares the target columns in the parent to the given target. * Compared to a scalar many-to-many, an alias of the association table will be rendered as well, forming a natural join that is part of the main body of the query. This will not work for queries that go beyond simple AND conjunctions of comparisons, such as those which use OR. Use explicit joins, outerjoins, or :meth:`~.Relationship.Comparator.has` in conjunction with :func:`_expression.not_` for more comprehensive non-many-to-one scalar membership tests. * Comparisons against ``None`` given in a one-to-many or many-to-many context produce an EXISTS clause. """ if other is None or isinstance(other, expression.Null): if self.property.direction == MANYTOONE: return _orm_annotate( ~self.property._optimized_compare( None, adapt_source=self.adapter ) ) else: return self._criterion_exists() elif self.property.uselist: raise sa_exc.InvalidRequestError( "Can't compare a collection" " to an object or collection; use " "contains() to test for membership." ) else: return _orm_annotate(self.__negated_contains_or_equals(other)) def _memoized_attr_property(self) -> RelationshipProperty[_PT]: self.prop.parent._check_configure() return self.prop def _with_parent( self, instance: object, alias_secondary: bool = True, from_entity: Optional[_EntityType[Any]] = None, ) -> ColumnElement[bool]: assert instance is not None adapt_source: Optional[_CoreAdapterProto] = None if from_entity is not None: insp: Optional[_InternalEntityType[Any]] = inspect(from_entity) assert insp is not None if insp_is_aliased_class(insp): adapt_source = insp._adapter.adapt_clause return self._optimized_compare( instance, value_is_parent=True, adapt_source=adapt_source, alias_secondary=alias_secondary, ) def _optimized_compare( self, state: Any, value_is_parent: bool = False, adapt_source: Optional[_CoreAdapterProto] = None, alias_secondary: bool = True, ) -> ColumnElement[bool]: if state is not None: try: state = inspect(state) except sa_exc.NoInspectionAvailable: state = None if state is None or not getattr(state, "is_instance", False): raise sa_exc.ArgumentError( "Mapped instance expected for relationship " "comparison to object. Classes, queries and other " "SQL elements are not accepted in this context; for " "comparison with a subquery, " "use %s.has(**criteria)." % self ) reverse_direction = not value_is_parent if state is None: return self._lazy_none_clause( reverse_direction, adapt_source=adapt_source ) if not reverse_direction: criterion, bind_to_col = ( self._lazy_strategy._lazywhere, self._lazy_strategy._bind_to_col, ) else: criterion, bind_to_col = ( self._lazy_strategy._rev_lazywhere, self._lazy_strategy._rev_bind_to_col, ) if reverse_direction: mapper = self.mapper else: mapper = self.parent dict_ = attributes.instance_dict(state.obj()) def visit_bindparam(bindparam: BindParameter[Any]) -> None: if bindparam._identifying_key in bind_to_col: bindparam.callable = self._get_attr_w_warn_on_none( mapper, state, dict_, bind_to_col[bindparam._identifying_key], ) if self.secondary is not None and alias_secondary: criterion = ClauseAdapter( self.secondary._anonymous_fromclause() ).traverse(criterion) criterion = visitors.cloned_traverse( criterion, {}, {"bindparam": visit_bindparam} ) if adapt_source: criterion = adapt_source(criterion) return criterion def _get_attr_w_warn_on_none( self, mapper: Mapper[Any], state: InstanceState[Any], dict_: _InstanceDict, column: ColumnElement[Any], ) -> Callable[[], Any]: """Create the callable that is used in a many-to-one expression. E.g.:: u1 = s.query(User).get(5) expr = Address.user == u1 Above, the SQL should be "address.user_id = 5". The callable returned by this method produces the value "5" based on the identity of ``u1``. """ # in this callable, we're trying to thread the needle through # a wide variety of scenarios, including: # # * the object hasn't been flushed yet and there's no value for # the attribute as of yet # # * the object hasn't been flushed yet but it has a user-defined # value # # * the object has a value but it's expired and not locally present # # * the object has a value but it's expired and not locally present, # and the object is also detached # # * The object hadn't been flushed yet, there was no value, but # later, the object has been expired and detached, and *now* # they're trying to evaluate it # # * the object had a value, but it was changed to a new value, and # then expired # # * the object had a value, but it was changed to a new value, and # then expired, then the object was detached # # * the object has a user-set value, but it's None and we don't do # the comparison correctly for that so warn # prop = mapper.get_property_by_column(column) # by invoking this method, InstanceState will track the last known # value for this key each time the attribute is to be expired. # this feature was added explicitly for use in this method. state._track_last_known_value(prop.key) lkv_fixed = state._last_known_values def _go() -> Any: assert lkv_fixed is not None last_known = to_return = lkv_fixed[prop.key] existing_is_available = ( last_known is not LoaderCallableStatus.NO_VALUE ) # we support that the value may have changed. so here we # try to get the most recent value including re-fetching. # only if we can't get a value now due to detachment do we return # the last known value current_value = mapper._get_state_attr_by_column( state, dict_, column, passive=PassiveFlag.PASSIVE_OFF if state.persistent else PassiveFlag.PASSIVE_NO_FETCH ^ PassiveFlag.INIT_OK, ) if current_value is LoaderCallableStatus.NEVER_SET: if not existing_is_available: raise sa_exc.InvalidRequestError( "Can't resolve value for column %s on object " "%s; no value has been set for this column" % (column, state_str(state)) ) elif current_value is LoaderCallableStatus.PASSIVE_NO_RESULT: if not existing_is_available: raise sa_exc.InvalidRequestError( "Can't resolve value for column %s on object " "%s; the object is detached and the value was " "expired" % (column, state_str(state)) ) else: to_return = current_value if to_return is None: util.warn( "Got None for value of column %s; this is unsupported " "for a relationship comparison and will not " "currently produce an IS comparison " "(but may in a future release)" % column ) return to_return return _go def _lazy_none_clause( self, reverse_direction: bool = False, adapt_source: Optional[_CoreAdapterProto] = None, ) -> ColumnElement[bool]: if not reverse_direction: criterion, bind_to_col = ( self._lazy_strategy._lazywhere, self._lazy_strategy._bind_to_col, ) else: criterion, bind_to_col = ( self._lazy_strategy._rev_lazywhere, self._lazy_strategy._rev_bind_to_col, ) criterion = adapt_criterion_to_null(criterion, bind_to_col) if adapt_source: criterion = adapt_source(criterion) return criterion def __str__(self) -> str: return str(self.parent.class_.__name__) + "." + self.key def merge( self, session: Session, source_state: InstanceState[Any], source_dict: _InstanceDict, dest_state: InstanceState[Any], dest_dict: _InstanceDict, load: bool, _recursive: Dict[Any, object], _resolve_conflict_map: Dict[_IdentityKeyType[Any], object], ) -> None: if load: for r in self._reverse_property: if (source_state, r) in _recursive: return if "merge" not in self._cascade: return if self.key not in source_dict: return if self.uselist: impl = source_state.get_impl(self.key) assert is_has_collection_adapter(impl) instances_iterable = impl.get_collection(source_state, source_dict) # if this is a CollectionAttributeImpl, then empty should # be False, otherwise "self.key in source_dict" should not be # True assert not instances_iterable.empty if impl.collection else True if load: # for a full merge, pre-load the destination collection, # so that individual _merge of each item pulls from identity # map for those already present. # also assumes CollectionAttributeImpl behavior of loading # "old" list in any case dest_state.get_impl(self.key).get( dest_state, dest_dict, passive=PassiveFlag.PASSIVE_MERGE ) dest_list = [] for current in instances_iterable: current_state = attributes.instance_state(current) current_dict = attributes.instance_dict(current) _recursive[(current_state, self)] = True obj = session._merge( current_state, current_dict, load=load, _recursive=_recursive, _resolve_conflict_map=_resolve_conflict_map, ) if obj is not None: dest_list.append(obj) if not load: coll = attributes.init_state_collection( dest_state, dest_dict, self.key ) for c in dest_list: coll.append_without_event(c) else: dest_impl = dest_state.get_impl(self.key) assert is_has_collection_adapter(dest_impl) dest_impl.set( dest_state, dest_dict, dest_list, _adapt=False, passive=PassiveFlag.PASSIVE_MERGE, ) else: current = source_dict[self.key] if current is not None: current_state = attributes.instance_state(current) current_dict = attributes.instance_dict(current) _recursive[(current_state, self)] = True obj = session._merge( current_state, current_dict, load=load, _recursive=_recursive, _resolve_conflict_map=_resolve_conflict_map, ) else: obj = None if not load: dest_dict[self.key] = obj else: dest_state.get_impl(self.key).set( dest_state, dest_dict, obj, None ) def _value_as_iterable( self, state: InstanceState[_O], dict_: _InstanceDict, key: str, passive: PassiveFlag = PassiveFlag.PASSIVE_OFF, ) -> Sequence[Tuple[InstanceState[_O], _O]]: """Return a list of tuples (state, obj) for the given key. returns an empty list if the value is None/empty/PASSIVE_NO_RESULT """ impl = state.manager[key].impl x = impl.get(state, dict_, passive=passive) if x is LoaderCallableStatus.PASSIVE_NO_RESULT or x is None: return [] elif is_has_collection_adapter(impl): return [ (attributes.instance_state(o), o) for o in impl.get_collection(state, dict_, x, passive=passive) ] else: return [(attributes.instance_state(x), x)] def cascade_iterator( self, type_: str, state: InstanceState[Any], dict_: _InstanceDict, visited_states: Set[InstanceState[Any]], halt_on: Optional[Callable[[InstanceState[Any]], bool]] = None, ) -> Iterator[Tuple[Any, Mapper[Any], InstanceState[Any], _InstanceDict]]: # assert type_ in self._cascade # only actively lazy load on the 'delete' cascade if type_ != "delete" or self.passive_deletes: passive = PassiveFlag.PASSIVE_NO_INITIALIZE else: passive = PassiveFlag.PASSIVE_OFF if type_ == "save-update": tuples = state.manager[self.key].impl.get_all_pending(state, dict_) else: tuples = self._value_as_iterable( state, dict_, self.key, passive=passive ) skip_pending = ( type_ == "refresh-expire" and "delete-orphan" not in self._cascade ) for instance_state, c in tuples: if instance_state in visited_states: continue if c is None: # would like to emit a warning here, but # would not be consistent with collection.append(None) # current behavior of silently skipping. # see [ticket:2229] continue assert instance_state is not None instance_dict = attributes.instance_dict(c) if halt_on and halt_on(instance_state): continue if skip_pending and not instance_state.key: continue instance_mapper = instance_state.manager.mapper if not instance_mapper.isa(self.mapper.class_manager.mapper): raise AssertionError( "Attribute '%s' on class '%s' " "doesn't handle objects " "of type '%s'" % (self.key, self.parent.class_, c.__class__) ) visited_states.add(instance_state) yield c, instance_mapper, instance_state, instance_dict @property def _effective_sync_backref(self) -> bool: if self.viewonly: return False else: return self.sync_backref is not False @staticmethod def _check_sync_backref( rel_a: RelationshipProperty[Any], rel_b: RelationshipProperty[Any] ) -> None: if rel_a.viewonly and rel_b.sync_backref: raise sa_exc.InvalidRequestError( "Relationship %s cannot specify sync_backref=True since %s " "includes viewonly=True." % (rel_b, rel_a) ) if ( rel_a.viewonly and not rel_b.viewonly and rel_b.sync_backref is not False ): rel_b.sync_backref = False def _add_reverse_property(self, key: str) -> None: other = self.mapper.get_property(key, _configure_mappers=False) if not isinstance(other, RelationshipProperty): raise sa_exc.InvalidRequestError( "back_populates on relationship '%s' refers to attribute '%s' " "that is not a relationship. The back_populates parameter " "should refer to the name of a relationship on the target " "class." % (self, other) ) # viewonly and sync_backref cases # 1. self.viewonly==True and other.sync_backref==True -> error # 2. self.viewonly==True and other.viewonly==False and # other.sync_backref==None -> warn sync_backref=False, set to False self._check_sync_backref(self, other) # 3. other.viewonly==True and self.sync_backref==True -> error # 4. other.viewonly==True and self.viewonly==False and # self.sync_backref==None -> warn sync_backref=False, set to False self._check_sync_backref(other, self) self._reverse_property.add(other) other._reverse_property.add(self) other._setup_entity() if not other.mapper.common_parent(self.parent): raise sa_exc.ArgumentError( "reverse_property %r on " "relationship %s references relationship %s, which " "does not reference mapper %s" % (key, self, other, self.parent) ) if ( other._configure_started and self.direction in (ONETOMANY, MANYTOONE) and self.direction == other.direction ): raise sa_exc.ArgumentError( "%s and back-reference %s are " "both of the same direction %r. Did you mean to " "set remote_side on the many-to-one side ?" % (other, self, self.direction) ) @util.memoized_property def entity(self) -> _InternalEntityType[_T]: """Return the target mapped entity, which is an inspect() of the class or aliased class that is referred towards. """ self.parent._check_configure() return self.entity @util.memoized_property def mapper(self) -> Mapper[_T]: """Return the targeted :class:`_orm.Mapper` for this :class:`.RelationshipProperty`. """ return self.entity.mapper def do_init(self) -> None: self._check_conflicts() self._process_dependent_arguments() self._setup_entity() self._setup_registry_dependencies() self._setup_join_conditions() self._check_cascade_settings(self._cascade) self._post_init() self._generate_backref() self._join_condition._warn_for_conflicting_sync_targets() super().do_init() self._lazy_strategy = cast( "LazyLoader", self._get_strategy((("lazy", "select"),)) ) def _setup_registry_dependencies(self) -> None: self.parent.mapper.registry._set_depends_on( self.entity.mapper.registry ) def _process_dependent_arguments(self) -> None: """Convert incoming configuration arguments to their proper form. Callables are resolved, ORM annotations removed. """ # accept callables for other attributes which may require # deferred initialization. This technique is used # by declarative "string configs" and some recipes. init_args = self._init_args for attr in ( "order_by", "primaryjoin", "secondaryjoin", "secondary", "foreign_keys", "remote_side", ): rel_arg = getattr(init_args, attr) rel_arg._resolve_against_registry(self._clsregistry_resolvers[1]) # remove "annotations" which are present if mapped class # descriptors are used to create the join expression. for attr in "primaryjoin", "secondaryjoin": rel_arg = getattr(init_args, attr) val = rel_arg.resolved if val is not None: rel_arg.resolved = _orm_deannotate( coercions.expect( roles.ColumnArgumentRole, val, argname=attr ) ) secondary = init_args.secondary.resolved if secondary is not None and _is_mapped_class(secondary): raise sa_exc.ArgumentError( "secondary argument %s passed to to relationship() %s must " "be a Table object or other FROM clause; can't send a mapped " "class directly as rows in 'secondary' are persisted " "independently of a class that is mapped " "to that same table." % (secondary, self) ) # ensure expressions in self.order_by, foreign_keys, # remote_side are all columns, not strings. if ( init_args.order_by.resolved is not False and init_args.order_by.resolved is not None ): self.order_by = tuple( coercions.expect( roles.ColumnArgumentRole, x, argname="order_by" ) for x in util.to_list(init_args.order_by.resolved) ) else: self.order_by = False self._user_defined_foreign_keys = util.column_set( coercions.expect( roles.ColumnArgumentRole, x, argname="foreign_keys" ) for x in util.to_column_set(init_args.foreign_keys.resolved) ) self.remote_side = util.column_set( coercions.expect( roles.ColumnArgumentRole, x, argname="remote_side" ) for x in util.to_column_set(init_args.remote_side.resolved) ) def declarative_scan( self, decl_scan: _ClassScanMapperConfig, registry: _RegistryType, cls: Type[Any], originating_module: Optional[str], key: str, mapped_container: Optional[Type[Mapped[Any]]], annotation: Optional[_AnnotationScanType], extracted_mapped_annotation: Optional[_AnnotationScanType], is_dataclass_field: bool, ) -> None: argument = extracted_mapped_annotation if extracted_mapped_annotation is None: if self.argument is None: self._raise_for_required(key, cls) else: return argument = extracted_mapped_annotation assert originating_module is not None is_write_only = mapped_container is not None and issubclass( mapped_container, WriteOnlyMapped ) if is_write_only: self.lazy = "write_only" self.strategy_key = (("lazy", self.lazy),) is_dynamic = mapped_container is not None and issubclass( mapped_container, DynamicMapped ) if is_dynamic: self.lazy = "dynamic" self.strategy_key = (("lazy", self.lazy),) argument = de_optionalize_union_types(argument) if hasattr(argument, "__origin__"): arg_origin = argument.__origin__ # type: ignore if isinstance(arg_origin, type) and issubclass( arg_origin, abc.Collection ): if self.collection_class is None: if _py_inspect.isabstract(arg_origin): raise sa_exc.ArgumentError( f"Collection annotation type {arg_origin} cannot " "be instantiated; please provide an explicit " "'collection_class' parameter " "(e.g. list, set, etc.) to the " "relationship() function to accompany this " "annotation" ) self.collection_class = arg_origin elif not is_write_only and not is_dynamic: self.uselist = False if argument.__args__: # type: ignore if isinstance(arg_origin, type) and issubclass( arg_origin, typing.Mapping # type: ignore ): type_arg = argument.__args__[-1] # type: ignore else: type_arg = argument.__args__[0] # type: ignore if hasattr(type_arg, "__forward_arg__"): str_argument = type_arg.__forward_arg__ argument = resolve_name_to_real_class_name( str_argument, originating_module ) else: argument = type_arg else: raise sa_exc.ArgumentError( f"Generic alias {argument} requires an argument" ) elif hasattr(argument, "__forward_arg__"): argument = argument.__forward_arg__ # type: ignore argument = resolve_name_to_real_class_name( argument, originating_module ) # we don't allow the collection class to be a # __forward_arg__ right now, so if we see a forward arg here, # we know there was no collection class either if ( self.collection_class is None and not is_write_only and not is_dynamic ): self.uselist = False # ticket #8759 # if a lead argument was given to relationship(), like # `relationship("B")`, use that, don't replace it with class we # found in the annotation. The declarative_scan() method call here is # still useful, as we continue to derive collection type and do # checking of the annotation in any case. if self.argument is None: self.argument = cast("_RelationshipArgumentType[_T]", argument) @util.preload_module("sqlalchemy.orm.mapper") def _setup_entity(self, __argument: Any = None) -> None: if "entity" in self.__dict__: return mapperlib = util.preloaded.orm_mapper if __argument: argument = __argument else: argument = self.argument resolved_argument: _ExternalEntityType[Any] if isinstance(argument, str): # we might want to cleanup clsregistry API to make this # more straightforward resolved_argument = cast( "_ExternalEntityType[Any]", self._clsregistry_resolve_name(argument)(), ) elif callable(argument) and not isinstance( argument, (type, mapperlib.Mapper) ): resolved_argument = argument() else: resolved_argument = argument entity: _InternalEntityType[Any] if isinstance(resolved_argument, type): entity = class_mapper(resolved_argument, configure=False) else: try: entity = inspect(resolved_argument) except sa_exc.NoInspectionAvailable: entity = None # type: ignore if not hasattr(entity, "mapper"): raise sa_exc.ArgumentError( "relationship '%s' expects " "a class or a mapper argument (received: %s)" % (self.key, type(resolved_argument)) ) self.entity = entity # type: ignore self.target = self.entity.persist_selectable def _setup_join_conditions(self) -> None: self._join_condition = jc = JoinCondition( parent_persist_selectable=self.parent.persist_selectable, child_persist_selectable=self.entity.persist_selectable, parent_local_selectable=self.parent.local_table, child_local_selectable=self.entity.local_table, primaryjoin=self._init_args.primaryjoin.resolved, secondary=self._init_args.secondary.resolved, secondaryjoin=self._init_args.secondaryjoin.resolved, parent_equivalents=self.parent._equivalent_columns, child_equivalents=self.mapper._equivalent_columns, consider_as_foreign_keys=self._user_defined_foreign_keys, local_remote_pairs=self.local_remote_pairs, remote_side=self.remote_side, self_referential=self._is_self_referential, prop=self, support_sync=not self.viewonly, can_be_synced_fn=self._columns_are_mapped, ) self.primaryjoin = jc.primaryjoin self.secondaryjoin = jc.secondaryjoin self.secondary = jc.secondary self.direction = jc.direction self.local_remote_pairs = jc.local_remote_pairs self.remote_side = jc.remote_columns self.local_columns = jc.local_columns self.synchronize_pairs = jc.synchronize_pairs self._calculated_foreign_keys = jc.foreign_key_columns self.secondary_synchronize_pairs = jc.secondary_synchronize_pairs @property def _clsregistry_resolve_arg( self, ) -> Callable[[str, bool], _class_resolver]: return self._clsregistry_resolvers[1] @property def _clsregistry_resolve_name( self, ) -> Callable[[str], Callable[[], Union[Type[Any], Table, _ModNS]]]: return self._clsregistry_resolvers[0] @util.memoized_property @util.preload_module("sqlalchemy.orm.clsregistry") def _clsregistry_resolvers( self, ) -> Tuple[ Callable[[str], Callable[[], Union[Type[Any], Table, _ModNS]]], Callable[[str, bool], _class_resolver], ]: _resolver = util.preloaded.orm_clsregistry._resolver return _resolver(self.parent.class_, self) def _check_conflicts(self) -> None: """Test that this relationship is legal, warn about inheritance conflicts.""" if self.parent.non_primary and not class_mapper( self.parent.class_, configure=False ).has_property(self.key): raise sa_exc.ArgumentError( "Attempting to assign a new " "relationship '%s' to a non-primary mapper on " "class '%s'. New relationships can only be added " "to the primary mapper, i.e. the very first mapper " "created for class '%s' " % ( self.key, self.parent.class_.__name__, self.parent.class_.__name__, ) ) @property def cascade(self) -> CascadeOptions: """Return the current cascade setting for this :class:`.RelationshipProperty`. """ return self._cascade @cascade.setter def cascade(self, cascade: Union[str, CascadeOptions]) -> None: self._set_cascade(cascade) def _set_cascade(self, cascade_arg: Union[str, CascadeOptions]) -> None: cascade = CascadeOptions(cascade_arg) if self.viewonly: cascade = CascadeOptions( cascade.intersection(CascadeOptions._viewonly_cascades) ) if "mapper" in self.__dict__: self._check_cascade_settings(cascade) self._cascade = cascade if self._dependency_processor: self._dependency_processor.cascade = cascade def _check_cascade_settings(self, cascade: CascadeOptions) -> None: if ( cascade.delete_orphan and not self.single_parent and (self.direction is MANYTOMANY or self.direction is MANYTOONE) ): raise sa_exc.ArgumentError( "For %(direction)s relationship %(rel)s, delete-orphan " "cascade is normally " 'configured only on the "one" side of a one-to-many ' "relationship, " 'and not on the "many" side of a many-to-one or many-to-many ' "relationship. " "To force this relationship to allow a particular " '"%(relatedcls)s" object to be referred towards by only ' 'a single "%(clsname)s" object at a time via the ' "%(rel)s relationship, which " "would allow " "delete-orphan cascade to take place in this direction, set " "the single_parent=True flag." % { "rel": self, "direction": "many-to-one" if self.direction is MANYTOONE else "many-to-many", "clsname": self.parent.class_.__name__, "relatedcls": self.mapper.class_.__name__, }, code="bbf0", ) if self.passive_deletes == "all" and ( "delete" in cascade or "delete-orphan" in cascade ): raise sa_exc.ArgumentError( "On %s, can't set passive_deletes='all' in conjunction " "with 'delete' or 'delete-orphan' cascade" % self ) if cascade.delete_orphan: self.mapper.primary_mapper()._delete_orphans.append( (self.key, self.parent.class_) ) def _persists_for(self, mapper: Mapper[Any]) -> bool: """Return True if this property will persist values on behalf of the given mapper. """ return ( self.key in mapper.relationships and mapper.relationships[self.key] is self ) def _columns_are_mapped(self, *cols: ColumnElement[Any]) -> bool: """Return True if all columns in the given collection are mapped by the tables referenced by this :class:`.RelationshipProperty`. """ secondary = self._init_args.secondary.resolved for c in cols: if secondary is not None and secondary.c.contains_column(c): continue if not self.parent.persist_selectable.c.contains_column( c ) and not self.target.c.contains_column(c): return False return True def _generate_backref(self) -> None: """Interpret the 'backref' instruction to create a :func:`_orm.relationship` complementary to this one.""" if self.parent.non_primary: return if self.backref is not None and not self.back_populates: kwargs: Dict[str, Any] if isinstance(self.backref, str): backref_key, kwargs = self.backref, {} else: backref_key, kwargs = self.backref mapper = self.mapper.primary_mapper() if not mapper.concrete: check = set(mapper.iterate_to_root()).union( mapper.self_and_descendants ) for m in check: if m.has_property(backref_key) and not m.concrete: raise sa_exc.ArgumentError( "Error creating backref " "'%s' on relationship '%s': property of that " "name exists on mapper '%s'" % (backref_key, self, m) ) # determine primaryjoin/secondaryjoin for the # backref. Use the one we had, so that # a custom join doesn't have to be specified in # both directions. if self.secondary is not None: # for many to many, just switch primaryjoin/ # secondaryjoin. use the annotated # pj/sj on the _join_condition. pj = kwargs.pop( "primaryjoin", self._join_condition.secondaryjoin_minus_local, ) sj = kwargs.pop( "secondaryjoin", self._join_condition.primaryjoin_minus_local, ) else: pj = kwargs.pop( "primaryjoin", self._join_condition.primaryjoin_reverse_remote, ) sj = kwargs.pop("secondaryjoin", None) if sj: raise sa_exc.InvalidRequestError( "Can't assign 'secondaryjoin' on a backref " "against a non-secondary relationship." ) foreign_keys = kwargs.pop( "foreign_keys", self._user_defined_foreign_keys ) parent = self.parent.primary_mapper() kwargs.setdefault("viewonly", self.viewonly) kwargs.setdefault("post_update", self.post_update) kwargs.setdefault("passive_updates", self.passive_updates) kwargs.setdefault("sync_backref", self.sync_backref) self.back_populates = backref_key relationship = RelationshipProperty( parent, self.secondary, primaryjoin=pj, secondaryjoin=sj, foreign_keys=foreign_keys, back_populates=self.key, **kwargs, ) mapper._configure_property( backref_key, relationship, warn_for_existing=True ) if self.back_populates: self._add_reverse_property(self.back_populates) @util.preload_module("sqlalchemy.orm.dependency") def _post_init(self) -> None: dependency = util.preloaded.orm_dependency if self.uselist is None: self.uselist = self.direction is not MANYTOONE if not self.viewonly: self._dependency_processor = ( # type: ignore dependency.DependencyProcessor.from_relationship )(self) @util.memoized_property def _use_get(self) -> bool: """memoize the 'use_get' attribute of this RelationshipLoader's lazyloader.""" strategy = self._lazy_strategy return strategy.use_get @util.memoized_property def _is_self_referential(self) -> bool: return self.mapper.common_parent(self.parent) def _create_joins( self, source_polymorphic: bool = False, source_selectable: Optional[FromClause] = None, dest_selectable: Optional[FromClause] = None, of_type_entity: Optional[_InternalEntityType[Any]] = None, alias_secondary: bool = False, extra_criteria: Tuple[ColumnElement[bool], ...] = (), ) -> Tuple[ ColumnElement[bool], Optional[ColumnElement[bool]], FromClause, FromClause, Optional[FromClause], Optional[ClauseAdapter], ]: aliased = False if alias_secondary and self.secondary is not None: aliased = True if source_selectable is None: if source_polymorphic and self.parent.with_polymorphic: source_selectable = self.parent._with_polymorphic_selectable if of_type_entity: dest_mapper = of_type_entity.mapper if dest_selectable is None: dest_selectable = of_type_entity.selectable aliased = True else: dest_mapper = self.mapper if dest_selectable is None: dest_selectable = self.entity.selectable if self.mapper.with_polymorphic: aliased = True if self._is_self_referential and source_selectable is None: dest_selectable = dest_selectable._anonymous_fromclause() aliased = True elif ( dest_selectable is not self.mapper._with_polymorphic_selectable or self.mapper.with_polymorphic ): aliased = True single_crit = dest_mapper._single_table_criterion aliased = aliased or ( source_selectable is not None and ( source_selectable is not self.parent._with_polymorphic_selectable or source_selectable._is_subquery ) ) ( primaryjoin, secondaryjoin, secondary, target_adapter, dest_selectable, ) = self._join_condition.join_targets( source_selectable, dest_selectable, aliased, single_crit, extra_criteria, ) if source_selectable is None: source_selectable = self.parent.local_table if dest_selectable is None: dest_selectable = self.entity.local_table return ( primaryjoin, secondaryjoin, source_selectable, dest_selectable, secondary, target_adapter, ) def _annotate_columns(element: _CE, annotations: _AnnotationDict) -> _CE: def clone(elem: _CE) -> _CE: if isinstance(elem, expression.ColumnClause): elem = elem._annotate(annotations.copy()) # type: ignore elem._copy_internals(clone=clone) return elem if element is not None: element = clone(element) clone = None # type: ignore # remove gc cycles return element class JoinCondition: primaryjoin_initial: Optional[ColumnElement[bool]] primaryjoin: ColumnElement[bool] secondaryjoin: Optional[ColumnElement[bool]] secondary: Optional[FromClause] prop: RelationshipProperty[Any] synchronize_pairs: _ColumnPairs secondary_synchronize_pairs: _ColumnPairs direction: RelationshipDirection parent_persist_selectable: FromClause child_persist_selectable: FromClause parent_local_selectable: FromClause child_local_selectable: FromClause _local_remote_pairs: Optional[_ColumnPairs] def __init__( self, parent_persist_selectable: FromClause, child_persist_selectable: FromClause, parent_local_selectable: FromClause, child_local_selectable: FromClause, *, primaryjoin: Optional[ColumnElement[bool]] = None, secondary: Optional[FromClause] = None, secondaryjoin: Optional[ColumnElement[bool]] = None, parent_equivalents: Optional[_EquivalentColumnMap] = None, child_equivalents: Optional[_EquivalentColumnMap] = None, consider_as_foreign_keys: Any = None, local_remote_pairs: Optional[_ColumnPairs] = None, remote_side: Any = None, self_referential: Any = False, prop: RelationshipProperty[Any], support_sync: bool = True, can_be_synced_fn: Callable[..., bool] = lambda *c: True, ): self.parent_persist_selectable = parent_persist_selectable self.parent_local_selectable = parent_local_selectable self.child_persist_selectable = child_persist_selectable self.child_local_selectable = child_local_selectable self.parent_equivalents = parent_equivalents self.child_equivalents = child_equivalents self.primaryjoin_initial = primaryjoin self.secondaryjoin = secondaryjoin self.secondary = secondary self.consider_as_foreign_keys = consider_as_foreign_keys self._local_remote_pairs = local_remote_pairs self._remote_side = remote_side self.prop = prop self.self_referential = self_referential self.support_sync = support_sync self.can_be_synced_fn = can_be_synced_fn self._determine_joins() assert self.primaryjoin is not None self._sanitize_joins() self._annotate_fks() self._annotate_remote() self._annotate_local() self._annotate_parentmapper() self._setup_pairs() self._check_foreign_cols(self.primaryjoin, True) if self.secondaryjoin is not None: self._check_foreign_cols(self.secondaryjoin, False) self._determine_direction() self._check_remote_side() self._log_joins() def _log_joins(self) -> None: log = self.prop.logger log.info("%s setup primary join %s", self.prop, self.primaryjoin) log.info("%s setup secondary join %s", self.prop, self.secondaryjoin) log.info( "%s synchronize pairs [%s]", self.prop, ",".join( "(%s => %s)" % (l, r) for (l, r) in self.synchronize_pairs ), ) log.info( "%s secondary synchronize pairs [%s]", self.prop, ",".join( "(%s => %s)" % (l, r) for (l, r) in self.secondary_synchronize_pairs or [] ), ) log.info( "%s local/remote pairs [%s]", self.prop, ",".join( "(%s / %s)" % (l, r) for (l, r) in self.local_remote_pairs ), ) log.info( "%s remote columns [%s]", self.prop, ",".join("%s" % col for col in self.remote_columns), ) log.info( "%s local columns [%s]", self.prop, ",".join("%s" % col for col in self.local_columns), ) log.info("%s relationship direction %s", self.prop, self.direction) def _sanitize_joins(self) -> None: """remove the parententity annotation from our join conditions which can leak in here based on some declarative patterns and maybe others. "parentmapper" is relied upon both by the ORM evaluator as well as the use case in _join_fixture_inh_selfref_w_entity that relies upon it being present, see :ticket:`3364`. """ self.primaryjoin = _deep_deannotate( self.primaryjoin, values=("parententity", "proxy_key") ) if self.secondaryjoin is not None: self.secondaryjoin = _deep_deannotate( self.secondaryjoin, values=("parententity", "proxy_key") ) def _determine_joins(self) -> None: """Determine the 'primaryjoin' and 'secondaryjoin' attributes, if not passed to the constructor already. This is based on analysis of the foreign key relationships between the parent and target mapped selectables. """ if self.secondaryjoin is not None and self.secondary is None: raise sa_exc.ArgumentError( "Property %s specified with secondary " "join condition but " "no secondary argument" % self.prop ) # find a join between the given mapper's mapped table and # the given table. will try the mapper's local table first # for more specificity, then if not found will try the more # general mapped table, which in the case of inheritance is # a join. try: consider_as_foreign_keys = self.consider_as_foreign_keys or None if self.secondary is not None: if self.secondaryjoin is None: self.secondaryjoin = join_condition( self.child_persist_selectable, self.secondary, a_subset=self.child_local_selectable, consider_as_foreign_keys=consider_as_foreign_keys, ) if self.primaryjoin_initial is None: self.primaryjoin = join_condition( self.parent_persist_selectable, self.secondary, a_subset=self.parent_local_selectable, consider_as_foreign_keys=consider_as_foreign_keys, ) else: self.primaryjoin = self.primaryjoin_initial else: if self.primaryjoin_initial is None: self.primaryjoin = join_condition( self.parent_persist_selectable, self.child_persist_selectable, a_subset=self.parent_local_selectable, consider_as_foreign_keys=consider_as_foreign_keys, ) else: self.primaryjoin = self.primaryjoin_initial except sa_exc.NoForeignKeysError as nfe: if self.secondary is not None: raise sa_exc.NoForeignKeysError( "Could not determine join " "condition between parent/child tables on " "relationship %s - there are no foreign keys " "linking these tables via secondary table '%s'. " "Ensure that referencing columns are associated " "with a ForeignKey or ForeignKeyConstraint, or " "specify 'primaryjoin' and 'secondaryjoin' " "expressions." % (self.prop, self.secondary) ) from nfe else: raise sa_exc.NoForeignKeysError( "Could not determine join " "condition between parent/child tables on " "relationship %s - there are no foreign keys " "linking these tables. " "Ensure that referencing columns are associated " "with a ForeignKey or ForeignKeyConstraint, or " "specify a 'primaryjoin' expression." % self.prop ) from nfe except sa_exc.AmbiguousForeignKeysError as afe: if self.secondary is not None: raise sa_exc.AmbiguousForeignKeysError( "Could not determine join " "condition between parent/child tables on " "relationship %s - there are multiple foreign key " "paths linking the tables via secondary table '%s'. " "Specify the 'foreign_keys' " "argument, providing a list of those columns which " "should be counted as containing a foreign key " "reference from the secondary table to each of the " "parent and child tables." % (self.prop, self.secondary) ) from afe else: raise sa_exc.AmbiguousForeignKeysError( "Could not determine join " "condition between parent/child tables on " "relationship %s - there are multiple foreign key " "paths linking the tables. Specify the " "'foreign_keys' argument, providing a list of those " "columns which should be counted as containing a " "foreign key reference to the parent table." % self.prop ) from afe @property def primaryjoin_minus_local(self) -> ColumnElement[bool]: return _deep_deannotate(self.primaryjoin, values=("local", "remote")) @property def secondaryjoin_minus_local(self) -> ColumnElement[bool]: assert self.secondaryjoin is not None return _deep_deannotate(self.secondaryjoin, values=("local", "remote")) @util.memoized_property def primaryjoin_reverse_remote(self) -> ColumnElement[bool]: """Return the primaryjoin condition suitable for the "reverse" direction. If the primaryjoin was delivered here with pre-existing "remote" annotations, the local/remote annotations are reversed. Otherwise, the local/remote annotations are removed. """ if self._has_remote_annotations: def replace(element: _CE, **kw: Any) -> Optional[_CE]: if "remote" in element._annotations: v = dict(element._annotations) del v["remote"] v["local"] = True return element._with_annotations(v) elif "local" in element._annotations: v = dict(element._annotations) del v["local"] v["remote"] = True return element._with_annotations(v) return None return visitors.replacement_traverse(self.primaryjoin, {}, replace) else: if self._has_foreign_annotations: # TODO: coverage return _deep_deannotate( self.primaryjoin, values=("local", "remote") ) else: return _deep_deannotate(self.primaryjoin) def _has_annotation(self, clause: ClauseElement, annotation: str) -> bool: for col in visitors.iterate(clause, {}): if annotation in col._annotations: return True else: return False @util.memoized_property def _has_foreign_annotations(self) -> bool: return self._has_annotation(self.primaryjoin, "foreign") @util.memoized_property def _has_remote_annotations(self) -> bool: return self._has_annotation(self.primaryjoin, "remote") def _annotate_fks(self) -> None: """Annotate the primaryjoin and secondaryjoin structures with 'foreign' annotations marking columns considered as foreign. """ if self._has_foreign_annotations: return if self.consider_as_foreign_keys: self._annotate_from_fk_list() else: self._annotate_present_fks() def _annotate_from_fk_list(self) -> None: def check_fk(element: _CE, **kw: Any) -> Optional[_CE]: if element in self.consider_as_foreign_keys: return element._annotate({"foreign": True}) return None self.primaryjoin = visitors.replacement_traverse( self.primaryjoin, {}, check_fk ) if self.secondaryjoin is not None: self.secondaryjoin = visitors.replacement_traverse( self.secondaryjoin, {}, check_fk ) def _annotate_present_fks(self) -> None: if self.secondary is not None: secondarycols = util.column_set(self.secondary.c) else: secondarycols = set() def is_foreign( a: ColumnElement[Any], b: ColumnElement[Any] ) -> Optional[ColumnElement[Any]]: if isinstance(a, schema.Column) and isinstance(b, schema.Column): if a.references(b): return a elif b.references(a): return b if secondarycols: if a in secondarycols and b not in secondarycols: return a elif b in secondarycols and a not in secondarycols: return b return None def visit_binary(binary: BinaryExpression[Any]) -> None: if not isinstance( binary.left, sql.ColumnElement ) or not isinstance(binary.right, sql.ColumnElement): return if ( "foreign" not in binary.left._annotations and "foreign" not in binary.right._annotations ): col = is_foreign(binary.left, binary.right) if col is not None: if col.compare(binary.left): binary.left = binary.left._annotate({"foreign": True}) elif col.compare(binary.right): binary.right = binary.right._annotate( {"foreign": True} ) self.primaryjoin = visitors.cloned_traverse( self.primaryjoin, {}, {"binary": visit_binary} ) if self.secondaryjoin is not None: self.secondaryjoin = visitors.cloned_traverse( self.secondaryjoin, {}, {"binary": visit_binary} ) def _refers_to_parent_table(self) -> bool: """Return True if the join condition contains column comparisons where both columns are in both tables. """ pt = self.parent_persist_selectable mt = self.child_persist_selectable result = False def visit_binary(binary: BinaryExpression[Any]) -> None: nonlocal result c, f = binary.left, binary.right if ( isinstance(c, expression.ColumnClause) and isinstance(f, expression.ColumnClause) and pt.is_derived_from(c.table) and pt.is_derived_from(f.table) and mt.is_derived_from(c.table) and mt.is_derived_from(f.table) ): result = True visitors.traverse(self.primaryjoin, {}, {"binary": visit_binary}) return result def _tables_overlap(self) -> bool: """Return True if parent/child tables have some overlap.""" return selectables_overlap( self.parent_persist_selectable, self.child_persist_selectable ) def _annotate_remote(self) -> None: """Annotate the primaryjoin and secondaryjoin structures with 'remote' annotations marking columns considered as part of the 'remote' side. """ if self._has_remote_annotations: return if self.secondary is not None: self._annotate_remote_secondary() elif self._local_remote_pairs or self._remote_side: self._annotate_remote_from_args() elif self._refers_to_parent_table(): self._annotate_selfref( lambda col: "foreign" in col._annotations, False ) elif self._tables_overlap(): self._annotate_remote_with_overlap() else: self._annotate_remote_distinct_selectables() def _annotate_remote_secondary(self) -> None: """annotate 'remote' in primaryjoin, secondaryjoin when 'secondary' is present. """ assert self.secondary is not None fixed_secondary = self.secondary def repl(element: _CE, **kw: Any) -> Optional[_CE]: if fixed_secondary.c.contains_column(element): return element._annotate({"remote": True}) return None self.primaryjoin = visitors.replacement_traverse( self.primaryjoin, {}, repl ) assert self.secondaryjoin is not None self.secondaryjoin = visitors.replacement_traverse( self.secondaryjoin, {}, repl ) def _annotate_selfref( self, fn: Callable[[ColumnElement[Any]], bool], remote_side_given: bool ) -> None: """annotate 'remote' in primaryjoin, secondaryjoin when the relationship is detected as self-referential. """ def visit_binary(binary: BinaryExpression[Any]) -> None: equated = binary.left.compare(binary.right) if isinstance(binary.left, expression.ColumnClause) and isinstance( binary.right, expression.ColumnClause ): # assume one to many - FKs are "remote" if fn(binary.left): binary.left = binary.left._annotate({"remote": True}) if fn(binary.right) and not equated: binary.right = binary.right._annotate({"remote": True}) elif not remote_side_given: self._warn_non_column_elements() self.primaryjoin = visitors.cloned_traverse( self.primaryjoin, {}, {"binary": visit_binary} ) def _annotate_remote_from_args(self) -> None: """annotate 'remote' in primaryjoin, secondaryjoin when the 'remote_side' or '_local_remote_pairs' arguments are used. """ if self._local_remote_pairs: if self._remote_side: raise sa_exc.ArgumentError( "remote_side argument is redundant " "against more detailed _local_remote_side " "argument." ) remote_side = [r for (l, r) in self._local_remote_pairs] else: remote_side = self._remote_side if self._refers_to_parent_table(): self._annotate_selfref(lambda col: col in remote_side, True) else: def repl(element: _CE, **kw: Any) -> Optional[_CE]: # use set() to avoid generating ``__eq__()`` expressions # against each element if element in set(remote_side): return element._annotate({"remote": True}) return None self.primaryjoin = visitors.replacement_traverse( self.primaryjoin, {}, repl ) def _annotate_remote_with_overlap(self) -> None: """annotate 'remote' in primaryjoin, secondaryjoin when the parent/child tables have some set of tables in common, though is not a fully self-referential relationship. """ def visit_binary(binary: BinaryExpression[Any]) -> None: binary.left, binary.right = proc_left_right( binary.left, binary.right ) binary.right, binary.left = proc_left_right( binary.right, binary.left ) check_entities = ( self.prop is not None and self.prop.mapper is not self.prop.parent ) def proc_left_right( left: ColumnElement[Any], right: ColumnElement[Any] ) -> Tuple[ColumnElement[Any], ColumnElement[Any]]: if isinstance(left, expression.ColumnClause) and isinstance( right, expression.ColumnClause ): if self.child_persist_selectable.c.contains_column( right ) and self.parent_persist_selectable.c.contains_column(left): right = right._annotate({"remote": True}) elif ( check_entities and right._annotations.get("parentmapper") is self.prop.mapper ): right = right._annotate({"remote": True}) elif ( check_entities and left._annotations.get("parentmapper") is self.prop.mapper ): left = left._annotate({"remote": True}) else: self._warn_non_column_elements() return left, right self.primaryjoin = visitors.cloned_traverse( self.primaryjoin, {}, {"binary": visit_binary} ) def _annotate_remote_distinct_selectables(self) -> None: """annotate 'remote' in primaryjoin, secondaryjoin when the parent/child tables are entirely separate. """ def repl(element: _CE, **kw: Any) -> Optional[_CE]: if self.child_persist_selectable.c.contains_column(element) and ( not self.parent_local_selectable.c.contains_column(element) or self.child_local_selectable.c.contains_column(element) ): return element._annotate({"remote": True}) return None self.primaryjoin = visitors.replacement_traverse( self.primaryjoin, {}, repl ) def _warn_non_column_elements(self) -> None: util.warn( "Non-simple column elements in primary " "join condition for property %s - consider using " "remote() annotations to mark the remote side." % self.prop ) def _annotate_local(self) -> None: """Annotate the primaryjoin and secondaryjoin structures with 'local' annotations. This annotates all column elements found simultaneously in the parent table and the join condition that don't have a 'remote' annotation set up from _annotate_remote() or user-defined. """ if self._has_annotation(self.primaryjoin, "local"): return if self._local_remote_pairs: local_side = util.column_set( [l for (l, r) in self._local_remote_pairs] ) else: local_side = util.column_set(self.parent_persist_selectable.c) def locals_(element: _CE, **kw: Any) -> Optional[_CE]: if "remote" not in element._annotations and element in local_side: return element._annotate({"local": True}) return None self.primaryjoin = visitors.replacement_traverse( self.primaryjoin, {}, locals_ ) def _annotate_parentmapper(self) -> None: def parentmappers_(element: _CE, **kw: Any) -> Optional[_CE]: if "remote" in element._annotations: return element._annotate({"parentmapper": self.prop.mapper}) elif "local" in element._annotations: return element._annotate({"parentmapper": self.prop.parent}) return None self.primaryjoin = visitors.replacement_traverse( self.primaryjoin, {}, parentmappers_ ) def _check_remote_side(self) -> None: if not self.local_remote_pairs: raise sa_exc.ArgumentError( "Relationship %s could " "not determine any unambiguous local/remote column " "pairs based on join condition and remote_side " "arguments. " "Consider using the remote() annotation to " "accurately mark those elements of the join " "condition that are on the remote side of " "the relationship." % (self.prop,) ) else: not_target = util.column_set( self.parent_persist_selectable.c ).difference(self.child_persist_selectable.c) for _, rmt in self.local_remote_pairs: if rmt in not_target: util.warn( "Expression %s is marked as 'remote', but these " "column(s) are local to the local side. The " "remote() annotation is needed only for a " "self-referential relationship where both sides " "of the relationship refer to the same tables." % (rmt,) ) def _check_foreign_cols( self, join_condition: ColumnElement[bool], primary: bool ) -> None: """Check the foreign key columns collected and emit error messages.""" can_sync = False foreign_cols = self._gather_columns_with_annotation( join_condition, "foreign" ) has_foreign = bool(foreign_cols) if primary: can_sync = bool(self.synchronize_pairs) else: can_sync = bool(self.secondary_synchronize_pairs) if ( self.support_sync and can_sync or (not self.support_sync and has_foreign) ): return # from here below is just determining the best error message # to report. Check for a join condition using any operator # (not just ==), perhaps they need to turn on "viewonly=True". if self.support_sync and has_foreign and not can_sync: err = ( "Could not locate any simple equality expressions " "involving locally mapped foreign key columns for " "%s join condition " "'%s' on relationship %s." % ( primary and "primary" or "secondary", join_condition, self.prop, ) ) err += ( " Ensure that referencing columns are associated " "with a ForeignKey or ForeignKeyConstraint, or are " "annotated in the join condition with the foreign() " "annotation. To allow comparison operators other than " "'==', the relationship can be marked as viewonly=True." ) raise sa_exc.ArgumentError(err) else: err = ( "Could not locate any relevant foreign key columns " "for %s join condition '%s' on relationship %s." % ( primary and "primary" or "secondary", join_condition, self.prop, ) ) err += ( " Ensure that referencing columns are associated " "with a ForeignKey or ForeignKeyConstraint, or are " "annotated in the join condition with the foreign() " "annotation." ) raise sa_exc.ArgumentError(err) def _determine_direction(self) -> None: """Determine if this relationship is one to many, many to one, many to many. """ if self.secondaryjoin is not None: self.direction = MANYTOMANY else: parentcols = util.column_set(self.parent_persist_selectable.c) targetcols = util.column_set(self.child_persist_selectable.c) # fk collection which suggests ONETOMANY. onetomany_fk = targetcols.intersection(self.foreign_key_columns) # fk collection which suggests MANYTOONE. manytoone_fk = parentcols.intersection(self.foreign_key_columns) if onetomany_fk and manytoone_fk: # fks on both sides. test for overlap of local/remote # with foreign key. # we will gather columns directly from their annotations # without deannotating, so that we can distinguish on a column # that refers to itself. # 1. columns that are both remote and FK suggest # onetomany. onetomany_local = self._gather_columns_with_annotation( self.primaryjoin, "remote", "foreign" ) # 2. columns that are FK but are not remote (e.g. local) # suggest manytoone. manytoone_local = { c for c in self._gather_columns_with_annotation( self.primaryjoin, "foreign" ) if "remote" not in c._annotations } # 3. if both collections are present, remove columns that # refer to themselves. This is for the case of # and_(Me.id == Me.remote_id, Me.version == Me.version) if onetomany_local and manytoone_local: self_equated = self.remote_columns.intersection( self.local_columns ) onetomany_local = onetomany_local.difference(self_equated) manytoone_local = manytoone_local.difference(self_equated) # at this point, if only one or the other collection is # present, we know the direction, otherwise it's still # ambiguous. if onetomany_local and not manytoone_local: self.direction = ONETOMANY elif manytoone_local and not onetomany_local: self.direction = MANYTOONE else: raise sa_exc.ArgumentError( "Can't determine relationship" " direction for relationship '%s' - foreign " "key columns within the join condition are present " "in both the parent and the child's mapped tables. " "Ensure that only those columns referring " "to a parent column are marked as foreign, " "either via the foreign() annotation or " "via the foreign_keys argument." % self.prop ) elif onetomany_fk: self.direction = ONETOMANY elif manytoone_fk: self.direction = MANYTOONE else: raise sa_exc.ArgumentError( "Can't determine relationship " "direction for relationship '%s' - foreign " "key columns are present in neither the parent " "nor the child's mapped tables" % self.prop ) def _deannotate_pairs( self, collection: _ColumnPairIterable ) -> _MutableColumnPairs: """provide deannotation for the various lists of pairs, so that using them in hashes doesn't incur high-overhead __eq__() comparisons against original columns mapped. """ return [(x._deannotate(), y._deannotate()) for x, y in collection] def _setup_pairs(self) -> None: sync_pairs: _MutableColumnPairs = [] lrp: util.OrderedSet[ Tuple[ColumnElement[Any], ColumnElement[Any]] ] = util.OrderedSet([]) secondary_sync_pairs: _MutableColumnPairs = [] def go( joincond: ColumnElement[bool], collection: _MutableColumnPairs, ) -> None: def visit_binary( binary: BinaryExpression[Any], left: ColumnElement[Any], right: ColumnElement[Any], ) -> None: if ( "remote" in right._annotations and "remote" not in left._annotations and self.can_be_synced_fn(left) ): lrp.add((left, right)) elif ( "remote" in left._annotations and "remote" not in right._annotations and self.can_be_synced_fn(right) ): lrp.add((right, left)) if binary.operator is operators.eq and self.can_be_synced_fn( left, right ): if "foreign" in right._annotations: collection.append((left, right)) elif "foreign" in left._annotations: collection.append((right, left)) visit_binary_product(visit_binary, joincond) for joincond, collection in [ (self.primaryjoin, sync_pairs), (self.secondaryjoin, secondary_sync_pairs), ]: if joincond is None: continue go(joincond, collection) self.local_remote_pairs = self._deannotate_pairs(lrp) self.synchronize_pairs = self._deannotate_pairs(sync_pairs) self.secondary_synchronize_pairs = self._deannotate_pairs( secondary_sync_pairs ) _track_overlapping_sync_targets: weakref.WeakKeyDictionary[ ColumnElement[Any], weakref.WeakKeyDictionary[ RelationshipProperty[Any], ColumnElement[Any] ], ] = weakref.WeakKeyDictionary() def _warn_for_conflicting_sync_targets(self) -> None: if not self.support_sync: return # we would like to detect if we are synchronizing any column # pairs in conflict with another relationship that wishes to sync # an entirely different column to the same target. This is a # very rare edge case so we will try to minimize the memory/overhead # impact of this check for from_, to_ in [ (from_, to_) for (from_, to_) in self.synchronize_pairs ] + [ (from_, to_) for (from_, to_) in self.secondary_synchronize_pairs ]: # save ourselves a ton of memory and overhead by only # considering columns that are subject to a overlapping # FK constraints at the core level. This condition can arise # if multiple relationships overlap foreign() directly, but # we're going to assume it's typically a ForeignKeyConstraint- # level configuration that benefits from this warning. if to_ not in self._track_overlapping_sync_targets: self._track_overlapping_sync_targets[ to_ ] = weakref.WeakKeyDictionary({self.prop: from_}) else: other_props = [] prop_to_from = self._track_overlapping_sync_targets[to_] for pr, fr_ in prop_to_from.items(): if ( not pr.mapper._dispose_called and pr not in self.prop._reverse_property and pr.key not in self.prop._overlaps and self.prop.key not in pr._overlaps # note: the "__*" symbol is used internally by # SQLAlchemy as a general means of suppressing the # overlaps warning for some extension cases, however # this is not currently # a publicly supported symbol and may change at # any time. and "__*" not in self.prop._overlaps and "__*" not in pr._overlaps and not self.prop.parent.is_sibling(pr.parent) and not self.prop.mapper.is_sibling(pr.mapper) and not self.prop.parent.is_sibling(pr.mapper) and not self.prop.mapper.is_sibling(pr.parent) and ( self.prop.key != pr.key or not self.prop.parent.common_parent(pr.parent) ) ): other_props.append((pr, fr_)) if other_props: util.warn( "relationship '%s' will copy column %s to column %s, " "which conflicts with relationship(s): %s. " "If this is not the intention, consider if these " "relationships should be linked with " "back_populates, or if viewonly=True should be " "applied to one or more if they are read-only. " "For the less common case that foreign key " "constraints are partially overlapping, the " "orm.foreign() " "annotation can be used to isolate the columns that " "should be written towards. To silence this " "warning, add the parameter 'overlaps=\"%s\"' to the " "'%s' relationship." % ( self.prop, from_, to_, ", ".join( sorted( "'%s' (copies %s to %s)" % (pr, fr_, to_) for (pr, fr_) in other_props ) ), ",".join(sorted(pr.key for pr, fr in other_props)), self.prop, ), code="qzyx", ) self._track_overlapping_sync_targets[to_][self.prop] = from_ @util.memoized_property def remote_columns(self) -> Set[ColumnElement[Any]]: return self._gather_join_annotations("remote") @util.memoized_property def local_columns(self) -> Set[ColumnElement[Any]]: return self._gather_join_annotations("local") @util.memoized_property def foreign_key_columns(self) -> Set[ColumnElement[Any]]: return self._gather_join_annotations("foreign") def _gather_join_annotations( self, annotation: str ) -> Set[ColumnElement[Any]]: s = set( self._gather_columns_with_annotation(self.primaryjoin, annotation) ) if self.secondaryjoin is not None: s.update( self._gather_columns_with_annotation( self.secondaryjoin, annotation ) ) return {x._deannotate() for x in s} def _gather_columns_with_annotation( self, clause: ColumnElement[Any], *annotation: Iterable[str] ) -> Set[ColumnElement[Any]]: annotation_set = set(annotation) return { cast(ColumnElement[Any], col) for col in visitors.iterate(clause, {}) if annotation_set.issubset(col._annotations) } def join_targets( self, source_selectable: Optional[FromClause], dest_selectable: FromClause, aliased: bool, single_crit: Optional[ColumnElement[bool]] = None, extra_criteria: Tuple[ColumnElement[bool], ...] = (), ) -> Tuple[ ColumnElement[bool], Optional[ColumnElement[bool]], Optional[FromClause], Optional[ClauseAdapter], FromClause, ]: """Given a source and destination selectable, create a join between them. This takes into account aliasing the join clause to reference the appropriate corresponding columns in the target objects, as well as the extra child criterion, equivalent column sets, etc. """ # place a barrier on the destination such that # replacement traversals won't ever dig into it. # its internal structure remains fixed # regardless of context. dest_selectable = _shallow_annotate( dest_selectable, {"no_replacement_traverse": True} ) primaryjoin, secondaryjoin, secondary = ( self.primaryjoin, self.secondaryjoin, self.secondary, ) # adjust the join condition for single table inheritance, # in the case that the join is to a subclass # this is analogous to the # "_adjust_for_single_table_inheritance()" method in Query. if single_crit is not None: if secondaryjoin is not None: secondaryjoin = secondaryjoin & single_crit else: primaryjoin = primaryjoin & single_crit if extra_criteria: if secondaryjoin is not None: secondaryjoin = secondaryjoin & sql.and_(*extra_criteria) else: primaryjoin = primaryjoin & sql.and_(*extra_criteria) if aliased: if secondary is not None: secondary = secondary._anonymous_fromclause(flat=True) primary_aliasizer = ClauseAdapter( secondary, exclude_fn=_ColInAnnotations("local") ) secondary_aliasizer = ClauseAdapter( dest_selectable, equivalents=self.child_equivalents ).chain(primary_aliasizer) if source_selectable is not None: primary_aliasizer = ClauseAdapter( secondary, exclude_fn=_ColInAnnotations("local") ).chain( ClauseAdapter( source_selectable, equivalents=self.parent_equivalents, ) ) secondaryjoin = secondary_aliasizer.traverse(secondaryjoin) else: primary_aliasizer = ClauseAdapter( dest_selectable, exclude_fn=_ColInAnnotations("local"), equivalents=self.child_equivalents, ) if source_selectable is not None: primary_aliasizer.chain( ClauseAdapter( source_selectable, exclude_fn=_ColInAnnotations("remote"), equivalents=self.parent_equivalents, ) ) secondary_aliasizer = None primaryjoin = primary_aliasizer.traverse(primaryjoin) target_adapter = secondary_aliasizer or primary_aliasizer target_adapter.exclude_fn = None else: target_adapter = None return ( primaryjoin, secondaryjoin, secondary, target_adapter, dest_selectable, ) def create_lazy_clause( self, reverse_direction: bool = False ) -> Tuple[ ColumnElement[bool], Dict[str, ColumnElement[Any]], Dict[ColumnElement[Any], ColumnElement[Any]], ]: binds: Dict[ColumnElement[Any], BindParameter[Any]] = {} equated_columns: Dict[ColumnElement[Any], ColumnElement[Any]] = {} has_secondary = self.secondaryjoin is not None if has_secondary: lookup = collections.defaultdict(list) for l, r in self.local_remote_pairs: lookup[l].append((l, r)) equated_columns[r] = l elif not reverse_direction: for l, r in self.local_remote_pairs: equated_columns[r] = l else: for l, r in self.local_remote_pairs: equated_columns[l] = r def col_to_bind( element: ColumnElement[Any], **kw: Any ) -> Optional[BindParameter[Any]]: if ( (not reverse_direction and "local" in element._annotations) or reverse_direction and ( (has_secondary and element in lookup) or (not has_secondary and "remote" in element._annotations) ) ): if element not in binds: binds[element] = sql.bindparam( None, None, type_=element.type, unique=True ) return binds[element] return None lazywhere = self.primaryjoin if self.secondaryjoin is None or not reverse_direction: lazywhere = visitors.replacement_traverse( lazywhere, {}, col_to_bind ) if self.secondaryjoin is not None: secondaryjoin = self.secondaryjoin if reverse_direction: secondaryjoin = visitors.replacement_traverse( secondaryjoin, {}, col_to_bind ) lazywhere = sql.and_(lazywhere, secondaryjoin) bind_to_col = {binds[col].key: col for col in binds} return lazywhere, bind_to_col, equated_columns class _ColInAnnotations: """Serializable object that tests for a name in c._annotations.""" __slots__ = ("name",) def __init__(self, name: str): self.name = name def __call__(self, c: ClauseElement) -> bool: return self.name in c._annotations class Relationship( # type: ignore RelationshipProperty[_T], _DeclarativeMapped[_T], WriteOnlyMapped[_T], # not compatible with Mapped[_T] DynamicMapped[_T], # not compatible with Mapped[_T] ): """Describes an object property that holds a single item or list of items that correspond to a related database table. Public constructor is the :func:`_orm.relationship` function. .. seealso:: :ref:`relationship_config_toplevel` .. versionchanged:: 2.0 Added :class:`_orm.Relationship` as a Declarative compatible subclass for :class:`_orm.RelationshipProperty`. """ inherit_cache = True """:meta private:"""