1# orm/strategy_options.py
2# Copyright (C) 2005-2025 the SQLAlchemy authors and contributors
3# <see AUTHORS file>
4#
5# This module is part of SQLAlchemy and is released under
6# the MIT License: https://www.opensource.org/licenses/mit-license.php
7# 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 Final
19from typing import Iterable
20from typing import Optional
21from typing import overload
22from typing import Sequence
23from typing import Tuple
24from typing import Type
25from typing import TypeVar
26from typing import Union
27
28from . import util as orm_util
29from ._typing import insp_is_aliased_class
30from ._typing import insp_is_attribute
31from ._typing import insp_is_mapper
32from ._typing import insp_is_mapper_property
33from .attributes import QueryableAttribute
34from .base import InspectionAttr
35from .interfaces import LoaderOption
36from .path_registry import _AbstractEntityRegistry
37from .path_registry import _DEFAULT_TOKEN
38from .path_registry import _StrPathToken
39from .path_registry import _TokenRegistry
40from .path_registry import _WILDCARD_TOKEN
41from .path_registry import path_is_property
42from .path_registry import PathRegistry
43from .util import _orm_full_deannotate
44from .util import AliasedInsp
45from .. import exc as sa_exc
46from .. import inspect
47from .. import util
48from ..sql import and_
49from ..sql import cache_key
50from ..sql import coercions
51from ..sql import roles
52from ..sql import traversals
53from ..sql import visitors
54from ..sql.base import _generative
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 @util.deprecated(
480 "2.1",
481 "The :func:`_orm.noload` option is deprecated and will be removed "
482 "in a future release. This option "
483 "produces incorrect results by returning ``None`` for related "
484 "items.",
485 )
486 def noload(self, attr: _AttrType) -> Self:
487 """Indicate that the given relationship attribute should remain
488 unloaded.
489
490 The relationship attribute will return ``None`` when accessed without
491 producing any loading effect.
492
493 :func:`_orm.noload` applies to :func:`_orm.relationship` attributes
494 only.
495
496 .. seealso::
497
498 :ref:`loading_toplevel`
499
500 """
501
502 return self._set_relationship_strategy(attr, {"lazy": "noload"})
503
504 def raiseload(self, attr: _AttrType, sql_only: bool = False) -> Self:
505 """Indicate that the given attribute should raise an error if accessed.
506
507 A relationship attribute configured with :func:`_orm.raiseload` will
508 raise an :exc:`~sqlalchemy.exc.InvalidRequestError` upon access. The
509 typical way this is useful is when an application is attempting to
510 ensure that all relationship attributes that are accessed in a
511 particular context would have been already loaded via eager loading.
512 Instead of having to read through SQL logs to ensure lazy loads aren't
513 occurring, this strategy will cause them to raise immediately.
514
515 :func:`_orm.raiseload` applies to :func:`_orm.relationship` attributes
516 only. In order to apply raise-on-SQL behavior to a column-based
517 attribute, use the :paramref:`.orm.defer.raiseload` parameter on the
518 :func:`.defer` loader option.
519
520 :param sql_only: if True, raise only if the lazy load would emit SQL,
521 but not if it is only checking the identity map, or determining that
522 the related value should just be None due to missing keys. When False,
523 the strategy will raise for all varieties of relationship loading.
524
525 This function is part of the :class:`_orm.Load` interface and supports
526 both method-chained and standalone operation.
527
528 .. seealso::
529
530 :ref:`loading_toplevel`
531
532 :ref:`prevent_lazy_with_raiseload`
533
534 :ref:`orm_queryguide_deferred_raiseload`
535
536 """
537
538 return self._set_relationship_strategy(
539 attr, {"lazy": "raise_on_sql" if sql_only else "raise"}
540 )
541
542 def defaultload(self, attr: _AttrType) -> Self:
543 """Indicate an attribute should load using its predefined loader style.
544
545 The behavior of this loading option is to not change the current
546 loading style of the attribute, meaning that the previously configured
547 one is used or, if no previous style was selected, the default
548 loading will be used.
549
550 This method is used to link to other loader options further into
551 a chain of attributes without altering the loader style of the links
552 along the chain. For example, to set joined eager loading for an
553 element of an element::
554
555 session.query(MyClass).options(
556 defaultload(MyClass.someattribute).joinedload(
557 MyOtherClass.someotherattribute
558 )
559 )
560
561 :func:`.defaultload` is also useful for setting column-level options on
562 a related class, namely that of :func:`.defer` and :func:`.undefer`::
563
564 session.scalars(
565 select(MyClass).options(
566 defaultload(MyClass.someattribute)
567 .defer("some_column")
568 .undefer("some_other_column")
569 )
570 )
571
572 .. seealso::
573
574 :ref:`orm_queryguide_relationship_sub_options`
575
576 :meth:`_orm.Load.options`
577
578 """
579 return self._set_relationship_strategy(attr, None)
580
581 def defer(self, key: _AttrType, raiseload: bool = False) -> Self:
582 r"""Indicate that the given column-oriented attribute should be
583 deferred, e.g. not loaded until accessed.
584
585 This function is part of the :class:`_orm.Load` interface and supports
586 both method-chained and standalone operation.
587
588 e.g.::
589
590 from sqlalchemy.orm import defer
591
592 session.query(MyClass).options(
593 defer(MyClass.attribute_one), defer(MyClass.attribute_two)
594 )
595
596 To specify a deferred load of an attribute on a related class,
597 the path can be specified one token at a time, specifying the loading
598 style for each link along the chain. To leave the loading style
599 for a link unchanged, use :func:`_orm.defaultload`::
600
601 session.query(MyClass).options(
602 defaultload(MyClass.someattr).defer(RelatedClass.some_column)
603 )
604
605 Multiple deferral options related to a relationship can be bundled
606 at once using :meth:`_orm.Load.options`::
607
608
609 select(MyClass).options(
610 defaultload(MyClass.someattr).options(
611 defer(RelatedClass.some_column),
612 defer(RelatedClass.some_other_column),
613 defer(RelatedClass.another_column),
614 )
615 )
616
617 :param key: Attribute to be deferred.
618
619 :param raiseload: raise :class:`.InvalidRequestError` rather than
620 lazy loading a value when the deferred attribute is accessed. Used
621 to prevent unwanted SQL from being emitted.
622
623 .. versionadded:: 1.4
624
625 .. seealso::
626
627 :ref:`orm_queryguide_column_deferral` - in the
628 :ref:`queryguide_toplevel`
629
630 :func:`_orm.load_only`
631
632 :func:`_orm.undefer`
633
634 """
635 strategy = {"deferred": True, "instrument": True}
636 if raiseload:
637 strategy["raiseload"] = True
638 return self._set_column_strategy(
639 _expand_column_strategy_attrs((key,)), strategy
640 )
641
642 def undefer(self, key: _AttrType) -> Self:
643 r"""Indicate that the given column-oriented attribute should be
644 undeferred, e.g. specified within the SELECT statement of the entity
645 as a whole.
646
647 The column being undeferred is typically set up on the mapping as a
648 :func:`.deferred` attribute.
649
650 This function is part of the :class:`_orm.Load` interface and supports
651 both method-chained and standalone operation.
652
653 Examples::
654
655 # undefer two columns
656 session.query(MyClass).options(
657 undefer(MyClass.col1), undefer(MyClass.col2)
658 )
659
660 # undefer all columns specific to a single class using Load + *
661 session.query(MyClass, MyOtherClass).options(Load(MyClass).undefer("*"))
662
663 # undefer a column on a related object
664 select(MyClass).options(defaultload(MyClass.items).undefer(MyClass.text))
665
666 :param key: Attribute to be undeferred.
667
668 .. seealso::
669
670 :ref:`orm_queryguide_column_deferral` - in the
671 :ref:`queryguide_toplevel`
672
673 :func:`_orm.defer`
674
675 :func:`_orm.undefer_group`
676
677 """ # noqa: E501
678 return self._set_column_strategy(
679 _expand_column_strategy_attrs((key,)),
680 {"deferred": False, "instrument": True},
681 )
682
683 def undefer_group(self, name: str) -> Self:
684 """Indicate that columns within the given deferred group name should be
685 undeferred.
686
687 The columns being undeferred are set up on the mapping as
688 :func:`.deferred` attributes and include a "group" name.
689
690 E.g::
691
692 session.query(MyClass).options(undefer_group("large_attrs"))
693
694 To undefer a group of attributes on a related entity, the path can be
695 spelled out using relationship loader options, such as
696 :func:`_orm.defaultload`::
697
698 select(MyClass).options(
699 defaultload("someattr").undefer_group("large_attrs")
700 )
701
702 .. seealso::
703
704 :ref:`orm_queryguide_column_deferral` - in the
705 :ref:`queryguide_toplevel`
706
707 :func:`_orm.defer`
708
709 :func:`_orm.undefer`
710
711 """
712 return self._set_column_strategy(
713 (_WILDCARD_TOKEN,), None, {f"undefer_group_{name}": True}
714 )
715
716 def with_expression(
717 self,
718 key: _AttrType,
719 expression: _ColumnExpressionArgument[Any],
720 ) -> Self:
721 r"""Apply an ad-hoc SQL expression to a "deferred expression"
722 attribute.
723
724 This option is used in conjunction with the
725 :func:`_orm.query_expression` mapper-level construct that indicates an
726 attribute which should be the target of an ad-hoc SQL expression.
727
728 E.g.::
729
730 stmt = select(SomeClass).options(
731 with_expression(SomeClass.x_y_expr, SomeClass.x + SomeClass.y)
732 )
733
734 :param key: Attribute to be populated
735
736 :param expr: SQL expression to be applied to the attribute.
737
738 .. seealso::
739
740 :ref:`orm_queryguide_with_expression` - background and usage
741 examples
742
743 """
744
745 expression = _orm_full_deannotate(
746 coercions.expect(roles.LabeledColumnExprRole, expression)
747 )
748
749 return self._set_column_strategy(
750 (key,), {"query_expression": True}, extra_criteria=(expression,)
751 )
752
753 def selectin_polymorphic(self, classes: Iterable[Type[Any]]) -> Self:
754 """Indicate an eager load should take place for all attributes
755 specific to a subclass.
756
757 This uses an additional SELECT with IN against all matched primary
758 key values, and is the per-query analogue to the ``"selectin"``
759 setting on the :paramref:`.mapper.polymorphic_load` parameter.
760
761 .. seealso::
762
763 :ref:`polymorphic_selectin`
764
765 """
766 self = self._set_class_strategy(
767 {"selectinload_polymorphic": True},
768 opts={
769 "entities": tuple(
770 sorted((inspect(cls) for cls in classes), key=id)
771 )
772 },
773 )
774 return self
775
776 @overload
777 def _coerce_strat(self, strategy: _StrategySpec) -> _StrategyKey: ...
778
779 @overload
780 def _coerce_strat(self, strategy: Literal[None]) -> None: ...
781
782 def _coerce_strat(
783 self, strategy: Optional[_StrategySpec]
784 ) -> Optional[_StrategyKey]:
785 if strategy is not None:
786 strategy_key = tuple(sorted(strategy.items()))
787 else:
788 strategy_key = None
789 return strategy_key
790
791 @_generative
792 def _set_relationship_strategy(
793 self,
794 attr: _AttrType,
795 strategy: Optional[_StrategySpec],
796 propagate_to_loaders: bool = True,
797 opts: Optional[_OptsType] = None,
798 _reconcile_to_other: Optional[bool] = None,
799 ) -> Self:
800 strategy_key = self._coerce_strat(strategy)
801
802 self._clone_for_bind_strategy(
803 (attr,),
804 strategy_key,
805 _RELATIONSHIP_TOKEN,
806 opts=opts,
807 propagate_to_loaders=propagate_to_loaders,
808 reconcile_to_other=_reconcile_to_other,
809 )
810 return self
811
812 @_generative
813 def _set_column_strategy(
814 self,
815 attrs: Tuple[_AttrType, ...],
816 strategy: Optional[_StrategySpec],
817 opts: Optional[_OptsType] = None,
818 extra_criteria: Optional[Tuple[Any, ...]] = None,
819 ) -> Self:
820 strategy_key = self._coerce_strat(strategy)
821
822 self._clone_for_bind_strategy(
823 attrs,
824 strategy_key,
825 _COLUMN_TOKEN,
826 opts=opts,
827 attr_group=attrs,
828 extra_criteria=extra_criteria,
829 )
830 return self
831
832 @_generative
833 def _set_generic_strategy(
834 self,
835 attrs: Tuple[_AttrType, ...],
836 strategy: _StrategySpec,
837 _reconcile_to_other: Optional[bool] = None,
838 ) -> Self:
839 strategy_key = self._coerce_strat(strategy)
840 self._clone_for_bind_strategy(
841 attrs,
842 strategy_key,
843 None,
844 propagate_to_loaders=True,
845 reconcile_to_other=_reconcile_to_other,
846 )
847 return self
848
849 @_generative
850 def _set_class_strategy(
851 self, strategy: _StrategySpec, opts: _OptsType
852 ) -> Self:
853 strategy_key = self._coerce_strat(strategy)
854
855 self._clone_for_bind_strategy(None, strategy_key, None, opts=opts)
856 return self
857
858 def _apply_to_parent(self, parent: Load) -> None:
859 """apply this :class:`_orm._AbstractLoad` object as a sub-option o
860 a :class:`_orm.Load` object.
861
862 Implementation is provided by subclasses.
863
864 """
865 raise NotImplementedError()
866
867 def options(self, *opts: _AbstractLoad) -> Self:
868 r"""Apply a series of options as sub-options to this
869 :class:`_orm._AbstractLoad` object.
870
871 Implementation is provided by subclasses.
872
873 """
874 raise NotImplementedError()
875
876 def _clone_for_bind_strategy(
877 self,
878 attrs: Optional[Tuple[_AttrType, ...]],
879 strategy: Optional[_StrategyKey],
880 wildcard_key: Optional[_WildcardKeyType],
881 opts: Optional[_OptsType] = None,
882 attr_group: Optional[_AttrGroupType] = None,
883 propagate_to_loaders: bool = True,
884 reconcile_to_other: Optional[bool] = None,
885 extra_criteria: Optional[Tuple[Any, ...]] = None,
886 ) -> Self:
887 raise NotImplementedError()
888
889 def process_compile_state_replaced_entities(
890 self,
891 compile_state: _ORMCompileState,
892 mapper_entities: Sequence[_MapperEntity],
893 ) -> None:
894 if not compile_state.compile_options._enable_eagerloads:
895 return
896
897 # process is being run here so that the options given are validated
898 # against what the lead entities were, as well as to accommodate
899 # for the entities having been replaced with equivalents
900 self._process(
901 compile_state,
902 mapper_entities,
903 not bool(compile_state.current_path),
904 )
905
906 def process_compile_state(self, compile_state: _ORMCompileState) -> None:
907 if not compile_state.compile_options._enable_eagerloads:
908 return
909
910 self._process(
911 compile_state,
912 compile_state._lead_mapper_entities,
913 not bool(compile_state.current_path)
914 and not compile_state.compile_options._for_refresh_state,
915 )
916
917 def _process(
918 self,
919 compile_state: _ORMCompileState,
920 mapper_entities: Sequence[_MapperEntity],
921 raiseerr: bool,
922 ) -> None:
923 """implemented by subclasses"""
924 raise NotImplementedError()
925
926 @classmethod
927 def _chop_path(
928 cls,
929 to_chop: _PathRepresentation,
930 path: PathRegistry,
931 debug: bool = False,
932 ) -> Optional[_PathRepresentation]:
933 i = -1
934
935 for i, (c_token, p_token) in enumerate(
936 zip(to_chop, path.natural_path)
937 ):
938 if isinstance(c_token, str):
939 if i == 0 and (
940 c_token.endswith(f":{_DEFAULT_TOKEN}")
941 or c_token.endswith(f":{_WILDCARD_TOKEN}")
942 ):
943 return to_chop
944 elif (
945 c_token != f"{_RELATIONSHIP_TOKEN}:{_WILDCARD_TOKEN}"
946 and c_token != p_token.key # type: ignore
947 ):
948 return None
949
950 if c_token is p_token:
951 continue
952 elif (
953 isinstance(c_token, InspectionAttr)
954 and insp_is_mapper(c_token)
955 and insp_is_mapper(p_token)
956 and c_token.isa(p_token)
957 ):
958 continue
959
960 else:
961 return None
962 return to_chop[i + 1 :]
963
964
965class Load(_AbstractLoad):
966 """Represents loader options which modify the state of a
967 ORM-enabled :class:`_sql.Select` or a legacy :class:`_query.Query` in
968 order to affect how various mapped attributes are loaded.
969
970 The :class:`_orm.Load` object is in most cases used implicitly behind the
971 scenes when one makes use of a query option like :func:`_orm.joinedload`,
972 :func:`_orm.defer`, or similar. It typically is not instantiated directly
973 except for in some very specific cases.
974
975 .. seealso::
976
977 :ref:`orm_queryguide_relationship_per_entity_wildcard` - illustrates an
978 example where direct use of :class:`_orm.Load` may be useful
979
980 """
981
982 __slots__ = (
983 "path",
984 "context",
985 "additional_source_entities",
986 )
987
988 _traverse_internals = [
989 ("path", visitors.ExtendedInternalTraversal.dp_has_cache_key),
990 (
991 "context",
992 visitors.InternalTraversal.dp_has_cache_key_list,
993 ),
994 ("propagate_to_loaders", visitors.InternalTraversal.dp_boolean),
995 (
996 "additional_source_entities",
997 visitors.InternalTraversal.dp_has_cache_key_list,
998 ),
999 ]
1000 _cache_key_traversal = None
1001
1002 path: PathRegistry
1003 context: Tuple[_LoadElement, ...]
1004 additional_source_entities: Tuple[_InternalEntityType[Any], ...]
1005
1006 def __init__(self, entity: _EntityType[Any]):
1007 insp = cast("Union[Mapper[Any], AliasedInsp[Any]]", inspect(entity))
1008 insp._post_inspect
1009
1010 self.path = insp._path_registry
1011 self.context = ()
1012 self.propagate_to_loaders = False
1013 self.additional_source_entities = ()
1014
1015 def __str__(self) -> str:
1016 return f"Load({self.path[0]})"
1017
1018 @classmethod
1019 def _construct_for_existing_path(
1020 cls, path: _AbstractEntityRegistry
1021 ) -> Load:
1022 load = cls.__new__(cls)
1023 load.path = path
1024 load.context = ()
1025 load.propagate_to_loaders = False
1026 load.additional_source_entities = ()
1027 return load
1028
1029 def _adapt_cached_option_to_uncached_option(
1030 self, context: QueryContext, uncached_opt: ORMOption
1031 ) -> ORMOption:
1032 if uncached_opt is self:
1033 return self
1034 return self._adjust_for_extra_criteria(context)
1035
1036 def _prepend_path(self, path: PathRegistry) -> Load:
1037 cloned = self._clone()
1038 cloned.context = tuple(
1039 element._prepend_path(path) for element in self.context
1040 )
1041 return cloned
1042
1043 def _adjust_for_extra_criteria(self, context: QueryContext) -> Load:
1044 """Apply the current bound parameters in a QueryContext to all
1045 occurrences "extra_criteria" stored within this ``Load`` object,
1046 returning a new instance of this ``Load`` object.
1047
1048 """
1049
1050 # avoid generating cache keys for the queries if we don't
1051 # actually have any extra_criteria options, which is the
1052 # common case
1053 for value in self.context:
1054 if value._extra_criteria:
1055 break
1056 else:
1057 return self
1058
1059 replacement_cache_key = context.user_passed_query._generate_cache_key()
1060
1061 if replacement_cache_key is None:
1062 return self
1063
1064 orig_query = context.compile_state.select_statement
1065 orig_cache_key = orig_query._generate_cache_key()
1066 assert orig_cache_key is not None
1067
1068 def process(
1069 opt: _LoadElement,
1070 replacement_cache_key: CacheKey,
1071 orig_cache_key: CacheKey,
1072 ) -> _LoadElement:
1073 cloned_opt = opt._clone()
1074
1075 cloned_opt._extra_criteria = tuple(
1076 replacement_cache_key._apply_params_to_element(
1077 orig_cache_key, crit
1078 )
1079 for crit in cloned_opt._extra_criteria
1080 )
1081
1082 return cloned_opt
1083
1084 cloned = self._clone()
1085 cloned.context = tuple(
1086 (
1087 process(value, replacement_cache_key, orig_cache_key)
1088 if value._extra_criteria
1089 else value
1090 )
1091 for value in self.context
1092 )
1093 return cloned
1094
1095 def _reconcile_query_entities_with_us(self, mapper_entities, raiseerr):
1096 """called at process time to allow adjustment of the root
1097 entity inside of _LoadElement objects.
1098
1099 """
1100 path = self.path
1101
1102 for ent in mapper_entities:
1103 ezero = ent.entity_zero
1104 if ezero and orm_util._entity_corresponds_to(
1105 # technically this can be a token also, but this is
1106 # safe to pass to _entity_corresponds_to()
1107 ezero,
1108 cast("_InternalEntityType[Any]", path[0]),
1109 ):
1110 return ezero
1111
1112 return None
1113
1114 def _process(
1115 self,
1116 compile_state: _ORMCompileState,
1117 mapper_entities: Sequence[_MapperEntity],
1118 raiseerr: bool,
1119 ) -> None:
1120 reconciled_lead_entity = self._reconcile_query_entities_with_us(
1121 mapper_entities, raiseerr
1122 )
1123
1124 # if the context has a current path, this is a lazy load
1125 has_current_path = bool(compile_state.compile_options._current_path)
1126
1127 for loader in self.context:
1128 # issue #11292
1129 # historically, propagate_to_loaders was only considered at
1130 # object loading time, whether or not to carry along options
1131 # onto an object's loaded state where it would be used by lazyload.
1132 # however, the defaultload() option needs to propagate in case
1133 # its sub-options propagate_to_loaders, but its sub-options
1134 # that dont propagate should not be applied for lazy loaders.
1135 # so we check again
1136 if has_current_path and not loader.propagate_to_loaders:
1137 continue
1138 loader.process_compile_state(
1139 self,
1140 compile_state,
1141 mapper_entities,
1142 reconciled_lead_entity,
1143 raiseerr,
1144 )
1145
1146 def _apply_to_parent(self, parent: Load) -> None:
1147 """apply this :class:`_orm.Load` object as a sub-option of another
1148 :class:`_orm.Load` object.
1149
1150 This method is used by the :meth:`_orm.Load.options` method.
1151
1152 """
1153 cloned = self._generate()
1154
1155 assert cloned.propagate_to_loaders == self.propagate_to_loaders
1156
1157 if not any(
1158 orm_util._entity_corresponds_to_use_path_impl(
1159 elem, cloned.path.odd_element(0)
1160 )
1161 for elem in (parent.path.odd_element(-1),)
1162 + parent.additional_source_entities
1163 ):
1164 if len(cloned.path) > 1:
1165 attrname = cloned.path[1]
1166 parent_entity = cloned.path[0]
1167 else:
1168 attrname = cloned.path[0]
1169 parent_entity = cloned.path[0]
1170 _raise_for_does_not_link(parent.path, attrname, parent_entity)
1171
1172 cloned.path = PathRegistry.coerce(parent.path[0:-1] + cloned.path[:])
1173
1174 if self.context:
1175 cloned.context = tuple(
1176 value._prepend_path_from(parent) for value in self.context
1177 )
1178
1179 if cloned.context:
1180 parent.context += cloned.context
1181 parent.additional_source_entities += (
1182 cloned.additional_source_entities
1183 )
1184
1185 @_generative
1186 def options(self, *opts: _AbstractLoad) -> Self:
1187 r"""Apply a series of options as sub-options to this
1188 :class:`_orm.Load`
1189 object.
1190
1191 E.g.::
1192
1193 query = session.query(Author)
1194 query = query.options(
1195 joinedload(Author.book).options(
1196 load_only(Book.summary, Book.excerpt),
1197 joinedload(Book.citations).options(joinedload(Citation.author)),
1198 )
1199 )
1200
1201 :param \*opts: A series of loader option objects (ultimately
1202 :class:`_orm.Load` objects) which should be applied to the path
1203 specified by this :class:`_orm.Load` object.
1204
1205 .. seealso::
1206
1207 :func:`.defaultload`
1208
1209 :ref:`orm_queryguide_relationship_sub_options`
1210
1211 """
1212 for opt in opts:
1213 try:
1214 opt._apply_to_parent(self)
1215 except AttributeError as ae:
1216 if not isinstance(opt, _AbstractLoad):
1217 raise sa_exc.ArgumentError(
1218 f"Loader option {opt} is not compatible with the "
1219 "Load.options() method."
1220 ) from ae
1221 else:
1222 raise
1223 return self
1224
1225 def _clone_for_bind_strategy(
1226 self,
1227 attrs: Optional[Tuple[_AttrType, ...]],
1228 strategy: Optional[_StrategyKey],
1229 wildcard_key: Optional[_WildcardKeyType],
1230 opts: Optional[_OptsType] = None,
1231 attr_group: Optional[_AttrGroupType] = None,
1232 propagate_to_loaders: bool = True,
1233 reconcile_to_other: Optional[bool] = None,
1234 extra_criteria: Optional[Tuple[Any, ...]] = None,
1235 ) -> Self:
1236 # for individual strategy that needs to propagate, set the whole
1237 # Load container to also propagate, so that it shows up in
1238 # InstanceState.load_options
1239 if propagate_to_loaders:
1240 self.propagate_to_loaders = True
1241
1242 if self.path.is_token:
1243 raise sa_exc.ArgumentError(
1244 "Wildcard token cannot be followed by another entity"
1245 )
1246
1247 elif path_is_property(self.path):
1248 # re-use the lookup which will raise a nicely formatted
1249 # LoaderStrategyException
1250 if strategy:
1251 self.path.prop._strategy_lookup(self.path.prop, strategy[0])
1252 else:
1253 raise sa_exc.ArgumentError(
1254 f"Mapped attribute '{self.path.prop}' does not "
1255 "refer to a mapped entity"
1256 )
1257
1258 if attrs is None:
1259 load_element = _ClassStrategyLoad.create(
1260 self.path,
1261 None,
1262 strategy,
1263 wildcard_key,
1264 opts,
1265 propagate_to_loaders,
1266 attr_group=attr_group,
1267 reconcile_to_other=reconcile_to_other,
1268 extra_criteria=extra_criteria,
1269 )
1270 if load_element:
1271 self.context += (load_element,)
1272 assert opts is not None
1273 self.additional_source_entities += cast(
1274 "Tuple[_InternalEntityType[Any]]", opts["entities"]
1275 )
1276
1277 else:
1278 for attr in attrs:
1279 if isinstance(attr, str):
1280 load_element = _TokenStrategyLoad.create(
1281 self.path,
1282 attr,
1283 strategy,
1284 wildcard_key,
1285 opts,
1286 propagate_to_loaders,
1287 attr_group=attr_group,
1288 reconcile_to_other=reconcile_to_other,
1289 extra_criteria=extra_criteria,
1290 )
1291 else:
1292 load_element = _AttributeStrategyLoad.create(
1293 self.path,
1294 attr,
1295 strategy,
1296 wildcard_key,
1297 opts,
1298 propagate_to_loaders,
1299 attr_group=attr_group,
1300 reconcile_to_other=reconcile_to_other,
1301 extra_criteria=extra_criteria,
1302 )
1303
1304 if load_element:
1305 # for relationship options, update self.path on this Load
1306 # object with the latest path.
1307 if wildcard_key is _RELATIONSHIP_TOKEN:
1308 self.path = load_element.path
1309 self.context += (load_element,)
1310
1311 # this seems to be effective for selectinloader,
1312 # giving the extra match to one more level deep.
1313 # but does not work for immediateloader, which still
1314 # must add additional options at load time
1315 if load_element.local_opts.get("recursion_depth", False):
1316 r1 = load_element._recurse()
1317 self.context += (r1,)
1318
1319 return self
1320
1321 def __getstate__(self):
1322 d = self._shallow_to_dict()
1323 d["path"] = self.path.serialize()
1324 return d
1325
1326 def __setstate__(self, state):
1327 state["path"] = PathRegistry.deserialize(state["path"])
1328 self._shallow_from_dict(state)
1329
1330
1331class _WildcardLoad(_AbstractLoad):
1332 """represent a standalone '*' load operation"""
1333
1334 __slots__ = ("strategy", "path", "local_opts")
1335
1336 _traverse_internals = [
1337 ("strategy", visitors.ExtendedInternalTraversal.dp_plain_obj),
1338 ("path", visitors.ExtendedInternalTraversal.dp_plain_obj),
1339 (
1340 "local_opts",
1341 visitors.ExtendedInternalTraversal.dp_string_multi_dict,
1342 ),
1343 ]
1344 cache_key_traversal: _CacheKeyTraversalType = None
1345
1346 strategy: Optional[Tuple[Any, ...]]
1347 local_opts: _OptsType
1348 path: Union[Tuple[()], Tuple[str]]
1349 propagate_to_loaders = False
1350
1351 def __init__(self) -> None:
1352 self.path = ()
1353 self.strategy = None
1354 self.local_opts = util.EMPTY_DICT
1355
1356 def _clone_for_bind_strategy(
1357 self,
1358 attrs,
1359 strategy,
1360 wildcard_key,
1361 opts=None,
1362 attr_group=None,
1363 propagate_to_loaders=True,
1364 reconcile_to_other=None,
1365 extra_criteria=None,
1366 ):
1367 assert attrs is not None
1368 attr = attrs[0]
1369 assert (
1370 wildcard_key
1371 and isinstance(attr, str)
1372 and attr in (_WILDCARD_TOKEN, _DEFAULT_TOKEN)
1373 )
1374
1375 attr = f"{wildcard_key}:{attr}"
1376
1377 self.strategy = strategy
1378 self.path = (attr,)
1379 if opts:
1380 self.local_opts = util.immutabledict(opts)
1381
1382 assert extra_criteria is None
1383
1384 def options(self, *opts: _AbstractLoad) -> Self:
1385 raise NotImplementedError("Star option does not support sub-options")
1386
1387 def _apply_to_parent(self, parent: Load) -> None:
1388 """apply this :class:`_orm._WildcardLoad` object as a sub-option of
1389 a :class:`_orm.Load` object.
1390
1391 This method is used by the :meth:`_orm.Load.options` method. Note
1392 that :class:`_orm.WildcardLoad` itself can't have sub-options, but
1393 it may be used as the sub-option of a :class:`_orm.Load` object.
1394
1395 """
1396 assert self.path
1397 attr = self.path[0]
1398 if attr.endswith(_DEFAULT_TOKEN):
1399 attr = f"{attr.split(':')[0]}:{_WILDCARD_TOKEN}"
1400
1401 effective_path = cast(_AbstractEntityRegistry, parent.path).token(attr)
1402
1403 assert effective_path.is_token
1404
1405 loader = _TokenStrategyLoad.create(
1406 effective_path,
1407 None,
1408 self.strategy,
1409 None,
1410 self.local_opts,
1411 self.propagate_to_loaders,
1412 )
1413
1414 parent.context += (loader,)
1415
1416 def _process(self, compile_state, mapper_entities, raiseerr):
1417 is_refresh = compile_state.compile_options._for_refresh_state
1418
1419 if is_refresh and not self.propagate_to_loaders:
1420 return
1421
1422 entities = [ent.entity_zero for ent in mapper_entities]
1423 current_path = compile_state.current_path
1424
1425 start_path: _PathRepresentation = self.path
1426
1427 if current_path:
1428 # TODO: no cases in test suite where we actually get
1429 # None back here
1430 new_path = self._chop_path(start_path, current_path)
1431 if new_path is None:
1432 return
1433
1434 # chop_path does not actually "chop" a wildcard token path,
1435 # just returns it
1436 assert new_path == start_path
1437
1438 # start_path is a single-token tuple
1439 assert start_path and len(start_path) == 1
1440
1441 token = start_path[0]
1442 assert isinstance(token, str)
1443 entity = self._find_entity_basestring(entities, token, raiseerr)
1444
1445 if not entity:
1446 return
1447
1448 path_element = entity
1449
1450 # transfer our entity-less state into a Load() object
1451 # with a real entity path. Start with the lead entity
1452 # we just located, then go through the rest of our path
1453 # tokens and populate into the Load().
1454
1455 assert isinstance(token, str)
1456 loader = _TokenStrategyLoad.create(
1457 path_element._path_registry,
1458 token,
1459 self.strategy,
1460 None,
1461 self.local_opts,
1462 self.propagate_to_loaders,
1463 raiseerr=raiseerr,
1464 )
1465 if not loader:
1466 return
1467
1468 assert loader.path.is_token
1469
1470 # don't pass a reconciled lead entity here
1471 loader.process_compile_state(
1472 self, compile_state, mapper_entities, None, raiseerr
1473 )
1474
1475 return loader
1476
1477 def _find_entity_basestring(
1478 self,
1479 entities: Iterable[_InternalEntityType[Any]],
1480 token: str,
1481 raiseerr: bool,
1482 ) -> Optional[_InternalEntityType[Any]]:
1483 if token.endswith(f":{_WILDCARD_TOKEN}"):
1484 if len(list(entities)) != 1:
1485 if raiseerr:
1486 raise sa_exc.ArgumentError(
1487 "Can't apply wildcard ('*') or load_only() "
1488 f"loader option to multiple entities "
1489 f"{', '.join(str(ent) for ent in entities)}. Specify "
1490 "loader options for each entity individually, such as "
1491 f"""{
1492 ", ".join(
1493 f"Load({ent}).some_option('*')"
1494 for ent in entities
1495 )
1496 }."""
1497 )
1498 elif token.endswith(_DEFAULT_TOKEN):
1499 raiseerr = False
1500
1501 for ent in entities:
1502 # return only the first _MapperEntity when searching
1503 # based on string prop name. Ideally object
1504 # attributes are used to specify more exactly.
1505 return ent
1506 else:
1507 if raiseerr:
1508 raise sa_exc.ArgumentError(
1509 "Query has only expression-based entities - "
1510 f'can\'t find property named "{token}".'
1511 )
1512 else:
1513 return None
1514
1515 def __getstate__(self) -> Dict[str, Any]:
1516 d = self._shallow_to_dict()
1517 return d
1518
1519 def __setstate__(self, state: Dict[str, Any]) -> None:
1520 self._shallow_from_dict(state)
1521
1522
1523class _LoadElement(
1524 cache_key.HasCacheKey, traversals.HasShallowCopy, visitors.Traversible
1525):
1526 """represents strategy information to select for a LoaderStrategy
1527 and pass options to it.
1528
1529 :class:`._LoadElement` objects provide the inner datastructure
1530 stored by a :class:`_orm.Load` object and are also the object passed
1531 to methods like :meth:`.LoaderStrategy.setup_query`.
1532
1533 .. versionadded:: 2.0
1534
1535 """
1536
1537 __slots__ = (
1538 "path",
1539 "strategy",
1540 "propagate_to_loaders",
1541 "local_opts",
1542 "_extra_criteria",
1543 "_reconcile_to_other",
1544 )
1545 __visit_name__ = "load_element"
1546
1547 _traverse_internals = [
1548 ("path", visitors.ExtendedInternalTraversal.dp_has_cache_key),
1549 ("strategy", visitors.ExtendedInternalTraversal.dp_plain_obj),
1550 (
1551 "local_opts",
1552 visitors.ExtendedInternalTraversal.dp_string_multi_dict,
1553 ),
1554 ("_extra_criteria", visitors.InternalTraversal.dp_clauseelement_list),
1555 ("propagate_to_loaders", visitors.InternalTraversal.dp_plain_obj),
1556 ("_reconcile_to_other", visitors.InternalTraversal.dp_plain_obj),
1557 ]
1558 _cache_key_traversal = None
1559
1560 _extra_criteria: Tuple[Any, ...]
1561
1562 _reconcile_to_other: Optional[bool]
1563 strategy: Optional[_StrategyKey]
1564 path: PathRegistry
1565 propagate_to_loaders: bool
1566
1567 local_opts: util.immutabledict[str, Any]
1568
1569 is_token_strategy: bool
1570 is_class_strategy: bool
1571
1572 def __hash__(self) -> int:
1573 return id(self)
1574
1575 def __eq__(self, other):
1576 return traversals.compare(self, other)
1577
1578 @property
1579 def is_opts_only(self) -> bool:
1580 return bool(self.local_opts and self.strategy is None)
1581
1582 def _clone(self, **kw: Any) -> _LoadElement:
1583 cls = self.__class__
1584 s = cls.__new__(cls)
1585
1586 self._shallow_copy_to(s)
1587 return s
1588
1589 def _update_opts(self, **kw: Any) -> _LoadElement:
1590 new = self._clone()
1591 new.local_opts = new.local_opts.union(kw)
1592 return new
1593
1594 def __getstate__(self) -> Dict[str, Any]:
1595 d = self._shallow_to_dict()
1596 d["path"] = self.path.serialize()
1597 return d
1598
1599 def __setstate__(self, state: Dict[str, Any]) -> None:
1600 state["path"] = PathRegistry.deserialize(state["path"])
1601 self._shallow_from_dict(state)
1602
1603 def _raise_for_no_match(self, parent_loader, mapper_entities):
1604 path = parent_loader.path
1605
1606 found_entities = False
1607 for ent in mapper_entities:
1608 ezero = ent.entity_zero
1609 if ezero:
1610 found_entities = True
1611 break
1612
1613 if not found_entities:
1614 raise sa_exc.ArgumentError(
1615 "Query has only expression-based entities; "
1616 f"attribute loader options for {path[0]} can't "
1617 "be applied here."
1618 )
1619 else:
1620 raise sa_exc.ArgumentError(
1621 f"Mapped class {path[0]} does not apply to any of the "
1622 f"root entities in this query, e.g. "
1623 f"""{
1624 ", ".join(
1625 str(x.entity_zero)
1626 for x in mapper_entities if x.entity_zero
1627 )}. Please """
1628 "specify the full path "
1629 "from one of the root entities to the target "
1630 "attribute. "
1631 )
1632
1633 def _adjust_effective_path_for_current_path(
1634 self, effective_path: PathRegistry, current_path: PathRegistry
1635 ) -> Optional[PathRegistry]:
1636 """receives the 'current_path' entry from an :class:`.ORMCompileState`
1637 instance, which is set during lazy loads and secondary loader strategy
1638 loads, and adjusts the given path to be relative to the
1639 current_path.
1640
1641 E.g. given a loader path and current path:
1642
1643 .. sourcecode:: text
1644
1645 lp: User -> orders -> Order -> items -> Item -> keywords -> Keyword
1646
1647 cp: User -> orders -> Order -> items
1648
1649 The adjusted path would be:
1650
1651 .. sourcecode:: text
1652
1653 Item -> keywords -> Keyword
1654
1655
1656 """
1657 chopped_start_path = Load._chop_path(
1658 effective_path.natural_path, current_path
1659 )
1660 if not chopped_start_path:
1661 return None
1662
1663 tokens_removed_from_start_path = len(effective_path) - len(
1664 chopped_start_path
1665 )
1666
1667 loader_lead_path_element = self.path[tokens_removed_from_start_path]
1668
1669 effective_path = PathRegistry.coerce(
1670 (loader_lead_path_element,) + chopped_start_path[1:]
1671 )
1672
1673 return effective_path
1674
1675 def _init_path(
1676 self, path, attr, wildcard_key, attr_group, raiseerr, extra_criteria
1677 ):
1678 """Apply ORM attributes and/or wildcard to an existing path, producing
1679 a new path.
1680
1681 This method is used within the :meth:`.create` method to initialize
1682 a :class:`._LoadElement` object.
1683
1684 """
1685 raise NotImplementedError()
1686
1687 def _prepare_for_compile_state(
1688 self,
1689 parent_loader,
1690 compile_state,
1691 mapper_entities,
1692 reconciled_lead_entity,
1693 raiseerr,
1694 ):
1695 """implemented by subclasses."""
1696 raise NotImplementedError()
1697
1698 def process_compile_state(
1699 self,
1700 parent_loader,
1701 compile_state,
1702 mapper_entities,
1703 reconciled_lead_entity,
1704 raiseerr,
1705 ):
1706 """populate ORMCompileState.attributes with loader state for this
1707 _LoadElement.
1708
1709 """
1710 keys = self._prepare_for_compile_state(
1711 parent_loader,
1712 compile_state,
1713 mapper_entities,
1714 reconciled_lead_entity,
1715 raiseerr,
1716 )
1717 for key in keys:
1718 if key in compile_state.attributes:
1719 compile_state.attributes[key] = _LoadElement._reconcile(
1720 self, compile_state.attributes[key]
1721 )
1722 else:
1723 compile_state.attributes[key] = self
1724
1725 @classmethod
1726 def create(
1727 cls,
1728 path: PathRegistry,
1729 attr: Union[_AttrType, _StrPathToken, None],
1730 strategy: Optional[_StrategyKey],
1731 wildcard_key: Optional[_WildcardKeyType],
1732 local_opts: Optional[_OptsType],
1733 propagate_to_loaders: bool,
1734 raiseerr: bool = True,
1735 attr_group: Optional[_AttrGroupType] = None,
1736 reconcile_to_other: Optional[bool] = None,
1737 extra_criteria: Optional[Tuple[Any, ...]] = None,
1738 ) -> _LoadElement:
1739 """Create a new :class:`._LoadElement` object."""
1740
1741 opt = cls.__new__(cls)
1742 opt.path = path
1743 opt.strategy = strategy
1744 opt.propagate_to_loaders = propagate_to_loaders
1745 opt.local_opts = (
1746 util.immutabledict(local_opts) if local_opts else util.EMPTY_DICT
1747 )
1748 opt._extra_criteria = ()
1749
1750 if reconcile_to_other is not None:
1751 opt._reconcile_to_other = reconcile_to_other
1752 elif strategy is None and not local_opts:
1753 opt._reconcile_to_other = True
1754 else:
1755 opt._reconcile_to_other = None
1756
1757 path = opt._init_path(
1758 path, attr, wildcard_key, attr_group, raiseerr, extra_criteria
1759 )
1760
1761 if not path:
1762 return None # type: ignore
1763
1764 assert opt.is_token_strategy == path.is_token
1765
1766 opt.path = path
1767 return opt
1768
1769 def __init__(self) -> None:
1770 raise NotImplementedError()
1771
1772 def _recurse(self) -> _LoadElement:
1773 cloned = self._clone()
1774 cloned.path = PathRegistry.coerce(self.path[:] + self.path[-2:])
1775
1776 return cloned
1777
1778 def _prepend_path_from(self, parent: Load) -> _LoadElement:
1779 """adjust the path of this :class:`._LoadElement` to be
1780 a subpath of that of the given parent :class:`_orm.Load` object's
1781 path.
1782
1783 This is used by the :meth:`_orm.Load._apply_to_parent` method,
1784 which is in turn part of the :meth:`_orm.Load.options` method.
1785
1786 """
1787
1788 if not any(
1789 orm_util._entity_corresponds_to_use_path_impl(
1790 elem,
1791 self.path.odd_element(0),
1792 )
1793 for elem in (parent.path.odd_element(-1),)
1794 + parent.additional_source_entities
1795 ):
1796 raise sa_exc.ArgumentError(
1797 f'Attribute "{self.path[1]}" does not link '
1798 f'from element "{parent.path[-1]}".'
1799 )
1800
1801 return self._prepend_path(parent.path)
1802
1803 def _prepend_path(self, path: PathRegistry) -> _LoadElement:
1804 cloned = self._clone()
1805
1806 assert cloned.strategy == self.strategy
1807 assert cloned.local_opts == self.local_opts
1808 assert cloned.is_class_strategy == self.is_class_strategy
1809
1810 cloned.path = PathRegistry.coerce(path[0:-1] + cloned.path[:])
1811
1812 return cloned
1813
1814 @staticmethod
1815 def _reconcile(
1816 replacement: _LoadElement, existing: _LoadElement
1817 ) -> _LoadElement:
1818 """define behavior for when two Load objects are to be put into
1819 the context.attributes under the same key.
1820
1821 :param replacement: ``_LoadElement`` that seeks to replace the
1822 existing one
1823
1824 :param existing: ``_LoadElement`` that is already present.
1825
1826 """
1827 # mapper inheritance loading requires fine-grained "block other
1828 # options" / "allow these options to be overridden" behaviors
1829 # see test_poly_loading.py
1830
1831 if replacement._reconcile_to_other:
1832 return existing
1833 elif replacement._reconcile_to_other is False:
1834 return replacement
1835 elif existing._reconcile_to_other:
1836 return replacement
1837 elif existing._reconcile_to_other is False:
1838 return existing
1839
1840 if existing is replacement:
1841 return replacement
1842 elif (
1843 existing.strategy == replacement.strategy
1844 and existing.local_opts == replacement.local_opts
1845 ):
1846 return replacement
1847 elif replacement.is_opts_only:
1848 existing = existing._clone()
1849 existing.local_opts = existing.local_opts.union(
1850 replacement.local_opts
1851 )
1852 existing._extra_criteria += replacement._extra_criteria
1853 return existing
1854 elif existing.is_opts_only:
1855 replacement = replacement._clone()
1856 replacement.local_opts = replacement.local_opts.union(
1857 existing.local_opts
1858 )
1859 replacement._extra_criteria += existing._extra_criteria
1860 return replacement
1861 elif replacement.path.is_token:
1862 # use 'last one wins' logic for wildcard options. this is also
1863 # kind of inconsistent vs. options that are specific paths which
1864 # will raise as below
1865 return replacement
1866
1867 raise sa_exc.InvalidRequestError(
1868 f"Loader strategies for {replacement.path} conflict"
1869 )
1870
1871
1872class _AttributeStrategyLoad(_LoadElement):
1873 """Loader strategies against specific relationship or column paths.
1874
1875 e.g.::
1876
1877 joinedload(User.addresses)
1878 defer(Order.name)
1879 selectinload(User.orders).lazyload(Order.items)
1880
1881 """
1882
1883 __slots__ = ("_of_type", "_path_with_polymorphic_path")
1884
1885 __visit_name__ = "attribute_strategy_load_element"
1886
1887 _traverse_internals = _LoadElement._traverse_internals + [
1888 ("_of_type", visitors.ExtendedInternalTraversal.dp_multi),
1889 (
1890 "_path_with_polymorphic_path",
1891 visitors.ExtendedInternalTraversal.dp_has_cache_key,
1892 ),
1893 ]
1894
1895 _of_type: Union[Mapper[Any], AliasedInsp[Any], None]
1896 _path_with_polymorphic_path: Optional[PathRegistry]
1897
1898 is_class_strategy = False
1899 is_token_strategy = False
1900
1901 def _init_path(
1902 self, path, attr, wildcard_key, attr_group, raiseerr, extra_criteria
1903 ):
1904 assert attr is not None
1905 self._of_type = None
1906 self._path_with_polymorphic_path = None
1907 insp, _, prop = _parse_attr_argument(attr)
1908
1909 if insp.is_property:
1910 # direct property can be sent from internal strategy logic
1911 # that sets up specific loaders, such as
1912 # emit_lazyload->_lazyload_reverse
1913 # prop = found_property = attr
1914 prop = attr
1915 path = path[prop]
1916
1917 if path.has_entity:
1918 path = path.entity_path
1919 return path
1920
1921 elif not insp.is_attribute:
1922 # should not reach here;
1923 assert False
1924
1925 # here we assume we have user-passed InstrumentedAttribute
1926 if not orm_util._entity_corresponds_to_use_path_impl(
1927 path[-1], attr.parent
1928 ):
1929 if raiseerr:
1930 if attr_group and attr is not attr_group[0]:
1931 raise sa_exc.ArgumentError(
1932 "Can't apply wildcard ('*') or load_only() "
1933 "loader option to multiple entities in the "
1934 "same option. Use separate options per entity."
1935 )
1936 else:
1937 _raise_for_does_not_link(path, str(attr), attr.parent)
1938 else:
1939 return None
1940
1941 # note the essential logic of this attribute was very different in
1942 # 1.4, where there were caching failures in e.g.
1943 # test_relationship_criteria.py::RelationshipCriteriaTest::
1944 # test_selectinload_nested_criteria[True] if an existing
1945 # "_extra_criteria" on a Load object were replaced with that coming
1946 # from an attribute. This appears to have been an artifact of how
1947 # _UnboundLoad / Load interacted together, which was opaque and
1948 # poorly defined.
1949 if extra_criteria:
1950 assert not attr._extra_criteria
1951 self._extra_criteria = extra_criteria
1952 else:
1953 self._extra_criteria = attr._extra_criteria
1954
1955 if getattr(attr, "_of_type", None):
1956 ac = attr._of_type
1957 ext_info = inspect(ac)
1958 self._of_type = ext_info
1959
1960 self._path_with_polymorphic_path = path.entity_path[prop]
1961
1962 path = path[prop][ext_info]
1963
1964 else:
1965 path = path[prop]
1966
1967 if path.has_entity:
1968 path = path.entity_path
1969
1970 return path
1971
1972 def _generate_extra_criteria(self, context):
1973 """Apply the current bound parameters in a QueryContext to the
1974 immediate "extra_criteria" stored with this Load object.
1975
1976 Load objects are typically pulled from the cached version of
1977 the statement from a QueryContext. The statement currently being
1978 executed will have new values (and keys) for bound parameters in the
1979 extra criteria which need to be applied by loader strategies when
1980 they handle this criteria for a result set.
1981
1982 """
1983
1984 assert (
1985 self._extra_criteria
1986 ), "this should only be called if _extra_criteria is present"
1987
1988 orig_query = context.compile_state.select_statement
1989 current_query = context.query
1990
1991 # NOTE: while it seems like we should not do the "apply" operation
1992 # here if orig_query is current_query, skipping it in the "optimized"
1993 # case causes the query to be different from a cache key perspective,
1994 # because we are creating a copy of the criteria which is no longer
1995 # the same identity of the _extra_criteria in the loader option
1996 # itself. cache key logic produces a different key for
1997 # (A, copy_of_A) vs. (A, A), because in the latter case it shortens
1998 # the second part of the key to just indicate on identity.
1999
2000 # if orig_query is current_query:
2001 # not cached yet. just do the and_()
2002 # return and_(*self._extra_criteria)
2003
2004 k1 = orig_query._generate_cache_key()
2005 k2 = current_query._generate_cache_key()
2006
2007 return k2._apply_params_to_element(k1, and_(*self._extra_criteria))
2008
2009 def _set_of_type_info(self, context, current_path):
2010 assert self._path_with_polymorphic_path
2011
2012 pwpi = self._of_type
2013 assert pwpi
2014 if not pwpi.is_aliased_class:
2015 pwpi = inspect(
2016 orm_util.AliasedInsp._with_polymorphic_factory(
2017 pwpi.mapper.base_mapper,
2018 (pwpi.mapper,),
2019 aliased=True,
2020 _use_mapper_path=True,
2021 )
2022 )
2023 start_path = self._path_with_polymorphic_path
2024 if current_path:
2025 new_path = self._adjust_effective_path_for_current_path(
2026 start_path, current_path
2027 )
2028 if new_path is None:
2029 return
2030 start_path = new_path
2031
2032 key = ("path_with_polymorphic", start_path.natural_path)
2033 if key in context:
2034 existing_aliased_insp = context[key]
2035 this_aliased_insp = pwpi
2036 new_aliased_insp = existing_aliased_insp._merge_with(
2037 this_aliased_insp
2038 )
2039 context[key] = new_aliased_insp
2040 else:
2041 context[key] = pwpi
2042
2043 def _prepare_for_compile_state(
2044 self,
2045 parent_loader,
2046 compile_state,
2047 mapper_entities,
2048 reconciled_lead_entity,
2049 raiseerr,
2050 ):
2051 # _AttributeStrategyLoad
2052
2053 current_path = compile_state.current_path
2054 is_refresh = compile_state.compile_options._for_refresh_state
2055 assert not self.path.is_token
2056
2057 if is_refresh and not self.propagate_to_loaders:
2058 return []
2059
2060 if self._of_type:
2061 # apply additional with_polymorphic alias that may have been
2062 # generated. this has to happen even if this is a defaultload
2063 self._set_of_type_info(compile_state.attributes, current_path)
2064
2065 # omit setting loader attributes for a "defaultload" type of option
2066 if not self.strategy and not self.local_opts:
2067 return []
2068
2069 if raiseerr and not reconciled_lead_entity:
2070 self._raise_for_no_match(parent_loader, mapper_entities)
2071
2072 if self.path.has_entity:
2073 effective_path = self.path.parent
2074 else:
2075 effective_path = self.path
2076
2077 if current_path:
2078 assert effective_path is not None
2079 effective_path = self._adjust_effective_path_for_current_path(
2080 effective_path, current_path
2081 )
2082 if effective_path is None:
2083 return []
2084
2085 return [("loader", cast(PathRegistry, effective_path).natural_path)]
2086
2087 def __getstate__(self):
2088 d = super().__getstate__()
2089
2090 # can't pickle this. See
2091 # test_pickled.py -> test_lazyload_extra_criteria_not_supported
2092 # where we should be emitting a warning for the usual case where this
2093 # would be non-None
2094 d["_extra_criteria"] = ()
2095
2096 if self._path_with_polymorphic_path:
2097 d["_path_with_polymorphic_path"] = (
2098 self._path_with_polymorphic_path.serialize()
2099 )
2100
2101 if self._of_type:
2102 if self._of_type.is_aliased_class:
2103 d["_of_type"] = None
2104 elif self._of_type.is_mapper:
2105 d["_of_type"] = self._of_type.class_
2106 else:
2107 assert False, "unexpected object for _of_type"
2108
2109 return d
2110
2111 def __setstate__(self, state):
2112 super().__setstate__(state)
2113
2114 if state.get("_path_with_polymorphic_path", None):
2115 self._path_with_polymorphic_path = PathRegistry.deserialize(
2116 state["_path_with_polymorphic_path"]
2117 )
2118 else:
2119 self._path_with_polymorphic_path = None
2120
2121 if state.get("_of_type", None):
2122 self._of_type = inspect(state["_of_type"])
2123 else:
2124 self._of_type = None
2125
2126
2127class _TokenStrategyLoad(_LoadElement):
2128 """Loader strategies against wildcard attributes
2129
2130 e.g.::
2131
2132 raiseload("*")
2133 Load(User).lazyload("*")
2134 defer("*")
2135 load_only(User.name, User.email) # will create a defer('*')
2136 joinedload(User.addresses).raiseload("*")
2137
2138 """
2139
2140 __visit_name__ = "token_strategy_load_element"
2141
2142 inherit_cache = True
2143 is_class_strategy = False
2144 is_token_strategy = True
2145
2146 def _init_path(
2147 self, path, attr, wildcard_key, attr_group, raiseerr, extra_criteria
2148 ):
2149 # assert isinstance(attr, str) or attr is None
2150 if attr is not None:
2151 default_token = attr.endswith(_DEFAULT_TOKEN)
2152 if attr.endswith(_WILDCARD_TOKEN) or default_token:
2153 if wildcard_key:
2154 attr = f"{wildcard_key}:{attr}"
2155
2156 path = path.token(attr)
2157 return path
2158 else:
2159 raise sa_exc.ArgumentError(
2160 "Strings are not accepted for attribute names in loader "
2161 "options; please use class-bound attributes directly."
2162 )
2163 return path
2164
2165 def _prepare_for_compile_state(
2166 self,
2167 parent_loader,
2168 compile_state,
2169 mapper_entities,
2170 reconciled_lead_entity,
2171 raiseerr,
2172 ):
2173 # _TokenStrategyLoad
2174
2175 current_path = compile_state.current_path
2176 is_refresh = compile_state.compile_options._for_refresh_state
2177
2178 assert self.path.is_token
2179
2180 if is_refresh and not self.propagate_to_loaders:
2181 return []
2182
2183 # omit setting attributes for a "defaultload" type of option
2184 if not self.strategy and not self.local_opts:
2185 return []
2186
2187 effective_path = self.path
2188 if reconciled_lead_entity:
2189 effective_path = PathRegistry.coerce(
2190 (reconciled_lead_entity,) + effective_path.path[1:]
2191 )
2192
2193 if current_path:
2194 new_effective_path = self._adjust_effective_path_for_current_path(
2195 effective_path, current_path
2196 )
2197 if new_effective_path is None:
2198 return []
2199 effective_path = new_effective_path
2200
2201 # for a wildcard token, expand out the path we set
2202 # to encompass everything from the query entity on
2203 # forward. not clear if this is necessary when current_path
2204 # is set.
2205
2206 return [
2207 ("loader", natural_path)
2208 for natural_path in (
2209 cast(
2210 _TokenRegistry, effective_path
2211 )._generate_natural_for_superclasses()
2212 )
2213 ]
2214
2215
2216class _ClassStrategyLoad(_LoadElement):
2217 """Loader strategies that deals with a class as a target, not
2218 an attribute path
2219
2220 e.g.::
2221
2222 q = s.query(Person).options(
2223 selectin_polymorphic(Person, [Engineer, Manager])
2224 )
2225
2226 """
2227
2228 inherit_cache = True
2229 is_class_strategy = True
2230 is_token_strategy = False
2231
2232 __visit_name__ = "class_strategy_load_element"
2233
2234 def _init_path(
2235 self, path, attr, wildcard_key, attr_group, raiseerr, extra_criteria
2236 ):
2237 return path
2238
2239 def _prepare_for_compile_state(
2240 self,
2241 parent_loader,
2242 compile_state,
2243 mapper_entities,
2244 reconciled_lead_entity,
2245 raiseerr,
2246 ):
2247 # _ClassStrategyLoad
2248
2249 current_path = compile_state.current_path
2250 is_refresh = compile_state.compile_options._for_refresh_state
2251
2252 if is_refresh and not self.propagate_to_loaders:
2253 return []
2254
2255 # omit setting attributes for a "defaultload" type of option
2256 if not self.strategy and not self.local_opts:
2257 return []
2258
2259 effective_path = self.path
2260
2261 if current_path:
2262 new_effective_path = self._adjust_effective_path_for_current_path(
2263 effective_path, current_path
2264 )
2265 if new_effective_path is None:
2266 return []
2267 effective_path = new_effective_path
2268
2269 return [("loader", effective_path.natural_path)]
2270
2271
2272def _generate_from_keys(
2273 meth: Callable[..., _AbstractLoad],
2274 keys: Tuple[_AttrType, ...],
2275 chained: bool,
2276 kw: Any,
2277) -> _AbstractLoad:
2278 lead_element: Optional[_AbstractLoad] = None
2279
2280 attr: Any
2281 for is_default, _keys in (True, keys[0:-1]), (False, keys[-1:]):
2282 for attr in _keys:
2283 if isinstance(attr, str):
2284 if attr.startswith("." + _WILDCARD_TOKEN):
2285 util.warn_deprecated(
2286 "The undocumented `.{WILDCARD}` format is "
2287 "deprecated "
2288 "and will be removed in a future version as "
2289 "it is "
2290 "believed to be unused. "
2291 "If you have been using this functionality, "
2292 "please "
2293 "comment on Issue #4390 on the SQLAlchemy project "
2294 "tracker.",
2295 version="1.4",
2296 )
2297 attr = attr[1:]
2298
2299 if attr == _WILDCARD_TOKEN:
2300 if is_default:
2301 raise sa_exc.ArgumentError(
2302 "Wildcard token cannot be followed by "
2303 "another entity",
2304 )
2305
2306 if lead_element is None:
2307 lead_element = _WildcardLoad()
2308
2309 lead_element = meth(lead_element, _DEFAULT_TOKEN, **kw)
2310
2311 else:
2312 raise sa_exc.ArgumentError(
2313 "Strings are not accepted for attribute names in "
2314 "loader options; please use class-bound "
2315 "attributes directly.",
2316 )
2317 else:
2318 if lead_element is None:
2319 _, lead_entity, _ = _parse_attr_argument(attr)
2320 lead_element = Load(lead_entity)
2321
2322 if is_default:
2323 if not chained:
2324 lead_element = lead_element.defaultload(attr)
2325 else:
2326 lead_element = meth(
2327 lead_element, attr, _is_chain=True, **kw
2328 )
2329 else:
2330 lead_element = meth(lead_element, attr, **kw)
2331
2332 assert lead_element
2333 return lead_element
2334
2335
2336def _parse_attr_argument(
2337 attr: _AttrType,
2338) -> Tuple[InspectionAttr, _InternalEntityType[Any], MapperProperty[Any]]:
2339 """parse an attribute or wildcard argument to produce an
2340 :class:`._AbstractLoad` instance.
2341
2342 This is used by the standalone loader strategy functions like
2343 ``joinedload()``, ``defer()``, etc. to produce :class:`_orm.Load` or
2344 :class:`._WildcardLoad` objects.
2345
2346 """
2347 try:
2348 # TODO: need to figure out this None thing being returned by
2349 # inspect(), it should not have None as an option in most cases
2350 # if at all
2351 insp: InspectionAttr = inspect(attr) # type: ignore
2352 except sa_exc.NoInspectionAvailable as err:
2353 raise sa_exc.ArgumentError(
2354 "expected ORM mapped attribute for loader strategy argument"
2355 ) from err
2356
2357 lead_entity: _InternalEntityType[Any]
2358
2359 if insp_is_mapper_property(insp):
2360 lead_entity = insp.parent
2361 prop = insp
2362 elif insp_is_attribute(insp):
2363 lead_entity = insp.parent
2364 prop = insp.prop
2365 else:
2366 raise sa_exc.ArgumentError(
2367 "expected ORM mapped attribute for loader strategy argument"
2368 )
2369
2370 return insp, lead_entity, prop
2371
2372
2373def loader_unbound_fn(fn: _FN) -> _FN:
2374 """decorator that applies docstrings between standalone loader functions
2375 and the loader methods on :class:`._AbstractLoad`.
2376
2377 """
2378 bound_fn = getattr(_AbstractLoad, fn.__name__)
2379 fn_doc = bound_fn.__doc__
2380 bound_fn.__doc__ = f"""Produce a new :class:`_orm.Load` object with the
2381:func:`_orm.{fn.__name__}` option applied.
2382
2383See :func:`_orm.{fn.__name__}` for usage examples.
2384
2385"""
2386
2387 fn.__doc__ = fn_doc
2388 return fn
2389
2390
2391def _expand_column_strategy_attrs(
2392 attrs: Tuple[_AttrType, ...],
2393) -> Tuple[_AttrType, ...]:
2394 return cast(
2395 "Tuple[_AttrType, ...]",
2396 tuple(
2397 a
2398 for attr in attrs
2399 for a in (
2400 cast("QueryableAttribute[Any]", attr)._column_strategy_attrs()
2401 if hasattr(attr, "_column_strategy_attrs")
2402 else (attr,)
2403 )
2404 ),
2405 )
2406
2407
2408# standalone functions follow. docstrings are filled in
2409# by the ``@loader_unbound_fn`` decorator.
2410
2411
2412@loader_unbound_fn
2413def contains_eager(*keys: _AttrType, **kw: Any) -> _AbstractLoad:
2414 return _generate_from_keys(Load.contains_eager, keys, True, kw)
2415
2416
2417@loader_unbound_fn
2418def load_only(*attrs: _AttrType, raiseload: bool = False) -> _AbstractLoad:
2419 # TODO: attrs against different classes. we likely have to
2420 # add some extra state to Load of some kind
2421 attrs = _expand_column_strategy_attrs(attrs)
2422 _, lead_element, _ = _parse_attr_argument(attrs[0])
2423 return Load(lead_element).load_only(*attrs, raiseload=raiseload)
2424
2425
2426@loader_unbound_fn
2427def joinedload(*keys: _AttrType, **kw: Any) -> _AbstractLoad:
2428 return _generate_from_keys(Load.joinedload, keys, False, kw)
2429
2430
2431@loader_unbound_fn
2432def subqueryload(*keys: _AttrType) -> _AbstractLoad:
2433 return _generate_from_keys(Load.subqueryload, keys, False, {})
2434
2435
2436@loader_unbound_fn
2437def selectinload(
2438 *keys: _AttrType, recursion_depth: Optional[int] = None
2439) -> _AbstractLoad:
2440 return _generate_from_keys(
2441 Load.selectinload, keys, False, {"recursion_depth": recursion_depth}
2442 )
2443
2444
2445@loader_unbound_fn
2446def lazyload(*keys: _AttrType) -> _AbstractLoad:
2447 return _generate_from_keys(Load.lazyload, keys, False, {})
2448
2449
2450@loader_unbound_fn
2451def immediateload(
2452 *keys: _AttrType, recursion_depth: Optional[int] = None
2453) -> _AbstractLoad:
2454 return _generate_from_keys(
2455 Load.immediateload, keys, False, {"recursion_depth": recursion_depth}
2456 )
2457
2458
2459@loader_unbound_fn
2460def noload(*keys: _AttrType) -> _AbstractLoad:
2461 return _generate_from_keys(Load.noload, keys, False, {})
2462
2463
2464@loader_unbound_fn
2465def raiseload(*keys: _AttrType, **kw: Any) -> _AbstractLoad:
2466 return _generate_from_keys(Load.raiseload, keys, False, kw)
2467
2468
2469@loader_unbound_fn
2470def defaultload(*keys: _AttrType) -> _AbstractLoad:
2471 return _generate_from_keys(Load.defaultload, keys, False, {})
2472
2473
2474@loader_unbound_fn
2475def defer(key: _AttrType, *, raiseload: bool = False) -> _AbstractLoad:
2476 if raiseload:
2477 kw = {"raiseload": raiseload}
2478 else:
2479 kw = {}
2480
2481 return _generate_from_keys(Load.defer, (key,), False, kw)
2482
2483
2484@loader_unbound_fn
2485def undefer(key: _AttrType) -> _AbstractLoad:
2486 return _generate_from_keys(Load.undefer, (key,), False, {})
2487
2488
2489@loader_unbound_fn
2490def undefer_group(name: str) -> _AbstractLoad:
2491 element = _WildcardLoad()
2492 return element.undefer_group(name)
2493
2494
2495@loader_unbound_fn
2496def with_expression(
2497 key: _AttrType, expression: _ColumnExpressionArgument[Any]
2498) -> _AbstractLoad:
2499 return _generate_from_keys(
2500 Load.with_expression, (key,), False, {"expression": expression}
2501 )
2502
2503
2504@loader_unbound_fn
2505def selectin_polymorphic(
2506 base_cls: _EntityType[Any], classes: Iterable[Type[Any]]
2507) -> _AbstractLoad:
2508 ul = Load(base_cls)
2509 return ul.selectin_polymorphic(classes)
2510
2511
2512def _raise_for_does_not_link(path, attrname, parent_entity):
2513 if len(path) > 1:
2514 path_is_of_type = path[-1].entity is not path[-2].mapper.class_
2515 if insp_is_aliased_class(parent_entity):
2516 parent_entity_str = str(parent_entity)
2517 else:
2518 parent_entity_str = parent_entity.class_.__name__
2519
2520 raise sa_exc.ArgumentError(
2521 f'ORM mapped entity or attribute "{attrname}" does not '
2522 f'link from relationship "{path[-2]}%s".%s'
2523 % (
2524 f".of_type({path[-1]})" if path_is_of_type else "",
2525 (
2526 " Did you mean to use "
2527 f'"{path[-2]}'
2528 f'.of_type({parent_entity_str})" or "loadopt.options('
2529 f"selectin_polymorphic({path[-2].mapper.class_.__name__}, "
2530 f'[{parent_entity_str}]), ...)" ?'
2531 if not path_is_of_type
2532 and not path[-1].is_aliased_class
2533 and orm_util._entity_corresponds_to(
2534 path.entity, inspect(parent_entity).mapper
2535 )
2536 else ""
2537 ),
2538 )
2539 )
2540 else:
2541 raise sa_exc.ArgumentError(
2542 f'ORM mapped attribute "{attrname}" does not '
2543 f'link mapped class "{path[-1]}"'
2544 )