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