1# orm/interfaces.py
2# Copyright (C) 2005-2025 the SQLAlchemy authors and contributors
3# <see AUTHORS file>
4#
5# This module is part of SQLAlchemy and is released under
6# the MIT License: https://www.opensource.org/licenses/mit-license.php
7
8"""
9
10Contains various base classes used throughout the ORM.
11
12Defines some key base classes prominent within the internals.
13
14This module and the classes within are mostly private, though some attributes
15are exposed when inspecting mappings.
16
17"""
18
19from __future__ import annotations
20
21import collections
22import dataclasses
23import typing
24from typing import Any
25from typing import Callable
26from typing import cast
27from typing import ClassVar
28from typing import Dict
29from typing import Generic
30from typing import Iterator
31from typing import List
32from typing import NamedTuple
33from typing import NoReturn
34from typing import Optional
35from typing import Sequence
36from typing import Set
37from typing import Tuple
38from typing import Type
39from typing import TYPE_CHECKING
40from typing import TypeVar
41from typing import Union
42
43from . import exc as orm_exc
44from . import path_registry
45from .base import _MappedAttribute as _MappedAttribute
46from .base import EXT_CONTINUE as EXT_CONTINUE # noqa: F401
47from .base import EXT_SKIP as EXT_SKIP # noqa: F401
48from .base import EXT_STOP as EXT_STOP # noqa: F401
49from .base import InspectionAttr as InspectionAttr # noqa: F401
50from .base import InspectionAttrInfo as InspectionAttrInfo
51from .base import MANYTOMANY as MANYTOMANY # noqa: F401
52from .base import MANYTOONE as MANYTOONE # noqa: F401
53from .base import NO_KEY as NO_KEY # noqa: F401
54from .base import NO_VALUE as NO_VALUE # noqa: F401
55from .base import NotExtension as NotExtension # noqa: F401
56from .base import ONETOMANY as ONETOMANY # noqa: F401
57from .base import RelationshipDirection as RelationshipDirection # noqa: F401
58from .base import SQLORMOperations
59from .. import ColumnElement
60from .. import exc as sa_exc
61from .. import inspection
62from .. import util
63from ..sql import operators
64from ..sql import roles
65from ..sql import visitors
66from ..sql.base import _NoArg
67from ..sql.base import ExecutableOption
68from ..sql.cache_key import HasCacheKey
69from ..sql.operators import ColumnOperators
70from ..sql.schema import Column
71from ..sql.type_api import TypeEngine
72from ..util import warn_deprecated
73from ..util.typing import RODescriptorReference
74from ..util.typing import TypedDict
75
76if typing.TYPE_CHECKING:
77 from ._typing import _EntityType
78 from ._typing import _IdentityKeyType
79 from ._typing import _InstanceDict
80 from ._typing import _InternalEntityType
81 from ._typing import _ORMAdapterProto
82 from .attributes import InstrumentedAttribute
83 from .base import Mapped
84 from .context import _MapperEntity
85 from .context import ORMCompileState
86 from .context import QueryContext
87 from .decl_api import RegistryType
88 from .decl_base import _ClassScanMapperConfig
89 from .loading import _PopulatorDict
90 from .mapper import Mapper
91 from .path_registry import AbstractEntityRegistry
92 from .query import Query
93 from .session import Session
94 from .state import InstanceState
95 from .strategy_options import _LoadElement
96 from .util import AliasedInsp
97 from .util import ORMAdapter
98 from ..engine.result import Result
99 from ..sql._typing import _ColumnExpressionArgument
100 from ..sql._typing import _ColumnsClauseArgument
101 from ..sql._typing import _DMLColumnArgument
102 from ..sql._typing import _InfoType
103 from ..sql.operators import OperatorType
104 from ..sql.visitors import _TraverseInternalsType
105 from ..util.typing import _AnnotationScanType
106
107_StrategyKey = Tuple[Any, ...]
108
109_T = TypeVar("_T", bound=Any)
110_T_co = TypeVar("_T_co", bound=Any, covariant=True)
111
112_TLS = TypeVar("_TLS", bound="Type[LoaderStrategy]")
113
114
115class ORMStatementRole(roles.StatementRole):
116 __slots__ = ()
117 _role_name = (
118 "Executable SQL or text() construct, including ORM aware objects"
119 )
120
121
122class ORMColumnsClauseRole(
123 roles.ColumnsClauseRole, roles.TypedColumnsClauseRole[_T]
124):
125 __slots__ = ()
126 _role_name = "ORM mapped entity, aliased entity, or Column expression"
127
128
129class ORMEntityColumnsClauseRole(ORMColumnsClauseRole[_T]):
130 __slots__ = ()
131 _role_name = "ORM mapped or aliased entity"
132
133
134class ORMFromClauseRole(roles.StrictFromClauseRole):
135 __slots__ = ()
136 _role_name = "ORM mapped entity, aliased entity, or FROM expression"
137
138
139class ORMColumnDescription(TypedDict):
140 name: str
141 # TODO: add python_type and sql_type here; combining them
142 # into "type" is a bad idea
143 type: Union[Type[Any], TypeEngine[Any]]
144 aliased: bool
145 expr: _ColumnsClauseArgument[Any]
146 entity: Optional[_ColumnsClauseArgument[Any]]
147
148
149class _IntrospectsAnnotations:
150 __slots__ = ()
151
152 @classmethod
153 def _mapper_property_name(cls) -> str:
154 return cls.__name__
155
156 def found_in_pep593_annotated(self) -> Any:
157 """return a copy of this object to use in declarative when the
158 object is found inside of an Annotated object."""
159
160 raise NotImplementedError(
161 f"Use of the {self._mapper_property_name()!r} "
162 "construct inside of an Annotated object is not yet supported."
163 )
164
165 def declarative_scan(
166 self,
167 decl_scan: _ClassScanMapperConfig,
168 registry: RegistryType,
169 cls: Type[Any],
170 originating_module: Optional[str],
171 key: str,
172 mapped_container: Optional[Type[Mapped[Any]]],
173 annotation: Optional[_AnnotationScanType],
174 extracted_mapped_annotation: Optional[_AnnotationScanType],
175 is_dataclass_field: bool,
176 ) -> None:
177 """Perform class-specific initializaton at early declarative scanning
178 time.
179
180 .. versionadded:: 2.0
181
182 """
183
184 def _raise_for_required(self, key: str, cls: Type[Any]) -> NoReturn:
185 raise sa_exc.ArgumentError(
186 f"Python typing annotation is required for attribute "
187 f'"{cls.__name__}.{key}" when primary argument(s) for '
188 f'"{self._mapper_property_name()}" '
189 "construct are None or not present"
190 )
191
192
193class _AttributeOptions(NamedTuple):
194 """define Python-local attribute behavior options common to all
195 :class:`.MapperProperty` objects.
196
197 Currently this includes dataclass-generation arguments.
198
199 .. versionadded:: 2.0
200
201 """
202
203 dataclasses_init: Union[_NoArg, bool]
204 dataclasses_repr: Union[_NoArg, bool]
205 dataclasses_default: Union[_NoArg, Any]
206 dataclasses_default_factory: Union[_NoArg, Callable[[], Any]]
207 dataclasses_compare: Union[_NoArg, bool]
208 dataclasses_kw_only: Union[_NoArg, bool]
209 dataclasses_hash: Union[_NoArg, bool, None]
210
211 def _as_dataclass_field(self, key: str) -> Any:
212 """Return a ``dataclasses.Field`` object given these arguments."""
213
214 kw: Dict[str, Any] = {}
215 if self.dataclasses_default_factory is not _NoArg.NO_ARG:
216 kw["default_factory"] = self.dataclasses_default_factory
217 if self.dataclasses_default is not _NoArg.NO_ARG:
218 kw["default"] = self.dataclasses_default
219 if self.dataclasses_init is not _NoArg.NO_ARG:
220 kw["init"] = self.dataclasses_init
221 if self.dataclasses_repr is not _NoArg.NO_ARG:
222 kw["repr"] = self.dataclasses_repr
223 if self.dataclasses_compare is not _NoArg.NO_ARG:
224 kw["compare"] = self.dataclasses_compare
225 if self.dataclasses_kw_only is not _NoArg.NO_ARG:
226 kw["kw_only"] = self.dataclasses_kw_only
227 if self.dataclasses_hash is not _NoArg.NO_ARG:
228 kw["hash"] = self.dataclasses_hash
229
230 if "default" in kw and callable(kw["default"]):
231 # callable defaults are ambiguous. deprecate them in favour of
232 # insert_default or default_factory. #9936
233 warn_deprecated(
234 f"Callable object passed to the ``default`` parameter for "
235 f"attribute {key!r} in a ORM-mapped Dataclasses context is "
236 "ambiguous, "
237 "and this use will raise an error in a future release. "
238 "If this callable is intended to produce Core level INSERT "
239 "default values for an underlying ``Column``, use "
240 "the ``mapped_column.insert_default`` parameter instead. "
241 "To establish this callable as providing a default value "
242 "for instances of the dataclass itself, use the "
243 "``default_factory`` dataclasses parameter.",
244 "2.0",
245 )
246
247 if (
248 "init" in kw
249 and not kw["init"]
250 and "default" in kw
251 and not callable(kw["default"]) # ignore callable defaults. #9936
252 and "default_factory" not in kw # illegal but let dc.field raise
253 ):
254 # fix for #9879
255 default = kw.pop("default")
256 kw["default_factory"] = lambda: default
257
258 return dataclasses.field(**kw)
259
260 @classmethod
261 def _get_arguments_for_make_dataclass(
262 cls,
263 key: str,
264 annotation: _AnnotationScanType,
265 mapped_container: Optional[Any],
266 elem: _T,
267 ) -> Union[
268 Tuple[str, _AnnotationScanType],
269 Tuple[str, _AnnotationScanType, dataclasses.Field[Any]],
270 ]:
271 """given attribute key, annotation, and value from a class, return
272 the argument tuple we would pass to dataclasses.make_dataclass()
273 for this attribute.
274
275 """
276 if isinstance(elem, _DCAttributeOptions):
277 dc_field = elem._attribute_options._as_dataclass_field(key)
278
279 return (key, annotation, dc_field)
280 elif elem is not _NoArg.NO_ARG:
281 # why is typing not erroring on this?
282 return (key, annotation, elem)
283 elif mapped_container is not None:
284 # it's Mapped[], but there's no "element", which means declarative
285 # did not actually do anything for this field. this shouldn't
286 # happen.
287 # previously, this would occur because _scan_attributes would
288 # skip a field that's on an already mapped superclass, but it
289 # would still include it in the annotations, leading
290 # to issue #8718
291
292 assert False, "Mapped[] received without a mapping declaration"
293
294 else:
295 # plain dataclass field, not mapped. Is only possible
296 # if __allow_unmapped__ is set up. I can see this mode causing
297 # problems...
298 return (key, annotation)
299
300
301_DEFAULT_ATTRIBUTE_OPTIONS = _AttributeOptions(
302 _NoArg.NO_ARG,
303 _NoArg.NO_ARG,
304 _NoArg.NO_ARG,
305 _NoArg.NO_ARG,
306 _NoArg.NO_ARG,
307 _NoArg.NO_ARG,
308 _NoArg.NO_ARG,
309)
310
311_DEFAULT_READONLY_ATTRIBUTE_OPTIONS = _AttributeOptions(
312 False,
313 _NoArg.NO_ARG,
314 _NoArg.NO_ARG,
315 _NoArg.NO_ARG,
316 _NoArg.NO_ARG,
317 _NoArg.NO_ARG,
318 _NoArg.NO_ARG,
319)
320
321
322class _DCAttributeOptions:
323 """mixin for descriptors or configurational objects that include dataclass
324 field options.
325
326 This includes :class:`.MapperProperty`, :class:`._MapsColumn` within
327 the ORM, but also includes :class:`.AssociationProxy` within ext.
328 Can in theory be used for other descriptors that serve a similar role
329 as association proxy. (*maybe* hybrids, not sure yet.)
330
331 """
332
333 __slots__ = ()
334
335 _attribute_options: _AttributeOptions
336 """behavioral options for ORM-enabled Python attributes
337
338 .. versionadded:: 2.0
339
340 """
341
342 _has_dataclass_arguments: bool
343
344
345class _MapsColumns(_DCAttributeOptions, _MappedAttribute[_T]):
346 """interface for declarative-capable construct that delivers one or more
347 Column objects to the declarative process to be part of a Table.
348 """
349
350 __slots__ = ()
351
352 @property
353 def mapper_property_to_assign(self) -> Optional[MapperProperty[_T]]:
354 """return a MapperProperty to be assigned to the declarative mapping"""
355 raise NotImplementedError()
356
357 @property
358 def columns_to_assign(self) -> List[Tuple[Column[_T], int]]:
359 """A list of Column objects that should be declaratively added to the
360 new Table object.
361
362 """
363 raise NotImplementedError()
364
365
366# NOTE: MapperProperty needs to extend _MappedAttribute so that declarative
367# typing works, i.e. "Mapped[A] = relationship()". This introduces an
368# inconvenience which is that all the MapperProperty objects are treated
369# as descriptors by typing tools, which are misled by this as assignment /
370# access to a descriptor attribute wants to move through __get__.
371# Therefore, references to MapperProperty as an instance variable, such
372# as in PropComparator, may have some special typing workarounds such as the
373# use of sqlalchemy.util.typing.DescriptorReference to avoid mis-interpretation
374# by typing tools
375@inspection._self_inspects
376class MapperProperty(
377 HasCacheKey,
378 _DCAttributeOptions,
379 _MappedAttribute[_T],
380 InspectionAttrInfo,
381 util.MemoizedSlots,
382):
383 """Represent a particular class attribute mapped by :class:`_orm.Mapper`.
384
385 The most common occurrences of :class:`.MapperProperty` are the
386 mapped :class:`_schema.Column`, which is represented in a mapping as
387 an instance of :class:`.ColumnProperty`,
388 and a reference to another class produced by :func:`_orm.relationship`,
389 represented in the mapping as an instance of
390 :class:`.Relationship`.
391
392 """
393
394 __slots__ = (
395 "_configure_started",
396 "_configure_finished",
397 "_attribute_options",
398 "_has_dataclass_arguments",
399 "parent",
400 "key",
401 "info",
402 "doc",
403 )
404
405 _cache_key_traversal: _TraverseInternalsType = [
406 ("parent", visitors.ExtendedInternalTraversal.dp_has_cache_key),
407 ("key", visitors.ExtendedInternalTraversal.dp_string),
408 ]
409
410 if not TYPE_CHECKING:
411 cascade = None
412
413 is_property = True
414 """Part of the InspectionAttr interface; states this object is a
415 mapper property.
416
417 """
418
419 comparator: PropComparator[_T]
420 """The :class:`_orm.PropComparator` instance that implements SQL
421 expression construction on behalf of this mapped attribute."""
422
423 key: str
424 """name of class attribute"""
425
426 parent: Mapper[Any]
427 """the :class:`.Mapper` managing this property."""
428
429 _is_relationship = False
430
431 _links_to_entity: bool
432 """True if this MapperProperty refers to a mapped entity.
433
434 Should only be True for Relationship, False for all others.
435
436 """
437
438 doc: Optional[str]
439 """optional documentation string"""
440
441 info: _InfoType
442 """Info dictionary associated with the object, allowing user-defined
443 data to be associated with this :class:`.InspectionAttr`.
444
445 The dictionary is generated when first accessed. Alternatively,
446 it can be specified as a constructor argument to the
447 :func:`.column_property`, :func:`_orm.relationship`, or :func:`.composite`
448 functions.
449
450 .. seealso::
451
452 :attr:`.QueryableAttribute.info`
453
454 :attr:`.SchemaItem.info`
455
456 """
457
458 def _memoized_attr_info(self) -> _InfoType:
459 """Info dictionary associated with the object, allowing user-defined
460 data to be associated with this :class:`.InspectionAttr`.
461
462 The dictionary is generated when first accessed. Alternatively,
463 it can be specified as a constructor argument to the
464 :func:`.column_property`, :func:`_orm.relationship`, or
465 :func:`.composite`
466 functions.
467
468 .. seealso::
469
470 :attr:`.QueryableAttribute.info`
471
472 :attr:`.SchemaItem.info`
473
474 """
475 return {}
476
477 def setup(
478 self,
479 context: ORMCompileState,
480 query_entity: _MapperEntity,
481 path: AbstractEntityRegistry,
482 adapter: Optional[ORMAdapter],
483 **kwargs: Any,
484 ) -> None:
485 """Called by Query for the purposes of constructing a SQL statement.
486
487 Each MapperProperty associated with the target mapper processes the
488 statement referenced by the query context, adding columns and/or
489 criterion as appropriate.
490
491 """
492
493 def create_row_processor(
494 self,
495 context: ORMCompileState,
496 query_entity: _MapperEntity,
497 path: AbstractEntityRegistry,
498 mapper: Mapper[Any],
499 result: Result[Any],
500 adapter: Optional[ORMAdapter],
501 populators: _PopulatorDict,
502 ) -> None:
503 """Produce row processing functions and append to the given
504 set of populators lists.
505
506 """
507
508 def cascade_iterator(
509 self,
510 type_: str,
511 state: InstanceState[Any],
512 dict_: _InstanceDict,
513 visited_states: Set[InstanceState[Any]],
514 halt_on: Optional[Callable[[InstanceState[Any]], bool]] = None,
515 ) -> Iterator[
516 Tuple[object, Mapper[Any], InstanceState[Any], _InstanceDict]
517 ]:
518 """Iterate through instances related to the given instance for
519 a particular 'cascade', starting with this MapperProperty.
520
521 Return an iterator3-tuples (instance, mapper, state).
522
523 Note that the 'cascade' collection on this MapperProperty is
524 checked first for the given type before cascade_iterator is called.
525
526 This method typically only applies to Relationship.
527
528 """
529
530 return iter(())
531
532 def set_parent(self, parent: Mapper[Any], init: bool) -> None:
533 """Set the parent mapper that references this MapperProperty.
534
535 This method is overridden by some subclasses to perform extra
536 setup when the mapper is first known.
537
538 """
539 self.parent = parent
540
541 def instrument_class(self, mapper: Mapper[Any]) -> None:
542 """Hook called by the Mapper to the property to initiate
543 instrumentation of the class attribute managed by this
544 MapperProperty.
545
546 The MapperProperty here will typically call out to the
547 attributes module to set up an InstrumentedAttribute.
548
549 This step is the first of two steps to set up an InstrumentedAttribute,
550 and is called early in the mapper setup process.
551
552 The second step is typically the init_class_attribute step,
553 called from StrategizedProperty via the post_instrument_class()
554 hook. This step assigns additional state to the InstrumentedAttribute
555 (specifically the "impl") which has been determined after the
556 MapperProperty has determined what kind of persistence
557 management it needs to do (e.g. scalar, object, collection, etc).
558
559 """
560
561 def __init__(
562 self,
563 attribute_options: Optional[_AttributeOptions] = None,
564 _assume_readonly_dc_attributes: bool = False,
565 ) -> None:
566 self._configure_started = False
567 self._configure_finished = False
568
569 if _assume_readonly_dc_attributes:
570 default_attrs = _DEFAULT_READONLY_ATTRIBUTE_OPTIONS
571 else:
572 default_attrs = _DEFAULT_ATTRIBUTE_OPTIONS
573
574 if attribute_options and attribute_options != default_attrs:
575 self._has_dataclass_arguments = True
576 self._attribute_options = attribute_options
577 else:
578 self._has_dataclass_arguments = False
579 self._attribute_options = default_attrs
580
581 def init(self) -> None:
582 """Called after all mappers are created to assemble
583 relationships between mappers and perform other post-mapper-creation
584 initialization steps.
585
586
587 """
588 self._configure_started = True
589 self.do_init()
590 self._configure_finished = True
591
592 @property
593 def class_attribute(self) -> InstrumentedAttribute[_T]:
594 """Return the class-bound descriptor corresponding to this
595 :class:`.MapperProperty`.
596
597 This is basically a ``getattr()`` call::
598
599 return getattr(self.parent.class_, self.key)
600
601 I.e. if this :class:`.MapperProperty` were named ``addresses``,
602 and the class to which it is mapped is ``User``, this sequence
603 is possible::
604
605 >>> from sqlalchemy import inspect
606 >>> mapper = inspect(User)
607 >>> addresses_property = mapper.attrs.addresses
608 >>> addresses_property.class_attribute is User.addresses
609 True
610 >>> User.addresses.property is addresses_property
611 True
612
613
614 """
615
616 return getattr(self.parent.class_, self.key) # type: ignore
617
618 def do_init(self) -> None:
619 """Perform subclass-specific initialization post-mapper-creation
620 steps.
621
622 This is a template method called by the ``MapperProperty``
623 object's init() method.
624
625 """
626
627 def post_instrument_class(self, mapper: Mapper[Any]) -> None:
628 """Perform instrumentation adjustments that need to occur
629 after init() has completed.
630
631 The given Mapper is the Mapper invoking the operation, which
632 may not be the same Mapper as self.parent in an inheritance
633 scenario; however, Mapper will always at least be a sub-mapper of
634 self.parent.
635
636 This method is typically used by StrategizedProperty, which delegates
637 it to LoaderStrategy.init_class_attribute() to perform final setup
638 on the class-bound InstrumentedAttribute.
639
640 """
641
642 def merge(
643 self,
644 session: Session,
645 source_state: InstanceState[Any],
646 source_dict: _InstanceDict,
647 dest_state: InstanceState[Any],
648 dest_dict: _InstanceDict,
649 load: bool,
650 _recursive: Dict[Any, object],
651 _resolve_conflict_map: Dict[_IdentityKeyType[Any], object],
652 ) -> None:
653 """Merge the attribute represented by this ``MapperProperty``
654 from source to destination object.
655
656 """
657
658 def __repr__(self) -> str:
659 return "<%s at 0x%x; %s>" % (
660 self.__class__.__name__,
661 id(self),
662 getattr(self, "key", "no key"),
663 )
664
665
666@inspection._self_inspects
667class PropComparator(SQLORMOperations[_T_co], Generic[_T_co], ColumnOperators):
668 r"""Defines SQL operations for ORM mapped attributes.
669
670 SQLAlchemy allows for operators to
671 be redefined at both the Core and ORM level. :class:`.PropComparator`
672 is the base class of operator redefinition for ORM-level operations,
673 including those of :class:`.ColumnProperty`,
674 :class:`.Relationship`, and :class:`.Composite`.
675
676 User-defined subclasses of :class:`.PropComparator` may be created. The
677 built-in Python comparison and math operator methods, such as
678 :meth:`.operators.ColumnOperators.__eq__`,
679 :meth:`.operators.ColumnOperators.__lt__`, and
680 :meth:`.operators.ColumnOperators.__add__`, can be overridden to provide
681 new operator behavior. The custom :class:`.PropComparator` is passed to
682 the :class:`.MapperProperty` instance via the ``comparator_factory``
683 argument. In each case,
684 the appropriate subclass of :class:`.PropComparator` should be used::
685
686 # definition of custom PropComparator subclasses
687
688 from sqlalchemy.orm.properties import (
689 ColumnProperty,
690 Composite,
691 Relationship,
692 )
693
694
695 class MyColumnComparator(ColumnProperty.Comparator):
696 def __eq__(self, other):
697 return self.__clause_element__() == other
698
699
700 class MyRelationshipComparator(Relationship.Comparator):
701 def any(self, expression):
702 "define the 'any' operation"
703 # ...
704
705
706 class MyCompositeComparator(Composite.Comparator):
707 def __gt__(self, other):
708 "redefine the 'greater than' operation"
709
710 return sql.and_(
711 *[
712 a > b
713 for a, b in zip(
714 self.__clause_element__().clauses,
715 other.__composite_values__(),
716 )
717 ]
718 )
719
720
721 # application of custom PropComparator subclasses
722
723 from sqlalchemy.orm import column_property, relationship, composite
724 from sqlalchemy import Column, String
725
726
727 class SomeMappedClass(Base):
728 some_column = column_property(
729 Column("some_column", String),
730 comparator_factory=MyColumnComparator,
731 )
732
733 some_relationship = relationship(
734 SomeOtherClass, comparator_factory=MyRelationshipComparator
735 )
736
737 some_composite = composite(
738 Column("a", String),
739 Column("b", String),
740 comparator_factory=MyCompositeComparator,
741 )
742
743 Note that for column-level operator redefinition, it's usually
744 simpler to define the operators at the Core level, using the
745 :attr:`.TypeEngine.comparator_factory` attribute. See
746 :ref:`types_operators` for more detail.
747
748 .. seealso::
749
750 :class:`.ColumnProperty.Comparator`
751
752 :class:`.Relationship.Comparator`
753
754 :class:`.Composite.Comparator`
755
756 :class:`.ColumnOperators`
757
758 :ref:`types_operators`
759
760 :attr:`.TypeEngine.comparator_factory`
761
762 """
763
764 __slots__ = "prop", "_parententity", "_adapt_to_entity"
765
766 __visit_name__ = "orm_prop_comparator"
767
768 _parententity: _InternalEntityType[Any]
769 _adapt_to_entity: Optional[AliasedInsp[Any]]
770 prop: RODescriptorReference[MapperProperty[_T_co]]
771
772 def __init__(
773 self,
774 prop: MapperProperty[_T],
775 parentmapper: _InternalEntityType[Any],
776 adapt_to_entity: Optional[AliasedInsp[Any]] = None,
777 ):
778 self.prop = prop
779 self._parententity = adapt_to_entity or parentmapper
780 self._adapt_to_entity = adapt_to_entity
781
782 @util.non_memoized_property
783 def property(self) -> MapperProperty[_T_co]:
784 """Return the :class:`.MapperProperty` associated with this
785 :class:`.PropComparator`.
786
787
788 Return values here will commonly be instances of
789 :class:`.ColumnProperty` or :class:`.Relationship`.
790
791
792 """
793 return self.prop
794
795 def __clause_element__(self) -> roles.ColumnsClauseRole:
796 raise NotImplementedError("%r" % self)
797
798 def _bulk_update_tuples(
799 self, value: Any
800 ) -> Sequence[Tuple[_DMLColumnArgument, Any]]:
801 """Receive a SQL expression that represents a value in the SET
802 clause of an UPDATE statement.
803
804 Return a tuple that can be passed to a :class:`_expression.Update`
805 construct.
806
807 """
808
809 return [(cast("_DMLColumnArgument", self.__clause_element__()), value)]
810
811 def adapt_to_entity(
812 self, adapt_to_entity: AliasedInsp[Any]
813 ) -> PropComparator[_T_co]:
814 """Return a copy of this PropComparator which will use the given
815 :class:`.AliasedInsp` to produce corresponding expressions.
816 """
817 return self.__class__(self.prop, self._parententity, adapt_to_entity)
818
819 @util.ro_non_memoized_property
820 def _parentmapper(self) -> Mapper[Any]:
821 """legacy; this is renamed to _parententity to be
822 compatible with QueryableAttribute."""
823 return self._parententity.mapper
824
825 def _criterion_exists(
826 self,
827 criterion: Optional[_ColumnExpressionArgument[bool]] = None,
828 **kwargs: Any,
829 ) -> ColumnElement[Any]:
830 return self.prop.comparator._criterion_exists(criterion, **kwargs)
831
832 @util.ro_non_memoized_property
833 def adapter(self) -> Optional[_ORMAdapterProto]:
834 """Produce a callable that adapts column expressions
835 to suit an aliased version of this comparator.
836
837 """
838 if self._adapt_to_entity is None:
839 return None
840 else:
841 return self._adapt_to_entity._orm_adapt_element
842
843 @util.ro_non_memoized_property
844 def info(self) -> _InfoType:
845 return self.prop.info
846
847 @staticmethod
848 def _any_op(a: Any, b: Any, **kwargs: Any) -> Any:
849 return a.any(b, **kwargs)
850
851 @staticmethod
852 def _has_op(left: Any, other: Any, **kwargs: Any) -> Any:
853 return left.has(other, **kwargs)
854
855 @staticmethod
856 def _of_type_op(a: Any, class_: Any) -> Any:
857 return a.of_type(class_)
858
859 any_op = cast(operators.OperatorType, _any_op)
860 has_op = cast(operators.OperatorType, _has_op)
861 of_type_op = cast(operators.OperatorType, _of_type_op)
862
863 if typing.TYPE_CHECKING:
864
865 def operate(
866 self, op: OperatorType, *other: Any, **kwargs: Any
867 ) -> ColumnElement[Any]: ...
868
869 def reverse_operate(
870 self, op: OperatorType, other: Any, **kwargs: Any
871 ) -> ColumnElement[Any]: ...
872
873 def of_type(self, class_: _EntityType[Any]) -> PropComparator[_T_co]:
874 r"""Redefine this object in terms of a polymorphic subclass,
875 :func:`_orm.with_polymorphic` construct, or :func:`_orm.aliased`
876 construct.
877
878 Returns a new PropComparator from which further criterion can be
879 evaluated.
880
881 e.g.::
882
883 query.join(Company.employees.of_type(Engineer)).filter(
884 Engineer.name == "foo"
885 )
886
887 :param \class_: a class or mapper indicating that criterion will be
888 against this specific subclass.
889
890 .. seealso::
891
892 :ref:`orm_queryguide_joining_relationships_aliased` - in the
893 :ref:`queryguide_toplevel`
894
895 :ref:`inheritance_of_type`
896
897 """
898
899 return self.operate(PropComparator.of_type_op, class_) # type: ignore
900
901 def and_(
902 self, *criteria: _ColumnExpressionArgument[bool]
903 ) -> PropComparator[bool]:
904 """Add additional criteria to the ON clause that's represented by this
905 relationship attribute.
906
907 E.g.::
908
909
910 stmt = select(User).join(
911 User.addresses.and_(Address.email_address != "foo")
912 )
913
914 stmt = select(User).options(
915 joinedload(User.addresses.and_(Address.email_address != "foo"))
916 )
917
918 .. versionadded:: 1.4
919
920 .. seealso::
921
922 :ref:`orm_queryguide_join_on_augmented`
923
924 :ref:`loader_option_criteria`
925
926 :func:`.with_loader_criteria`
927
928 """
929 return self.operate(operators.and_, *criteria) # type: ignore
930
931 def any(
932 self,
933 criterion: Optional[_ColumnExpressionArgument[bool]] = None,
934 **kwargs: Any,
935 ) -> ColumnElement[bool]:
936 r"""Return a SQL expression representing true if this element
937 references a member which meets the given criterion.
938
939 The usual implementation of ``any()`` is
940 :meth:`.Relationship.Comparator.any`.
941
942 :param criterion: an optional ClauseElement formulated against the
943 member class' table or attributes.
944
945 :param \**kwargs: key/value pairs corresponding to member class
946 attribute names which will be compared via equality to the
947 corresponding values.
948
949 """
950
951 return self.operate(PropComparator.any_op, criterion, **kwargs)
952
953 def has(
954 self,
955 criterion: Optional[_ColumnExpressionArgument[bool]] = None,
956 **kwargs: Any,
957 ) -> ColumnElement[bool]:
958 r"""Return a SQL expression representing true if this element
959 references a member which meets the given criterion.
960
961 The usual implementation of ``has()`` is
962 :meth:`.Relationship.Comparator.has`.
963
964 :param criterion: an optional ClauseElement formulated against the
965 member class' table or attributes.
966
967 :param \**kwargs: key/value pairs corresponding to member class
968 attribute names which will be compared via equality to the
969 corresponding values.
970
971 """
972
973 return self.operate(PropComparator.has_op, criterion, **kwargs)
974
975
976class StrategizedProperty(MapperProperty[_T]):
977 """A MapperProperty which uses selectable strategies to affect
978 loading behavior.
979
980 There is a single strategy selected by default. Alternate
981 strategies can be selected at Query time through the usage of
982 ``StrategizedOption`` objects via the Query.options() method.
983
984 The mechanics of StrategizedProperty are used for every Query
985 invocation for every mapped attribute participating in that Query,
986 to determine first how the attribute will be rendered in SQL
987 and secondly how the attribute will retrieve a value from a result
988 row and apply it to a mapped object. The routines here are very
989 performance-critical.
990
991 """
992
993 __slots__ = (
994 "_strategies",
995 "strategy",
996 "_wildcard_token",
997 "_default_path_loader_key",
998 "strategy_key",
999 )
1000 inherit_cache = True
1001 strategy_wildcard_key: ClassVar[str]
1002
1003 strategy_key: _StrategyKey
1004
1005 _strategies: Dict[_StrategyKey, LoaderStrategy]
1006
1007 def _memoized_attr__wildcard_token(self) -> Tuple[str]:
1008 return (
1009 f"{self.strategy_wildcard_key}:{path_registry._WILDCARD_TOKEN}",
1010 )
1011
1012 def _memoized_attr__default_path_loader_key(
1013 self,
1014 ) -> Tuple[str, Tuple[str]]:
1015 return (
1016 "loader",
1017 (f"{self.strategy_wildcard_key}:{path_registry._DEFAULT_TOKEN}",),
1018 )
1019
1020 def _get_context_loader(
1021 self, context: ORMCompileState, path: AbstractEntityRegistry
1022 ) -> Optional[_LoadElement]:
1023 load: Optional[_LoadElement] = None
1024
1025 search_path = path[self]
1026
1027 # search among: exact match, "attr.*", "default" strategy
1028 # if any.
1029 for path_key in (
1030 search_path._loader_key,
1031 search_path._wildcard_path_loader_key,
1032 search_path._default_path_loader_key,
1033 ):
1034 if path_key in context.attributes:
1035 load = context.attributes[path_key]
1036 break
1037
1038 # note that if strategy_options.Load is placing non-actionable
1039 # objects in the context like defaultload(), we would
1040 # need to continue the loop here if we got such an
1041 # option as below.
1042 # if load.strategy or load.local_opts:
1043 # break
1044
1045 return load
1046
1047 def _get_strategy(self, key: _StrategyKey) -> LoaderStrategy:
1048 try:
1049 return self._strategies[key]
1050 except KeyError:
1051 pass
1052
1053 # run outside to prevent transfer of exception context
1054 cls = self._strategy_lookup(self, *key)
1055 # this previously was setting self._strategies[cls], that's
1056 # a bad idea; should use strategy key at all times because every
1057 # strategy has multiple keys at this point
1058 self._strategies[key] = strategy = cls(self, key)
1059 return strategy
1060
1061 def setup(
1062 self,
1063 context: ORMCompileState,
1064 query_entity: _MapperEntity,
1065 path: AbstractEntityRegistry,
1066 adapter: Optional[ORMAdapter],
1067 **kwargs: Any,
1068 ) -> None:
1069 loader = self._get_context_loader(context, path)
1070 if loader and loader.strategy:
1071 strat = self._get_strategy(loader.strategy)
1072 else:
1073 strat = self.strategy
1074 strat.setup_query(
1075 context, query_entity, path, loader, adapter, **kwargs
1076 )
1077
1078 def create_row_processor(
1079 self,
1080 context: ORMCompileState,
1081 query_entity: _MapperEntity,
1082 path: AbstractEntityRegistry,
1083 mapper: Mapper[Any],
1084 result: Result[Any],
1085 adapter: Optional[ORMAdapter],
1086 populators: _PopulatorDict,
1087 ) -> None:
1088 loader = self._get_context_loader(context, path)
1089 if loader and loader.strategy:
1090 strat = self._get_strategy(loader.strategy)
1091 else:
1092 strat = self.strategy
1093 strat.create_row_processor(
1094 context,
1095 query_entity,
1096 path,
1097 loader,
1098 mapper,
1099 result,
1100 adapter,
1101 populators,
1102 )
1103
1104 def do_init(self) -> None:
1105 self._strategies = {}
1106 self.strategy = self._get_strategy(self.strategy_key)
1107
1108 def post_instrument_class(self, mapper: Mapper[Any]) -> None:
1109 if (
1110 not self.parent.non_primary
1111 and not mapper.class_manager._attr_has_impl(self.key)
1112 ):
1113 self.strategy.init_class_attribute(mapper)
1114
1115 _all_strategies: collections.defaultdict[
1116 Type[MapperProperty[Any]], Dict[_StrategyKey, Type[LoaderStrategy]]
1117 ] = collections.defaultdict(dict)
1118
1119 @classmethod
1120 def strategy_for(cls, **kw: Any) -> Callable[[_TLS], _TLS]:
1121 def decorate(dec_cls: _TLS) -> _TLS:
1122 # ensure each subclass of the strategy has its
1123 # own _strategy_keys collection
1124 if "_strategy_keys" not in dec_cls.__dict__:
1125 dec_cls._strategy_keys = []
1126 key = tuple(sorted(kw.items()))
1127 cls._all_strategies[cls][key] = dec_cls
1128 dec_cls._strategy_keys.append(key)
1129 return dec_cls
1130
1131 return decorate
1132
1133 @classmethod
1134 def _strategy_lookup(
1135 cls, requesting_property: MapperProperty[Any], *key: Any
1136 ) -> Type[LoaderStrategy]:
1137 requesting_property.parent._with_polymorphic_mappers
1138
1139 for prop_cls in cls.__mro__:
1140 if prop_cls in cls._all_strategies:
1141 if TYPE_CHECKING:
1142 assert issubclass(prop_cls, MapperProperty)
1143 strategies = cls._all_strategies[prop_cls]
1144 try:
1145 return strategies[key]
1146 except KeyError:
1147 pass
1148
1149 for property_type, strats in cls._all_strategies.items():
1150 if key in strats:
1151 intended_property_type = property_type
1152 actual_strategy = strats[key]
1153 break
1154 else:
1155 intended_property_type = None
1156 actual_strategy = None
1157
1158 raise orm_exc.LoaderStrategyException(
1159 cls,
1160 requesting_property,
1161 intended_property_type,
1162 actual_strategy,
1163 key,
1164 )
1165
1166
1167class ORMOption(ExecutableOption):
1168 """Base class for option objects that are passed to ORM queries.
1169
1170 These options may be consumed by :meth:`.Query.options`,
1171 :meth:`.Select.options`, or in a more general sense by any
1172 :meth:`.Executable.options` method. They are interpreted at
1173 statement compile time or execution time in modern use. The
1174 deprecated :class:`.MapperOption` is consumed at ORM query construction
1175 time.
1176
1177 .. versionadded:: 1.4
1178
1179 """
1180
1181 __slots__ = ()
1182
1183 _is_legacy_option = False
1184
1185 propagate_to_loaders = False
1186 """if True, indicate this option should be carried along
1187 to "secondary" SELECT statements that occur for relationship
1188 lazy loaders as well as attribute load / refresh operations.
1189
1190 """
1191
1192 _is_core = False
1193
1194 _is_user_defined = False
1195
1196 _is_compile_state = False
1197
1198 _is_criteria_option = False
1199
1200 _is_strategy_option = False
1201
1202 def _adapt_cached_option_to_uncached_option(
1203 self, context: QueryContext, uncached_opt: ORMOption
1204 ) -> ORMOption:
1205 """adapt this option to the "uncached" version of itself in a
1206 loader strategy context.
1207
1208 given "self" which is an option from a cached query, as well as the
1209 corresponding option from the uncached version of the same query,
1210 return the option we should use in a new query, in the context of a
1211 loader strategy being asked to load related rows on behalf of that
1212 cached query, which is assumed to be building a new query based on
1213 entities passed to us from the cached query.
1214
1215 Currently this routine chooses between "self" and "uncached" without
1216 manufacturing anything new. If the option is itself a loader strategy
1217 option which has a path, that path needs to match to the entities being
1218 passed to us by the cached query, so the :class:`_orm.Load` subclass
1219 overrides this to return "self". For all other options, we return the
1220 uncached form which may have changing state, such as a
1221 with_loader_criteria() option which will very often have new state.
1222
1223 This routine could in the future involve
1224 generating a new option based on both inputs if use cases arise,
1225 such as if with_loader_criteria() needed to match up to
1226 ``AliasedClass`` instances given in the parent query.
1227
1228 However, longer term it might be better to restructure things such that
1229 ``AliasedClass`` entities are always matched up on their cache key,
1230 instead of identity, in things like paths and such, so that this whole
1231 issue of "the uncached option does not match the entities" goes away.
1232 However this would make ``PathRegistry`` more complicated and difficult
1233 to debug as well as potentially less performant in that it would be
1234 hashing enormous cache keys rather than a simple AliasedInsp. UNLESS,
1235 we could get cache keys overall to be reliably hashed into something
1236 like an md5 key.
1237
1238 .. versionadded:: 1.4.41
1239
1240 """
1241 if uncached_opt is not None:
1242 return uncached_opt
1243 else:
1244 return self
1245
1246
1247class CompileStateOption(HasCacheKey, ORMOption):
1248 """base for :class:`.ORMOption` classes that affect the compilation of
1249 a SQL query and therefore need to be part of the cache key.
1250
1251 .. note:: :class:`.CompileStateOption` is generally non-public and
1252 should not be used as a base class for user-defined options; instead,
1253 use :class:`.UserDefinedOption`, which is easier to use as it does not
1254 interact with ORM compilation internals or caching.
1255
1256 :class:`.CompileStateOption` defines an internal attribute
1257 ``_is_compile_state=True`` which has the effect of the ORM compilation
1258 routines for SELECT and other statements will call upon these options when
1259 a SQL string is being compiled. As such, these classes implement
1260 :class:`.HasCacheKey` and need to provide robust ``_cache_key_traversal``
1261 structures.
1262
1263 The :class:`.CompileStateOption` class is used to implement the ORM
1264 :class:`.LoaderOption` and :class:`.CriteriaOption` classes.
1265
1266 .. versionadded:: 1.4.28
1267
1268
1269 """
1270
1271 __slots__ = ()
1272
1273 _is_compile_state = True
1274
1275 def process_compile_state(self, compile_state: ORMCompileState) -> None:
1276 """Apply a modification to a given :class:`.ORMCompileState`.
1277
1278 This method is part of the implementation of a particular
1279 :class:`.CompileStateOption` and is only invoked internally
1280 when an ORM query is compiled.
1281
1282 """
1283
1284 def process_compile_state_replaced_entities(
1285 self,
1286 compile_state: ORMCompileState,
1287 mapper_entities: Sequence[_MapperEntity],
1288 ) -> None:
1289 """Apply a modification to a given :class:`.ORMCompileState`,
1290 given entities that were replaced by with_only_columns() or
1291 with_entities().
1292
1293 This method is part of the implementation of a particular
1294 :class:`.CompileStateOption` and is only invoked internally
1295 when an ORM query is compiled.
1296
1297 .. versionadded:: 1.4.19
1298
1299 """
1300
1301
1302class LoaderOption(CompileStateOption):
1303 """Describe a loader modification to an ORM statement at compilation time.
1304
1305 .. versionadded:: 1.4
1306
1307 """
1308
1309 __slots__ = ()
1310
1311 def process_compile_state_replaced_entities(
1312 self,
1313 compile_state: ORMCompileState,
1314 mapper_entities: Sequence[_MapperEntity],
1315 ) -> None:
1316 self.process_compile_state(compile_state)
1317
1318
1319class CriteriaOption(CompileStateOption):
1320 """Describe a WHERE criteria modification to an ORM statement at
1321 compilation time.
1322
1323 .. versionadded:: 1.4
1324
1325 """
1326
1327 __slots__ = ()
1328
1329 _is_criteria_option = True
1330
1331 def get_global_criteria(self, attributes: Dict[str, Any]) -> None:
1332 """update additional entity criteria options in the given
1333 attributes dictionary.
1334
1335 """
1336
1337
1338class UserDefinedOption(ORMOption):
1339 """Base class for a user-defined option that can be consumed from the
1340 :meth:`.SessionEvents.do_orm_execute` event hook.
1341
1342 """
1343
1344 __slots__ = ("payload",)
1345
1346 _is_legacy_option = False
1347
1348 _is_user_defined = True
1349
1350 propagate_to_loaders = False
1351 """if True, indicate this option should be carried along
1352 to "secondary" Query objects produced during lazy loads
1353 or refresh operations.
1354
1355 """
1356
1357 def __init__(self, payload: Optional[Any] = None):
1358 self.payload = payload
1359
1360
1361@util.deprecated_cls(
1362 "1.4",
1363 "The :class:`.MapperOption class is deprecated and will be removed "
1364 "in a future release. For "
1365 "modifications to queries on a per-execution basis, use the "
1366 ":class:`.UserDefinedOption` class to establish state within a "
1367 ":class:`.Query` or other Core statement, then use the "
1368 ":meth:`.SessionEvents.before_orm_execute` hook to consume them.",
1369 constructor=None,
1370)
1371class MapperOption(ORMOption):
1372 """Describe a modification to a Query"""
1373
1374 __slots__ = ()
1375
1376 _is_legacy_option = True
1377
1378 propagate_to_loaders = False
1379 """if True, indicate this option should be carried along
1380 to "secondary" Query objects produced during lazy loads
1381 or refresh operations.
1382
1383 """
1384
1385 def process_query(self, query: Query[Any]) -> None:
1386 """Apply a modification to the given :class:`_query.Query`."""
1387
1388 def process_query_conditionally(self, query: Query[Any]) -> None:
1389 """same as process_query(), except that this option may not
1390 apply to the given query.
1391
1392 This is typically applied during a lazy load or scalar refresh
1393 operation to propagate options stated in the original Query to the
1394 new Query being used for the load. It occurs for those options that
1395 specify propagate_to_loaders=True.
1396
1397 """
1398
1399 self.process_query(query)
1400
1401
1402class LoaderStrategy:
1403 """Describe the loading behavior of a StrategizedProperty object.
1404
1405 The ``LoaderStrategy`` interacts with the querying process in three
1406 ways:
1407
1408 * it controls the configuration of the ``InstrumentedAttribute``
1409 placed on a class to handle the behavior of the attribute. this
1410 may involve setting up class-level callable functions to fire
1411 off a select operation when the attribute is first accessed
1412 (i.e. a lazy load)
1413
1414 * it processes the ``QueryContext`` at statement construction time,
1415 where it can modify the SQL statement that is being produced.
1416 For example, simple column attributes will add their represented
1417 column to the list of selected columns, a joined eager loader
1418 may establish join clauses to add to the statement.
1419
1420 * It produces "row processor" functions at result fetching time.
1421 These "row processor" functions populate a particular attribute
1422 on a particular mapped instance.
1423
1424 """
1425
1426 __slots__ = (
1427 "parent_property",
1428 "is_class_level",
1429 "parent",
1430 "key",
1431 "strategy_key",
1432 "strategy_opts",
1433 )
1434
1435 _strategy_keys: ClassVar[List[_StrategyKey]]
1436
1437 def __init__(
1438 self, parent: MapperProperty[Any], strategy_key: _StrategyKey
1439 ):
1440 self.parent_property = parent
1441 self.is_class_level = False
1442 self.parent = self.parent_property.parent
1443 self.key = self.parent_property.key
1444 self.strategy_key = strategy_key
1445 self.strategy_opts = dict(strategy_key)
1446
1447 def init_class_attribute(self, mapper: Mapper[Any]) -> None:
1448 pass
1449
1450 def setup_query(
1451 self,
1452 compile_state: ORMCompileState,
1453 query_entity: _MapperEntity,
1454 path: AbstractEntityRegistry,
1455 loadopt: Optional[_LoadElement],
1456 adapter: Optional[ORMAdapter],
1457 **kwargs: Any,
1458 ) -> None:
1459 """Establish column and other state for a given QueryContext.
1460
1461 This method fulfills the contract specified by MapperProperty.setup().
1462
1463 StrategizedProperty delegates its setup() method
1464 directly to this method.
1465
1466 """
1467
1468 def create_row_processor(
1469 self,
1470 context: ORMCompileState,
1471 query_entity: _MapperEntity,
1472 path: AbstractEntityRegistry,
1473 loadopt: Optional[_LoadElement],
1474 mapper: Mapper[Any],
1475 result: Result[Any],
1476 adapter: Optional[ORMAdapter],
1477 populators: _PopulatorDict,
1478 ) -> None:
1479 """Establish row processing functions for a given QueryContext.
1480
1481 This method fulfills the contract specified by
1482 MapperProperty.create_row_processor().
1483
1484 StrategizedProperty delegates its create_row_processor() method
1485 directly to this method.
1486
1487 """
1488
1489 def __str__(self) -> str:
1490 return str(self.parent_property)