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