codekingpro/portable-devtools
114k
1# orm/strategy_options.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# mypy: allow-untyped-defs, allow-untyped-calls
8
9"""
10
11"""
12
13from __future__ import annotations
14
15import typing
16from typing import Any
17from typing import Callable
18from typing import cast
19from typing import Dict
20from typing import Iterable
21from typing import Optional
22from typing import overload
23from typing import Sequence
24from typing import Tuple
25from typing import Type
26from typing import TypeVar
27from typing import Union
28
29from . import util as orm_util
30from ._typing import insp_is_aliased_class
31from ._typing import insp_is_attribute
32from ._typing import insp_is_mapper
33from ._typing import insp_is_mapper_property
34from .attributes import QueryableAttribute
35from .base import InspectionAttr
36from .interfaces import LoaderOption
37from .path_registry import _DEFAULT_TOKEN
38from .path_registry import _StrPathToken
39from .path_registry import _WILDCARD_TOKEN
40from .path_registry import AbstractEntityRegistry
41from .path_registry import path_is_property
42from .path_registry import PathRegistry
43from .path_registry import TokenRegistry
44from .util import _orm_full_deannotate
45from .util import AliasedInsp
46from .. import exc as sa_exc
47from .. import inspect
48from .. import util
49from ..sql import and_
50from ..sql import cache_key
51from ..sql import coercions
52from ..sql import roles
53from ..sql import traversals
54from ..sql import visitors
55from ..sql.base import _generative
56from ..util.typing import Final
57from ..util.typing import Literal
58from ..util.typing import Self
59
60_RELATIONSHIP_TOKEN: Final[Literal["relationship"]] = "relationship"
61_COLUMN_TOKEN: Final[Literal["column"]] = "column"
62
63_FN = TypeVar("_FN", bound="Callable[..., Any]")
64
65if typing.TYPE_CHECKING:
66 from ._typing import _EntityType
67 from ._typing import _InternalEntityType
68 from .context import _MapperEntity
69 from .context import ORMCompileState
70 from .context import QueryContext
71 from .interfaces import _StrategyKey
72 from .interfaces import MapperProperty
73 from .interfaces import ORMOption
74 from .mapper import Mapper
75 from .path_registry import _PathRepresentation
76 from ..sql._typing import _ColumnExpressionArgument
77 from ..sql._typing import _FromClauseArgument
78 from ..sql.cache_key import _CacheKeyTraversalType
79 from ..sql.cache_key import CacheKey
80
81
82_AttrType = Union[Literal["*"], "QueryableAttribute[Any]"]
83
84_WildcardKeyType = Literal["relationship", "column"]
85_StrategySpec = Dict[str, Any]
86_OptsType = Dict[str, Any]
87_AttrGroupType = Tuple[_AttrType, ...]
88
89
90class _AbstractLoad(traversals.GenerativeOnTraversal, LoaderOption):
91 __slots__ = ("propagate_to_loaders",)
92
93 _is_strategy_option = True
94 propagate_to_loaders: bool
95
96 def contains_eager(
97 self,
98 attr: _AttrType,
99 alias: Optional[_FromClauseArgument] = None,
100 _is_chain: bool = False,
101 _propagate_to_loaders: bool = False,
102 ) -> Self:
103 r"""Indicate that the given attribute should be eagerly loaded from
104 columns stated manually in the query.
105
106 This function is part of the :class:`_orm.Load` interface and supports
107 both method-chained and standalone operation.
108
109 The option is used in conjunction with an explicit join that loads
110 the desired rows, i.e.::
111
112 sess.query(Order).join(Order.user).options(
113 contains_eager(Order.user)
114 )
115
116 The above query would join from the ``Order`` entity to its related
117 ``User`` entity, and the returned ``Order`` objects would have the
118 ``Order.user`` attribute pre-populated.
119
120 It may also be used for customizing the entries in an eagerly loaded
121 collection; queries will normally want to use the
122 :ref:`orm_queryguide_populate_existing` execution option assuming the
123 primary collection of parent objects may already have been loaded::
124
125 sess.query(User).join(User.addresses).filter(
126 Address.email_address.like("%@aol.com")
127 ).options(contains_eager(User.addresses)).populate_existing()
128
129 See the section :ref:`contains_eager` for complete usage details.
130
131 .. seealso::
132
133 :ref:`loading_toplevel`
134
135 :ref:`contains_eager`
136
137 """
138 if alias is not None:
139 if not isinstance(alias, str):
140 coerced_alias = coercions.expect(roles.FromClauseRole, alias)
141 else:
142 util.warn_deprecated(
143 "Passing a string name for the 'alias' argument to "
144 "'contains_eager()` is deprecated, and will not work in a "
145 "future release. Please use a sqlalchemy.alias() or "
146 "sqlalchemy.orm.aliased() construct.",
147 version="1.4",
148 )
149 coerced_alias = alias
150
151 elif getattr(attr, "_of_type", None):
152 assert isinstance(attr, QueryableAttribute)
153 ot: Optional[_InternalEntityType[Any]] = inspect(attr._of_type)
154 assert ot is not None
155 coerced_alias = ot.selectable
156 else:
157 coerced_alias = None
158
159 cloned = self._set_relationship_strategy(
160 attr,
161 {"lazy": "joined"},
162 propagate_to_loaders=_propagate_to_loaders,
163 opts={"eager_from_alias": coerced_alias},
164 _reconcile_to_other=True if _is_chain else None,
165 )
166 return cloned
167
168 def load_only(self, *attrs: _AttrType, raiseload: bool = False) -> Self:
169 r"""Indicate that for a particular entity, only the given list
170 of column-based attribute names should be loaded; all others will be
171 deferred.
172
173 This function is part of the :class:`_orm.Load` interface and supports
174 both method-chained and standalone operation.
175
176 Example - given a class ``User``, load only the ``name`` and
177 ``fullname`` attributes::
178
179 session.query(User).options(load_only(User.name, User.fullname))
180
181 Example - given a relationship ``User.addresses -> Address``, specify
182 subquery loading for the ``User.addresses`` collection, but on each
183 ``Address`` object load only the ``email_address`` attribute::
184
185 session.query(User).options(
186 subqueryload(User.addresses).load_only(Address.email_address)
187 )
188
189 For a statement that has multiple entities,
190 the lead entity can be
191 specifically referred to using the :class:`_orm.Load` constructor::
192
193 stmt = (
194 select(User, Address)
195 .join(User.addresses)
196 .options(
197 Load(User).load_only(User.name, User.fullname),
198 Load(Address).load_only(Address.email_address),
199 )
200 )
201
202 When used together with the
203 :ref:`populate_existing <orm_queryguide_populate_existing>`
204 execution option only the attributes listed will be refreshed.
205
206 :param \*attrs: Attributes to be loaded, all others will be deferred.
207
208 :param raiseload: raise :class:`.InvalidRequestError` rather than
209 lazy loading a value when a deferred attribute is accessed. Used
210 to prevent unwanted SQL from being emitted.
211
212 .. versionadded:: 2.0
213
214 .. seealso::
215
216 :ref:`orm_queryguide_column_deferral` - in the
217 :ref:`queryguide_toplevel`
218
219 :param \*attrs: Attributes to be loaded, all others will be deferred.
220
221 :param raiseload: raise :class:`.InvalidRequestError` rather than
222 lazy loading a value when a deferred attribute is accessed. Used
223 to prevent unwanted SQL from being emitted.
224
225 .. versionadded:: 2.0
226
227 """
228 cloned = self._set_column_strategy(
229 attrs,
230 {"deferred": False, "instrument": True},
231 )
232
233 wildcard_strategy = {"deferred": True, "instrument": True}
234 if raiseload:
235 wildcard_strategy["raiseload"] = True
236
237 cloned = cloned._set_column_strategy(
238 ("*",),
239 wildcard_strategy,
240 )
241 return cloned
242
243 def joinedload(
244 self,
245 attr: _AttrType,
246 innerjoin: Optional[bool] = None,
247 ) -> Self:
248 """Indicate that the given attribute should be loaded using joined
249 eager loading.
250
251 This function is part of the :class:`_orm.Load` interface and supports
252 both method-chained and standalone operation.
253
254 examples::
255
256 # joined-load the "orders" collection on "User"
257 select(User).options(joinedload(User.orders))
258
259 # joined-load Order.items and then Item.keywords
260 select(Order).options(
261 joinedload(Order.items).joinedload(Item.keywords)
262 )
263
264 # lazily load Order.items, but when Items are loaded,
265 # joined-load the keywords collection
266 select(Order).options(
267 lazyload(Order.items).joinedload(Item.keywords)
268 )
269
270 :param innerjoin: if ``True``, indicates that the joined eager load
271 should use an inner join instead of the default of left outer join::
272
273 select(Order).options(joinedload(Order.user, innerjoin=True))
274
275 In order to chain multiple eager joins together where some may be
276 OUTER and others INNER, right-nested joins are used to link them::
277
278 select(A).options(
279 joinedload(A.bs, innerjoin=False).joinedload(
280 B.cs, innerjoin=True
281 )
282 )
283
284 The above query, linking A.bs via "outer" join and B.cs via "inner"
285 join would render the joins as "a LEFT OUTER JOIN (b JOIN c)". When
286 using older versions of SQLite (< 3.7.16), this form of JOIN is
287 translated to use full subqueries as this syntax is otherwise not
288 directly supported.
289
290 The ``innerjoin`` flag can also be stated with the term ``"unnested"``.
291 This indicates that an INNER JOIN should be used, *unless* the join
292 is linked to a LEFT OUTER JOIN to the left, in which case it
293 will render as LEFT OUTER JOIN. For example, supposing ``A.bs``
294 is an outerjoin::
295
296 select(A).options(
297 joinedload(A.bs).joinedload(B.cs, innerjoin="unnested")
298 )
299
300
301 The above join will render as "a LEFT OUTER JOIN b LEFT OUTER JOIN c",
302 rather than as "a LEFT OUTER JOIN (b JOIN c)".
303
304 .. note:: The "unnested" flag does **not** affect the JOIN rendered
305 from a many-to-many association table, e.g. a table configured as
306 :paramref:`_orm.relationship.secondary`, to the target table; for
307 correctness of results, these joins are always INNER and are
308 therefore right-nested if linked to an OUTER join.
309
310 .. note::
311
312 The joins produced by :func:`_orm.joinedload` are **anonymously
313 aliased**. The criteria by which the join proceeds cannot be
314 modified, nor can the ORM-enabled :class:`_sql.Select` or legacy
315 :class:`_query.Query` refer to these joins in any way, including
316 ordering. See :ref:`zen_of_eager_loading` for further detail.
317
318 To produce a specific SQL JOIN which is explicitly available, use
319 :meth:`_sql.Select.join` and :meth:`_query.Query.join`. To combine
320 explicit JOINs with eager loading of collections, use
321 :func:`_orm.contains_eager`; see :ref:`contains_eager`.
322
323 .. seealso::
324
325 :ref:`loading_toplevel`
326
327 :ref:`joined_eager_loading`
328
329 """
330 loader = self._set_relationship_strategy(
331 attr,
332 {"lazy": "joined"},
333 opts=(
334 {"innerjoin": innerjoin}
335 if innerjoin is not None
336 else util.EMPTY_DICT
337 ),
338 )
339 return loader
340
341 def subqueryload(self, attr: _AttrType) -> Self:
342 """Indicate that the given attribute should be loaded using
343 subquery eager loading.
344
345 This function is part of the :class:`_orm.Load` interface and supports
346 both method-chained and standalone operation.
347
348 examples::
349
350 # subquery-load the "orders" collection on "User"
351 select(User).options(subqueryload(User.orders))
352
353 # subquery-load Order.items and then Item.keywords
354 select(Order).options(
355 subqueryload(Order.items).subqueryload(Item.keywords)
356 )
357
358 # lazily load Order.items, but when Items are loaded,
359 # subquery-load the keywords collection
360 select(Order).options(
361 lazyload(Order.items).subqueryload(Item.keywords)
362 )
363
364
365 .. seealso::
366
367 :ref:`loading_toplevel`
368
369 :ref:`subquery_eager_loading`
370
371 """
372 return self._set_relationship_strategy(attr, {"lazy": "subquery"})
373
374 def selectinload(
375 self,
376 attr: _AttrType,
377 recursion_depth: Optional[int] = None,
378 ) -> Self:
379 """Indicate that the given attribute should be loaded using
380 SELECT IN eager loading.
381
382 This function is part of the :class:`_orm.Load` interface and supports
383 both method-chained and standalone operation.
384
385 examples::
386
387 # selectin-load the "orders" collection on "User"
388 select(User).options(selectinload(User.orders))
389
390 # selectin-load Order.items and then Item.keywords
391 select(Order).options(
392 selectinload(Order.items).selectinload(Item.keywords)
393 )
394
395 # lazily load Order.items, but when Items are loaded,
396 # selectin-load the keywords collection
397 select(Order).options(
398 lazyload(Order.items).selectinload(Item.keywords)
399 )
400
401 :param recursion_depth: optional int; when set to a positive integer
402 in conjunction with a self-referential relationship,
403 indicates "selectin" loading will continue that many levels deep
404 automatically until no items are found.
405
406 .. note:: The :paramref:`_orm.selectinload.recursion_depth` option
407 currently supports only self-referential relationships. There
408 is not yet an option to automatically traverse recursive structures
409 with more than one relationship involved.
410
411 Additionally, the :paramref:`_orm.selectinload.recursion_depth`
412 parameter is new and experimental and should be treated as "alpha"
413 status for the 2.0 series.
414
415 .. versionadded:: 2.0 added
416 :paramref:`_orm.selectinload.recursion_depth`
417
418
419 .. seealso::
420
421 :ref:`loading_toplevel`
422
423 :ref:`selectin_eager_loading`
424
425 """
426 return self._set_relationship_strategy(
427 attr,
428 {"lazy": "selectin"},
429 opts={"recursion_depth": recursion_depth},
430 )
431
432 def lazyload(self, attr: _AttrType) -> Self:
433 """Indicate that the given attribute should be loaded using "lazy"
434 loading.
435
436 This function is part of the :class:`_orm.Load` interface and supports
437 both method-chained and standalone operation.
438
439 .. seealso::
440
441 :ref:`loading_toplevel`
442
443 :ref:`lazy_loading`
444
445 """
446 return self._set_relationship_strategy(attr, {"lazy": "select"})
447
448 def immediateload(
449 self,
450 attr: _AttrType,
451 recursion_depth: Optional[int] = None,
452 ) -> Self:
453 """Indicate that the given attribute should be loaded using
454 an immediate load with a per-attribute SELECT statement.
455
456 The load is achieved using the "lazyloader" strategy and does not
457 fire off any additional eager loaders.
458
459 The :func:`.immediateload` option is superseded in general
460 by the :func:`.selectinload` option, which performs the same task
461 more efficiently by emitting a SELECT for all loaded objects.
462
463 This function is part of the :class:`_orm.Load` interface and supports
464 both method-chained and standalone operation.
465
466 :param recursion_depth: optional int; when set to a positive integer
467 in conjunction with a self-referential relationship,
468 indicates "selectin" loading will continue that many levels deep
469 automatically until no items are found.
470
471 .. note:: The :paramref:`_orm.immediateload.recursion_depth` option
472 currently supports only self-referential relationships. There
473 is not yet an option to automatically traverse recursive structures
474 with more than one relationship involved.
475
476 .. warning:: This parameter is new and experimental and should be
477 treated as "alpha" status
478
479 .. versionadded:: 2.0 added
480 :paramref:`_orm.immediateload.recursion_depth`
481
482
483 .. seealso::
484
485 :ref:`loading_toplevel`
486
487 :ref:`selectin_eager_loading`
488
489 """
490 loader = self._set_relationship_strategy(
491 attr,
492 {"lazy": "immediate"},
493 opts={"recursion_depth": recursion_depth},
494 )
495 return loader
496
497 def noload(self, attr: _AttrType) -> Self:
498 """Indicate that the given relationship attribute should remain
499 unloaded.
500
501 The relationship attribute will return ``None`` when accessed without
502 producing any loading effect.
503
504 This function is part of the :class:`_orm.Load` interface and supports
505 both method-chained and standalone operation.
506
507 :func:`_orm.noload` applies to :func:`_orm.relationship` attributes
508 only.
509
510 .. note:: Setting this loading strategy as the default strategy
511 for a relationship using the :paramref:`.orm.relationship.lazy`
512 parameter may cause issues with flushes, such if a delete operation
513 needs to load related objects and instead ``None`` was returned.
514
515 .. seealso::
516
517 :ref:`loading_toplevel`
518
519 """
520
521 return self._set_relationship_strategy(attr, {"lazy": "noload"})
522
523 def raiseload(self, attr: _AttrType, sql_only: bool = False) -> Self:
524 """Indicate that the given attribute should raise an error if accessed.
525
526 A relationship attribute configured with :func:`_orm.raiseload` will
527 raise an :exc:`~sqlalchemy.exc.InvalidRequestError` upon access. The
528 typical way this is useful is when an application is attempting to
529 ensure that all relationship attributes that are accessed in a
530 particular context would have been already loaded via eager loading.
531 Instead of having to read through SQL logs to ensure lazy loads aren't
532 occurring, this strategy will cause them to raise immediately.
533
534 :func:`_orm.raiseload` applies to :func:`_orm.relationship` attributes
535 only. In order to apply raise-on-SQL behavior to a column-based
536 attribute, use the :paramref:`.orm.defer.raiseload` parameter on the
537 :func:`.defer` loader option.
538
539 :param sql_only: if True, raise only if the lazy load would emit SQL,
540 but not if it is only checking the identity map, or determining that
541 the related value should just be None due to missing keys. When False,
542 the strategy will raise for all varieties of relationship loading.
543
544 This function is part of the :class:`_orm.Load` interface and supports
545 both method-chained and standalone operation.
546
547 .. seealso::
548
549 :ref:`loading_toplevel`
550
551 :ref:`prevent_lazy_with_raiseload`
552
553 :ref:`orm_queryguide_deferred_raiseload`
554
555 """
556
557 return self._set_relationship_strategy(
558 attr, {"lazy": "raise_on_sql" if sql_only else "raise"}
559 )
560
561 def defaultload(self, attr: _AttrType) -> Self:
562 """Indicate an attribute should load using its predefined loader style.
563
564 The behavior of this loading option is to not change the current
565 loading style of the attribute, meaning that the previously configured
566 one is used or, if no previous style was selected, the default
567 loading will be used.
568
569 This method is used to link to other loader options further into
570 a chain of attributes without altering the loader style of the links
571 along the chain. For example, to set joined eager loading for an
572 element of an element::
573
574 session.query(MyClass).options(
575 defaultload(MyClass.someattribute).joinedload(
576 MyOtherClass.someotherattribute
577 )
578 )
579
580 :func:`.defaultload` is also useful for setting column-level options on
581 a related class, namely that of :func:`.defer` and :func:`.undefer`::
582
583 session.scalars(
584 select(MyClass).options(
585 defaultload(MyClass.someattribute)
586 .defer("some_column")
587 .undefer("some_other_column")
588 )
589 )
590
591 .. seealso::
592
593 :ref:`orm_queryguide_relationship_sub_options`
594
595 :meth:`_orm.Load.options`
596
597 """
598 return self._set_relationship_strategy(attr, None)
599
600 def defer(self, key: _AttrType, raiseload: bool = False) -> Self:
601 r"""Indicate that the given column-oriented attribute should be
602 deferred, e.g. not loaded until accessed.
603
604 This function is part of the :class:`_orm.Load` interface and supports
605 both method-chained and standalone operation.
606
607 e.g.::
608
609 from sqlalchemy.orm import defer
610
611 session.query(MyClass).options(
612 defer(MyClass.attribute_one),
613 defer(MyClass.attribute_two)
614 )
615
616 To specify a deferred load of an attribute on a related class,
617 the path can be specified one token at a time, specifying the loading
618 style for each link along the chain. To leave the loading style
619 for a link unchanged, use :func:`_orm.defaultload`::
620
621 session.query(MyClass).options(
622 defaultload(MyClass.someattr).defer(RelatedClass.some_column)
623 )
624
625 Multiple deferral options related to a relationship can be bundled
626 at once using :meth:`_orm.Load.options`::
627
628
629 select(MyClass).options(
630 defaultload(MyClass.someattr).options(
631 defer(RelatedClass.some_column),
632 defer(RelatedClass.some_other_column),
633 defer(RelatedClass.another_column)
634 )
635 )
636
637 :param key: Attribute to be deferred.
638
639 :param raiseload: raise :class:`.InvalidRequestError` rather than
640 lazy loading a value when the deferred attribute is accessed. Used
641 to prevent unwanted SQL from being emitted.
642
643 .. versionadded:: 1.4
644
645 .. seealso::
646
647 :ref:`orm_queryguide_column_deferral` - in the
648 :ref:`queryguide_toplevel`
649
650 :func:`_orm.load_only`
651
652 :func:`_orm.undefer`
653
654 """
655 strategy = {"deferred": True, "instrument": True}
656 if raiseload:
657 strategy["raiseload"] = True
658 return self._set_column_strategy((key,), strategy)
659
660 def undefer(self, key: _AttrType) -> Self:
661 r"""Indicate that the given column-oriented attribute should be
662 undeferred, e.g. specified within the SELECT statement of the entity
663 as a whole.
664
665 The column being undeferred is typically set up on the mapping as a
666 :func:`.deferred` attribute.
667
668 This function is part of the :class:`_orm.Load` interface and supports
669 both method-chained and standalone operation.
670
671 Examples::
672
673 # undefer two columns
674 session.query(MyClass).options(
675 undefer(MyClass.col1), undefer(MyClass.col2)
676 )
677
678 # undefer all columns specific to a single class using Load + *
679 session.query(MyClass, MyOtherClass).options(
680 Load(MyClass).undefer("*")
681 )
682
683 # undefer a column on a related object
684 select(MyClass).options(
685 defaultload(MyClass.items).undefer(MyClass.text)
686 )
687
688 :param key: Attribute to be undeferred.
689
690 .. seealso::
691
692 :ref:`orm_queryguide_column_deferral` - in the
693 :ref:`queryguide_toplevel`
694
695 :func:`_orm.defer`
696
697 :func:`_orm.undefer_group`
698
699 """
700 return self._set_column_strategy(
701 (key,), {"deferred": False, "instrument": True}
702 )
703
704 def undefer_group(self, name: str) -> Self:
705 """Indicate that columns within the given deferred group name should be
706 undeferred.
707
708 The columns being undeferred are set up on the mapping as
709 :func:`.deferred` attributes and include a "group" name.
710
711 E.g::
712
713 session.query(MyClass).options(undefer_group("large_attrs"))
714
715 To undefer a group of attributes on a related entity, the path can be
716 spelled out using relationship loader options, such as
717 :func:`_orm.defaultload`::
718
719 select(MyClass).options(
720 defaultload("someattr").undefer_group("large_attrs")
721 )
722
723 .. seealso::
724
725 :ref:`orm_queryguide_column_deferral` - in the
726 :ref:`queryguide_toplevel`
727
728 :func:`_orm.defer`
729
730 :func:`_orm.undefer`
731
732 """
733 return self._set_column_strategy(
734 (_WILDCARD_TOKEN,), None, {f"undefer_group_{name}": True}
735 )
736
737 def with_expression(
738 self,
739 key: _AttrType,
740 expression: _ColumnExpressionArgument[Any],
741 ) -> Self:
742 r"""Apply an ad-hoc SQL expression to a "deferred expression"
743 attribute.
744
745 This option is used in conjunction with the
746 :func:`_orm.query_expression` mapper-level construct that indicates an
747 attribute which should be the target of an ad-hoc SQL expression.
748
749 E.g.::
750
751 stmt = select(SomeClass).options(
752 with_expression(SomeClass.x_y_expr, SomeClass.x + SomeClass.y)
753 )
754
755 .. versionadded:: 1.2
756
757 :param key: Attribute to be populated
758
759 :param expr: SQL expression to be applied to the attribute.
760
761 .. seealso::
762
763 :ref:`orm_queryguide_with_expression` - background and usage
764 examples
765
766 """
767
768 expression = _orm_full_deannotate(
769 coercions.expect(roles.LabeledColumnExprRole, expression)
770 )
771
772 return self._set_column_strategy(
773 (key,), {"query_expression": True}, extra_criteria=(expression,)
774 )
775
776 def selectin_polymorphic(self, classes: Iterable[Type[Any]]) -> Self:
777 """Indicate an eager load should take place for all attributes
778 specific to a subclass.
779
780 This uses an additional SELECT with IN against all matched primary
781 key values, and is the per-query analogue to the ``"selectin"``
782 setting on the :paramref:`.mapper.polymorphic_load` parameter.
783
784 .. versionadded:: 1.2
785
786 .. seealso::
787
788 :ref:`polymorphic_selectin`
789
790 """
791 self = self._set_class_strategy(
792 {"selectinload_polymorphic": True},
793 opts={
794 "entities": tuple(
795 sorted((inspect(cls) for cls in classes), key=id)
796 )
797 },
798 )
799 return self
800
801 @overload
802 def _coerce_strat(self, strategy: _StrategySpec) -> _StrategyKey: ...
803
804 @overload
805 def _coerce_strat(self, strategy: Literal[None]) -> None: ...
806
807 def _coerce_strat(
808 self, strategy: Optional[_StrategySpec]
809 ) -> Optional[_StrategyKey]:
810 if strategy is not None:
811 strategy_key = tuple(sorted(strategy.items()))
812 else:
813 strategy_key = None
814 return strategy_key
815
816 @_generative
817 def _set_relationship_strategy(
818 self,
819 attr: _AttrType,
820 strategy: Optional[_StrategySpec],
821 propagate_to_loaders: bool = True,
822 opts: Optional[_OptsType] = None,
823 _reconcile_to_other: Optional[bool] = None,
824 ) -> Self:
825 strategy_key = self._coerce_strat(strategy)
826
827 self._clone_for_bind_strategy(
828 (attr,),
829 strategy_key,
830 _RELATIONSHIP_TOKEN,
831 opts=opts,
832 propagate_to_loaders=propagate_to_loaders,
833 reconcile_to_other=_reconcile_to_other,
834 )
835 return self
836
837 @_generative
838 def _set_column_strategy(
839 self,
840 attrs: Tuple[_AttrType, ...],
841 strategy: Optional[_StrategySpec],
842 opts: Optional[_OptsType] = None,
843 extra_criteria: Optional[Tuple[Any, ...]] = None,
844 ) -> Self:
845 strategy_key = self._coerce_strat(strategy)
846
847 self._clone_for_bind_strategy(
848 attrs,
849 strategy_key,
850 _COLUMN_TOKEN,
851 opts=opts,
852 attr_group=attrs,
853 extra_criteria=extra_criteria,
854 )
855 return self
856
857 @_generative
858 def _set_generic_strategy(
859 self,
860 attrs: Tuple[_AttrType, ...],
861 strategy: _StrategySpec,
862 _reconcile_to_other: Optional[bool] = None,
863 ) -> Self:
864 strategy_key = self._coerce_strat(strategy)
865 self._clone_for_bind_strategy(
866 attrs,
867 strategy_key,
868 None,
869 propagate_to_loaders=True,
870 reconcile_to_other=_reconcile_to_other,
871 )
872 return self
873
874 @_generative
875 def _set_class_strategy(
876 self, strategy: _StrategySpec, opts: _OptsType
877 ) -> Self:
878 strategy_key = self._coerce_strat(strategy)
879
880 self._clone_for_bind_strategy(None, strategy_key, None, opts=opts)
881 return self
882
883 def _apply_to_parent(self, parent: Load) -> None:
884 """apply this :class:`_orm._AbstractLoad` object as a sub-option o
885 a :class:`_orm.Load` object.
886
887 Implementation is provided by subclasses.
888
889 """
890 raise NotImplementedError()
891
892 def options(self, *opts: _AbstractLoad) -> Self:
893 r"""Apply a series of options as sub-options to this
894 :class:`_orm._AbstractLoad` object.
895
896 Implementation is provided by subclasses.
897
898 """
899 raise NotImplementedError()
900
901 def _clone_for_bind_strategy(
902 self,
903 attrs: Optional[Tuple[_AttrType, ...]],
904 strategy: Optional[_StrategyKey],
905 wildcard_key: Optional[_WildcardKeyType],
906 opts: Optional[_OptsType] = None,
907 attr_group: Optional[_AttrGroupType] = None,
908 propagate_to_loaders: bool = True,
909 reconcile_to_other: Optional[bool] = None,
910 extra_criteria: Optional[Tuple[Any, ...]] = None,
911 ) -> Self:
912 raise NotImplementedError()
913
914 def process_compile_state_replaced_entities(
915 self,
916 compile_state: ORMCompileState,
917 mapper_entities: Sequence[_MapperEntity],
918 ) -> None:
919 if not compile_state.compile_options._enable_eagerloads:
920 return
921
922 # process is being run here so that the options given are validated
923 # against what the lead entities were, as well as to accommodate
924 # for the entities having been replaced with equivalents
925 self._process(
926 compile_state,
927 mapper_entities,
928 not bool(compile_state.current_path),
929 )
930
931 def process_compile_state(self, compile_state: ORMCompileState) -> None:
932 if not compile_state.compile_options._enable_eagerloads:
933 return
934
935 self._process(
936 compile_state,
937 compile_state._lead_mapper_entities,
938 not bool(compile_state.current_path)
939 and not compile_state.compile_options._for_refresh_state,
940 )
941
942 def _process(
943 self,
944 compile_state: ORMCompileState,
945 mapper_entities: Sequence[_MapperEntity],
946 raiseerr: bool,
947 ) -> None:
948 """implemented by subclasses"""
949 raise NotImplementedError()
950
951 @classmethod
952 def _chop_path(
953 cls,
954 to_chop: _PathRepresentation,
955 path: PathRegistry,
956 debug: bool = False,
957 ) -> Optional[_PathRepresentation]:
958 i = -1
959
960 for i, (c_token, p_token) in enumerate(
961 zip(to_chop, path.natural_path)
962 ):
963 if isinstance(c_token, str):
964 if i == 0 and (
965 c_token.endswith(f":{_DEFAULT_TOKEN}")
966 or c_token.endswith(f":{_WILDCARD_TOKEN}")
967 ):
968 return to_chop
969 elif (
970 c_token != f"{_RELATIONSHIP_TOKEN}:{_WILDCARD_TOKEN}"
971 and c_token != p_token.key # type: ignore
972 ):
973 return None
974
975 if c_token is p_token:
976 continue
977 elif (
978 isinstance(c_token, InspectionAttr)
979 and insp_is_mapper(c_token)
980 and insp_is_mapper(p_token)
981 and c_token.isa(p_token)
982 ):
983 continue
984
985 else:
986 return None
987 return to_chop[i + 1 :]
988
989
990class Load(_AbstractLoad):
991 """Represents loader options which modify the state of a
992 ORM-enabled :class:`_sql.Select` or a legacy :class:`_query.Query` in
993 order to affect how various mapped attributes are loaded.
994
995 The :class:`_orm.Load` object is in most cases used implicitly behind the
996 scenes when one makes use of a query option like :func:`_orm.joinedload`,
997 :func:`_orm.defer`, or similar. It typically is not instantiated directly
998 except for in some very specific cases.
999
1000 .. seealso::
1001
1002 :ref:`orm_queryguide_relationship_per_entity_wildcard` - illustrates an
1003 example where direct use of :class:`_orm.Load` may be useful
1004
1005 """
1006
1007 __slots__ = (
1008 "path",
1009 "context",
1010 "additional_source_entities",
1011 )
1012
1013 _traverse_internals = [
1014 ("path", visitors.ExtendedInternalTraversal.dp_has_cache_key),
1015 (
1016 "context",
1017 visitors.InternalTraversal.dp_has_cache_key_list,
1018 ),
1019 ("propagate_to_loaders", visitors.InternalTraversal.dp_boolean),
1020 (
1021 "additional_source_entities",
1022 visitors.InternalTraversal.dp_has_cache_key_list,
1023 ),
1024 ]
1025 _cache_key_traversal = None
1026
1027 path: PathRegistry
1028 context: Tuple[_LoadElement, ...]
1029 additional_source_entities: Tuple[_InternalEntityType[Any], ...]
1030
1031 def __init__(self, entity: _EntityType[Any]):
1032 insp = cast("Union[Mapper[Any], AliasedInsp[Any]]", inspect(entity))
1033 insp._post_inspect
1034
1035 self.path = insp._path_registry
1036 self.context = ()
1037 self.propagate_to_loaders = False
1038 self.additional_source_entities = ()
1039
1040 def __str__(self) -> str:
1041 return f"Load({self.path[0]})"
1042
1043 @classmethod
1044 def _construct_for_existing_path(
1045 cls, path: AbstractEntityRegistry
1046 ) -> Load:
1047 load = cls.__new__(cls)
1048 load.path = path
1049 load.context = ()
1050 load.propagate_to_loaders = False
1051 load.additional_source_entities = ()
1052 return load
1053
1054 def _adapt_cached_option_to_uncached_option(
1055 self, context: QueryContext, uncached_opt: ORMOption
1056 ) -> ORMOption:
1057 if uncached_opt is self:
1058 return self
1059 return self._adjust_for_extra_criteria(context)
1060
1061 def _prepend_path(self, path: PathRegistry) -> Load:
1062 cloned = self._clone()
1063 cloned.context = tuple(
1064 element._prepend_path(path) for element in self.context
1065 )
1066 return cloned
1067
1068 def _adjust_for_extra_criteria(self, context: QueryContext) -> Load:
1069 """Apply the current bound parameters in a QueryContext to all
1070 occurrences "extra_criteria" stored within this ``Load`` object,
1071 returning a new instance of this ``Load`` object.
1072
1073 """
1074
1075 # avoid generating cache keys for the queries if we don't
1076 # actually have any extra_criteria options, which is the
1077 # common case
1078 for value in self.context:
1079 if value._extra_criteria:
1080 break
1081 else:
1082 return self
1083
1084 replacement_cache_key = context.query._generate_cache_key()
1085
1086 if replacement_cache_key is None:
1087 return self
1088
1089 orig_query = context.compile_state.select_statement
1090 orig_cache_key = orig_query._generate_cache_key()
1091 assert orig_cache_key is not None
1092
1093 def process(
1094 opt: _LoadElement,
1095 replacement_cache_key: CacheKey,
1096 orig_cache_key: CacheKey,
1097 ) -> _LoadElement:
1098 cloned_opt = opt._clone()
1099
1100 cloned_opt._extra_criteria = tuple(
1101 replacement_cache_key._apply_params_to_element(
1102 orig_cache_key, crit
1103 )
1104 for crit in cloned_opt._extra_criteria
1105 )
1106
1107 return cloned_opt
1108
1109 cloned = self._clone()
1110 cloned.context = tuple(
1111 (
1112 process(value, replacement_cache_key, orig_cache_key)
1113 if value._extra_criteria
1114 else value
1115 )
1116 for value in self.context
1117 )
1118 return cloned
1119
1120 def _reconcile_query_entities_with_us(self, mapper_entities, raiseerr):
1121 """called at process time to allow adjustment of the root
1122 entity inside of _LoadElement objects.
1123
1124 """
1125 path = self.path
1126
1127 ezero = None
1128 for ent in mapper_entities:
1129 ezero = ent.entity_zero
1130 if ezero and orm_util._entity_corresponds_to(
1131 # technically this can be a token also, but this is
1132 # safe to pass to _entity_corresponds_to()
1133 ezero,
1134 cast("_InternalEntityType[Any]", path[0]),
1135 ):
1136 return ezero
1137
1138 return None
1139
1140 def _process(
1141 self,
1142 compile_state: ORMCompileState,
1143 mapper_entities: Sequence[_MapperEntity],
1144 raiseerr: bool,
1145 ) -> None:
1146 reconciled_lead_entity = self._reconcile_query_entities_with_us(
1147 mapper_entities, raiseerr
1148 )
1149
1150 # if the context has a current path, this is a lazy load
1151 has_current_path = bool(compile_state.compile_options._current_path)
1152
1153 for loader in self.context:
1154 # issue #11292
1155 # historically, propagate_to_loaders was only considered at
1156 # object loading time, whether or not to carry along options
1157 # onto an object's loaded state where it would be used by lazyload.
1158 # however, the defaultload() option needs to propagate in case
1159 # its sub-options propagate_to_loaders, but its sub-options
1160 # that dont propagate should not be applied for lazy loaders.
1161 # so we check again
1162 if has_current_path and not loader.propagate_to_loaders:
1163 continue
1164 loader.process_compile_state(
1165 self,
1166 compile_state,
1167 mapper_entities,
1168 reconciled_lead_entity,
1169 raiseerr,
1170 )
1171
1172 def _apply_to_parent(self, parent: Load) -> None:
1173 """apply this :class:`_orm.Load` object as a sub-option of another
1174 :class:`_orm.Load` object.
1175
1176 This method is used by the :meth:`_orm.Load.options` method.
1177
1178 """
1179 cloned = self._generate()
1180
1181 assert cloned.propagate_to_loaders == self.propagate_to_loaders
1182
1183 if not any(
1184 orm_util._entity_corresponds_to_use_path_impl(
1185 elem, cloned.path.odd_element(0)
1186 )
1187 for elem in (parent.path.odd_element(-1),)
1188 + parent.additional_source_entities
1189 ):
1190 if len(cloned.path) > 1:
1191 attrname = cloned.path[1]
1192 parent_entity = cloned.path[0]
1193 else:
1194 attrname = cloned.path[0]
1195 parent_entity = cloned.path[0]
1196 _raise_for_does_not_link(parent.path, attrname, parent_entity)
1197
1198 cloned.path = PathRegistry.coerce(parent.path[0:-1] + cloned.path[:])
1199
1200 if self.context:
