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 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(
1201 load_only(Book.summary, Book.excerpt),
1202 joinedload(Book.citations).options(joinedload(Citation.author)),
1203 )
1204 )
1205
1206 :param \*opts: A series of loader option objects (ultimately
1207 :class:`_orm.Load` objects) which should be applied to the path
1208 specified by this :class:`_orm.Load` object.
1209
1210 .. versionadded:: 1.3.6
1211
1212 .. seealso::
1213
1214 :func:`.defaultload`
1215
1216 :ref:`orm_queryguide_relationship_sub_options`
1217
1218 """
1219 for opt in opts:
1220 try:
1221 opt._apply_to_parent(self)
1222 except AttributeError as ae:
1223 if not isinstance(opt, _AbstractLoad):
1224 raise sa_exc.ArgumentError(
1225 f"Loader option {opt} is not compatible with the "
1226 "Load.options() method."
1227 ) from ae
1228 else:
1229 raise
1230 return self
1231
1232 def _clone_for_bind_strategy(
1233 self,
1234 attrs: Optional[Tuple[_AttrType, ...]],
1235 strategy: Optional[_StrategyKey],
1236 wildcard_key: Optional[_WildcardKeyType],
1237 opts: Optional[_OptsType] = None,
1238 attr_group: Optional[_AttrGroupType] = None,
1239 propagate_to_loaders: bool = True,
1240 reconcile_to_other: Optional[bool] = None,
1241 extra_criteria: Optional[Tuple[Any, ...]] = None,
1242 ) -> Self:
1243 # for individual strategy that needs to propagate, set the whole
1244 # Load container to also propagate, so that it shows up in
1245 # InstanceState.load_options
1246 if propagate_to_loaders:
1247 self.propagate_to_loaders = True
1248
1249 if self.path.is_token:
1250 raise sa_exc.ArgumentError(
1251 "Wildcard token cannot be followed by another entity"
1252 )
1253
1254 elif path_is_property(self.path):
1255 # re-use the lookup which will raise a nicely formatted
1256 # LoaderStrategyException
1257 if strategy:
1258 self.path.prop._strategy_lookup(self.path.prop, strategy[0])
1259 else:
1260 raise sa_exc.ArgumentError(
1261 f"Mapped attribute '{self.path.prop}' does not "
1262 "refer to a mapped entity"
1263 )
1264
1265 if attrs is None:
1266 load_element = _ClassStrategyLoad.create(
1267 self.path,
1268 None,
1269 strategy,
1270 wildcard_key,
1271 opts,
1272 propagate_to_loaders,
1273 attr_group=attr_group,
1274 reconcile_to_other=reconcile_to_other,
1275 extra_criteria=extra_criteria,
1276 )
1277 if load_element:
1278 self.context += (load_element,)
1279 assert opts is not None
1280 self.additional_source_entities += cast(
1281 "Tuple[_InternalEntityType[Any]]", opts["entities"]
1282 )
1283
1284 else:
1285 for attr in attrs:
1286 if isinstance(attr, str):
1287 load_element = _TokenStrategyLoad.create(
1288 self.path,
1289 attr,
1290 strategy,
1291 wildcard_key,
1292 opts,
1293 propagate_to_loaders,
1294 attr_group=attr_group,
1295 reconcile_to_other=reconcile_to_other,
1296 extra_criteria=extra_criteria,
1297 )
1298 else:
1299 load_element = _AttributeStrategyLoad.create(
1300 self.path,
1301 attr,
1302 strategy,
1303 wildcard_key,
1304 opts,
1305 propagate_to_loaders,
1306 attr_group=attr_group,
1307 reconcile_to_other=reconcile_to_other,
1308 extra_criteria=extra_criteria,
1309 )
1310
1311 if load_element:
1312 # for relationship options, update self.path on this Load
1313 # object with the latest path.
1314 if wildcard_key is _RELATIONSHIP_TOKEN:
1315 self.path = load_element.path
1316 self.context += (load_element,)
1317
1318 # this seems to be effective for selectinloader,
1319 # giving the extra match to one more level deep.
1320 # but does not work for immediateloader, which still
1321 # must add additional options at load time
1322 if load_element.local_opts.get("recursion_depth", False):
1323 r1 = load_element._recurse()
1324 self.context += (r1,)
1325
1326 return self
1327
1328 def __getstate__(self):
1329 d = self._shallow_to_dict()
1330 d["path"] = self.path.serialize()
1331 return d
1332
1333 def __setstate__(self, state):
1334 state["path"] = PathRegistry.deserialize(state["path"])
1335 self._shallow_from_dict(state)
1336
1337
1338class _WildcardLoad(_AbstractLoad):
1339 """represent a standalone '*' load operation"""
1340
1341 __slots__ = ("strategy", "path", "local_opts")
1342
1343 _traverse_internals = [
1344 ("strategy", visitors.ExtendedInternalTraversal.dp_plain_obj),
1345 ("path", visitors.ExtendedInternalTraversal.dp_plain_obj),
1346 (
1347 "local_opts",
1348 visitors.ExtendedInternalTraversal.dp_string_multi_dict,
1349 ),
1350 ]
1351 cache_key_traversal: _CacheKeyTraversalType = None
1352
1353 strategy: Optional[Tuple[Any, ...]]
1354 local_opts: _OptsType
1355 path: Union[Tuple[()], Tuple[str]]
1356 propagate_to_loaders = False
1357
1358 def __init__(self) -> None:
1359 self.path = ()
1360 self.strategy = None
1361 self.local_opts = util.EMPTY_DICT
1362
1363 def _clone_for_bind_strategy(
1364 self,
1365 attrs,
1366 strategy,
1367 wildcard_key,
1368 opts=None,
1369 attr_group=None,
1370 propagate_to_loaders=True,
1371 reconcile_to_other=None,
1372 extra_criteria=None,
1373 ):
1374 assert attrs is not None
1375 attr = attrs[0]
1376 assert (
1377 wildcard_key
1378 and isinstance(attr, str)
1379 and attr in (_WILDCARD_TOKEN, _DEFAULT_TOKEN)
1380 )
1381
1382 attr = f"{wildcard_key}:{attr}"
1383
1384 self.strategy = strategy
1385 self.path = (attr,)
1386 if opts:
1387 self.local_opts = util.immutabledict(opts)
1388
1389 assert extra_criteria is None
1390
1391 def options(self, *opts: _AbstractLoad) -> Self:
1392 raise NotImplementedError("Star option does not support sub-options")
1393
1394 def _apply_to_parent(self, parent: Load) -> None:
1395 """apply this :class:`_orm._WildcardLoad` object as a sub-option of
1396 a :class:`_orm.Load` object.
1397
1398 This method is used by the :meth:`_orm.Load.options` method. Note
1399 that :class:`_orm.WildcardLoad` itself can't have sub-options, but
1400 it may be used as the sub-option of a :class:`_orm.Load` object.
1401
1402 """
1403 assert self.path
1404 attr = self.path[0]
1405 if attr.endswith(_DEFAULT_TOKEN):
1406 attr = f"{attr.split(':')[0]}:{_WILDCARD_TOKEN}"
1407
1408 effective_path = cast(AbstractEntityRegistry, parent.path).token(attr)
1409
1410 assert effective_path.is_token
1411
1412 loader = _TokenStrategyLoad.create(
1413 effective_path,
1414 None,
1415 self.strategy,
1416 None,
1417 self.local_opts,
1418 self.propagate_to_loaders,
1419 )
1420
1421 parent.context += (loader,)
1422
1423 def _process(self, compile_state, mapper_entities, raiseerr):
1424 is_refresh = compile_state.compile_options._for_refresh_state
1425
1426 if is_refresh and not self.propagate_to_loaders:
1427 return
1428
1429 entities = [ent.entity_zero for ent in mapper_entities]
1430 current_path = compile_state.current_path
1431
1432 start_path: _PathRepresentation = self.path
1433
1434 if current_path:
1435 # TODO: no cases in test suite where we actually get
1436 # None back here
1437 new_path = self._chop_path(start_path, current_path)
1438 if new_path is None:
1439 return
1440
1441 # chop_path does not actually "chop" a wildcard token path,
1442 # just returns it
1443 assert new_path == start_path
1444
1445 # start_path is a single-token tuple
1446 assert start_path and len(start_path) == 1
1447
1448 token = start_path[0]
1449 assert isinstance(token, str)
1450 entity = self._find_entity_basestring(entities, token, raiseerr)
1451
1452 if not entity:
1453 return
1454
1455 path_element = entity
1456
1457 # transfer our entity-less state into a Load() object
1458 # with a real entity path. Start with the lead entity
1459 # we just located, then go through the rest of our path
1460 # tokens and populate into the Load().
1461
1462 assert isinstance(token, str)
1463 loader = _TokenStrategyLoad.create(
1464 path_element._path_registry,
1465 token,
1466 self.strategy,
1467 None,
1468 self.local_opts,
1469 self.propagate_to_loaders,
1470 raiseerr=raiseerr,
1471 )
1472 if not loader:
1473 return
1474
1475 assert loader.path.is_token
1476
1477 # don't pass a reconciled lead entity here
1478 loader.process_compile_state(
1479 self, compile_state, mapper_entities, None, raiseerr
1480 )
1481
1482 return loader
1483
1484 def _find_entity_basestring(
1485 self,
1486 entities: Iterable[_InternalEntityType[Any]],
1487 token: str,
1488 raiseerr: bool,
1489 ) -> Optional[_InternalEntityType[Any]]:
1490 if token.endswith(f":{_WILDCARD_TOKEN}"):
1491 if len(list(entities)) != 1:
1492 if raiseerr:
1493 raise sa_exc.ArgumentError(
1494 "Can't apply wildcard ('*') or load_only() "
1495 f"loader option to multiple entities "
1496 f"{', '.join(str(ent) for ent in entities)}. Specify "
1497 "loader options for each entity individually, such as "
1498 f"""{
1499 ", ".join(
1500 f"Load({ent}).some_option('*')"
1501 for ent in entities
1502 )
1503 }."""
1504 )
1505 elif token.endswith(_DEFAULT_TOKEN):
1506 raiseerr = False
1507
1508 for ent in entities:
1509 # return only the first _MapperEntity when searching
1510 # based on string prop name. Ideally object
1511 # attributes are used to specify more exactly.
1512 return ent
1513 else:
1514 if raiseerr:
1515 raise sa_exc.ArgumentError(
1516 "Query has only expression-based entities - "
1517 f'can\'t find property named "{token}".'
1518 )
1519 else:
1520 return None
1521
1522 def __getstate__(self) -> Dict[str, Any]:
1523 d = self._shallow_to_dict()
1524 return d
1525
1526 def __setstate__(self, state: Dict[str, Any]) -> None:
1527 self._shallow_from_dict(state)
1528
1529
1530class _LoadElement(
1531 cache_key.HasCacheKey, traversals.HasShallowCopy, visitors.Traversible
1532):
1533 """represents strategy information to select for a LoaderStrategy
1534 and pass options to it.
1535
1536 :class:`._LoadElement` objects provide the inner datastructure
1537 stored by a :class:`_orm.Load` object and are also the object passed
1538 to methods like :meth:`.LoaderStrategy.setup_query`.
1539
1540 .. versionadded:: 2.0
1541
1542 """
1543
1544 __slots__ = (
1545 "path",
1546 "strategy",
1547 "propagate_to_loaders",
1548 "local_opts",
1549 "_extra_criteria",
1550 "_reconcile_to_other",
1551 )
1552 __visit_name__ = "load_element"
1553
1554 _traverse_internals = [
1555 ("path", visitors.ExtendedInternalTraversal.dp_has_cache_key),
1556 ("strategy", visitors.ExtendedInternalTraversal.dp_plain_obj),
1557 (
1558 "local_opts",
1559 visitors.ExtendedInternalTraversal.dp_string_multi_dict,
1560 ),
1561 ("_extra_criteria", visitors.InternalTraversal.dp_clauseelement_list),
1562 ("propagate_to_loaders", visitors.InternalTraversal.dp_plain_obj),
1563 ("_reconcile_to_other", visitors.InternalTraversal.dp_plain_obj),
1564 ]
1565 _cache_key_traversal = None
1566
1567 _extra_criteria: Tuple[Any, ...]
1568
1569 _reconcile_to_other: Optional[bool]
1570 strategy: Optional[_StrategyKey]
1571 path: PathRegistry
1572 propagate_to_loaders: bool
1573
1574 local_opts: util.immutabledict[str, Any]
1575
1576 is_token_strategy: bool
1577 is_class_strategy: bool
1578
1579 def __hash__(self) -> int:
1580 return id(self)
1581
1582 def __eq__(self, other):
1583 return traversals.compare(self, other)
1584
1585 @property
1586 def is_opts_only(self) -> bool:
1587 return bool(self.local_opts and self.strategy is None)
1588
1589 def _clone(self, **kw: Any) -> _LoadElement:
1590 cls = self.__class__
1591 s = cls.__new__(cls)
1592
1593 self._shallow_copy_to(s)
1594 return s
1595
1596 def _update_opts(self, **kw: Any) -> _LoadElement:
1597 new = self._clone()
1598 new.local_opts = new.local_opts.union(kw)
1599 return new
1600
1601 def __getstate__(self) -> Dict[str, Any]:
1602 d = self._shallow_to_dict()
1603 d["path"] = self.path.serialize()
1604 return d
1605
1606 def __setstate__(self, state: Dict[str, Any]) -> None:
1607 state["path"] = PathRegistry.deserialize(state["path"])
1608 self._shallow_from_dict(state)
1609
1610 def _raise_for_no_match(self, parent_loader, mapper_entities):
1611 path = parent_loader.path
1612
1613 found_entities = False
1614 for ent in mapper_entities:
1615 ezero = ent.entity_zero
1616 if ezero:
1617 found_entities = True
1618 break
1619
1620 if not found_entities:
1621 raise sa_exc.ArgumentError(
1622 "Query has only expression-based entities; "
1623 f"attribute loader options for {path[0]} can't "
1624 "be applied here."
1625 )
1626 else:
1627 raise sa_exc.ArgumentError(
1628 f"Mapped class {path[0]} does not apply to any of the "
1629 f"root entities in this query, e.g. "
1630 f"""{
1631 ", ".join(
1632 str(x.entity_zero)
1633 for x in mapper_entities if x.entity_zero
1634 )}. Please """
1635 "specify the full path "
1636 "from one of the root entities to the target "
1637 "attribute. "
1638 )
1639
1640 def _adjust_effective_path_for_current_path(
1641 self, effective_path: PathRegistry, current_path: PathRegistry
1642 ) -> Optional[PathRegistry]:
1643 """receives the 'current_path' entry from an :class:`.ORMCompileState`
1644 instance, which is set during lazy loads and secondary loader strategy
1645 loads, and adjusts the given path to be relative to the
1646 current_path.
1647
1648 E.g. given a loader path and current path:
1649
1650 .. sourcecode:: text
1651
1652 lp: User -> orders -> Order -> items -> Item -> keywords -> Keyword
1653
1654 cp: User -> orders -> Order -> items
1655
1656 The adjusted path would be:
1657
1658 .. sourcecode:: text
1659
1660 Item -> keywords -> Keyword
1661
1662
1663 """
1664 chopped_start_path = Load._chop_path(
1665 effective_path.natural_path, current_path
1666 )
1667 if not chopped_start_path:
1668 return None
1669
1670 tokens_removed_from_start_path = len(effective_path) - len(
1671 chopped_start_path
1672 )
1673
1674 loader_lead_path_element = self.path[tokens_removed_from_start_path]
1675
1676 effective_path = PathRegistry.coerce(
1677 (loader_lead_path_element,) + chopped_start_path[1:]
1678 )
1679
1680 return effective_path
1681
1682 def _init_path(
1683 self, path, attr, wildcard_key, attr_group, raiseerr, extra_criteria
1684 ):
1685 """Apply ORM attributes and/or wildcard to an existing path, producing
1686 a new path.
1687
1688 This method is used within the :meth:`.create` method to initialize
1689 a :class:`._LoadElement` object.
1690
1691 """
1692 raise NotImplementedError()
1693
1694 def _prepare_for_compile_state(
1695 self,
1696 parent_loader,
1697 compile_state,
1698 mapper_entities,
1699 reconciled_lead_entity,
1700 raiseerr,
1701 ):
1702 """implemented by subclasses."""
1703 raise NotImplementedError()
1704
1705 def process_compile_state(
1706 self,
1707 parent_loader,
1708 compile_state,
1709 mapper_entities,
1710 reconciled_lead_entity,
1711 raiseerr,
1712 ):
1713 """populate ORMCompileState.attributes with loader state for this
1714 _LoadElement.
1715
1716 """
1717 keys = self._prepare_for_compile_state(
1718 parent_loader,
1719 compile_state,
1720 mapper_entities,
1721 reconciled_lead_entity,
1722 raiseerr,
1723 )
1724 for key in keys:
1725 if key in compile_state.attributes:
1726 compile_state.attributes[key] = _LoadElement._reconcile(
1727 self, compile_state.attributes[key]
1728 )
1729 else:
1730 compile_state.attributes[key] = self
1731
1732 @classmethod
1733 def create(
1734 cls,
1735 path: PathRegistry,
1736 attr: Union[_AttrType, _StrPathToken, None],
1737 strategy: Optional[_StrategyKey],
1738 wildcard_key: Optional[_WildcardKeyType],
1739 local_opts: Optional[_OptsType],
1740 propagate_to_loaders: bool,
1741 raiseerr: bool = True,
1742 attr_group: Optional[_AttrGroupType] = None,
1743 reconcile_to_other: Optional[bool] = None,
1744 extra_criteria: Optional[Tuple[Any, ...]] = None,
1745 ) -> _LoadElement:
1746 """Create a new :class:`._LoadElement` object."""
1747
1748 opt = cls.__new__(cls)
1749 opt.path = path
1750 opt.strategy = strategy
1751 opt.propagate_to_loaders = propagate_to_loaders
1752 opt.local_opts = (
1753 util.immutabledict(local_opts) if local_opts else util.EMPTY_DICT
1754 )
1755 opt._extra_criteria = ()
1756
1757 if reconcile_to_other is not None:
1758 opt._reconcile_to_other = reconcile_to_other
1759 elif strategy is None and not local_opts:
1760 opt._reconcile_to_other = True
1761 else:
1762 opt._reconcile_to_other = None
1763
1764 path = opt._init_path(
1765 path, attr, wildcard_key, attr_group, raiseerr, extra_criteria
1766 )
1767
1768 if not path:
1769 return None # type: ignore
1770
1771 assert opt.is_token_strategy == path.is_token
1772
1773 opt.path = path
1774 return opt
1775
1776 def __init__(self) -> None:
1777 raise NotImplementedError()
1778
1779 def _recurse(self) -> _LoadElement:
1780 cloned = self._clone()
1781 cloned.path = PathRegistry.coerce(self.path[:] + self.path[-2:])
1782
1783 return cloned
1784
1785 def _prepend_path_from(self, parent: Load) -> _LoadElement:
1786 """adjust the path of this :class:`._LoadElement` to be
1787 a subpath of that of the given parent :class:`_orm.Load` object's
1788 path.
1789
1790 This is used by the :meth:`_orm.Load._apply_to_parent` method,
1791 which is in turn part of the :meth:`_orm.Load.options` method.
1792
1793 """
1794
1795 if not any(
1796 orm_util._entity_corresponds_to_use_path_impl(
1797 elem,
1798 self.path.odd_element(0),
1799 )
1800 for elem in (parent.path.odd_element(-1),)
1801 + parent.additional_source_entities
1802 ):
1803 raise sa_exc.ArgumentError(
1804 f'Attribute "{self.path[1]}" does not link '
1805 f'from element "{parent.path[-1]}".'
1806 )
1807
1808 return self._prepend_path(parent.path)
1809
1810 def _prepend_path(self, path: PathRegistry) -> _LoadElement:
1811 cloned = self._clone()
1812
1813 assert cloned.strategy == self.strategy
1814 assert cloned.local_opts == self.local_opts
1815 assert cloned.is_class_strategy == self.is_class_strategy
1816
1817 cloned.path = PathRegistry.coerce(path[0:-1] + cloned.path[:])
1818
1819 return cloned
1820
1821 @staticmethod
1822 def _reconcile(
1823 replacement: _LoadElement, existing: _LoadElement
1824 ) -> _LoadElement:
1825 """define behavior for when two Load objects are to be put into
1826 the context.attributes under the same key.
1827
1828 :param replacement: ``_LoadElement`` that seeks to replace the
1829 existing one
1830
1831 :param existing: ``_LoadElement`` that is already present.
1832
1833 """
1834 # mapper inheritance loading requires fine-grained "block other
1835 # options" / "allow these options to be overridden" behaviors
1836 # see test_poly_loading.py
1837
1838 if replacement._reconcile_to_other:
1839 return existing
1840 elif replacement._reconcile_to_other is False:
1841 return replacement
1842 elif existing._reconcile_to_other:
1843 return replacement
1844 elif existing._reconcile_to_other is False:
1845 return existing
1846
1847 if existing is replacement:
1848 return replacement
1849 elif (
1850 existing.strategy == replacement.strategy
1851 and existing.local_opts == replacement.local_opts
1852 ):
1853 return replacement
1854 elif replacement.is_opts_only:
1855 existing = existing._clone()
1856 existing.local_opts = existing.local_opts.union(
1857 replacement.local_opts
1858 )
1859 existing._extra_criteria += replacement._extra_criteria
1860 return existing
1861 elif existing.is_opts_only:
1862 replacement = replacement._clone()
1863 replacement.local_opts = replacement.local_opts.union(
1864 existing.local_opts
1865 )
1866 replacement._extra_criteria += existing._extra_criteria
1867 return replacement
1868 elif replacement.path.is_token:
1869 # use 'last one wins' logic for wildcard options. this is also
1870 # kind of inconsistent vs. options that are specific paths which
1871 # will raise as below
1872 return replacement
1873
1874 raise sa_exc.InvalidRequestError(
1875 f"Loader strategies for {replacement.path} conflict"
1876 )
1877
1878
1879class _AttributeStrategyLoad(_LoadElement):
1880 """Loader strategies against specific relationship or column paths.
1881
1882 e.g.::
1883
1884 joinedload(User.addresses)
1885 defer(Order.name)
1886 selectinload(User.orders).lazyload(Order.items)
1887
1888 """
1889
1890 __slots__ = ("_of_type", "_path_with_polymorphic_path")
1891
1892 __visit_name__ = "attribute_strategy_load_element"
1893
1894 _traverse_internals = _LoadElement._traverse_internals + [
1895 ("_of_type", visitors.ExtendedInternalTraversal.dp_multi),
1896 (
1897 "_path_with_polymorphic_path",
1898 visitors.ExtendedInternalTraversal.dp_has_cache_key,
1899 ),
1900 ]
1901
1902 _of_type: Union[Mapper[Any], AliasedInsp[Any], None]
1903 _path_with_polymorphic_path: Optional[PathRegistry]
1904
1905 is_class_strategy = False
1906 is_token_strategy = False
1907
1908 def _init_path(
1909 self, path, attr, wildcard_key, attr_group, raiseerr, extra_criteria
1910 ):
1911 assert attr is not None
1912 self._of_type = None
1913 self._path_with_polymorphic_path = None
1914 insp, _, prop = _parse_attr_argument(attr)
1915
1916 if insp.is_property:
1917 # direct property can be sent from internal strategy logic
1918 # that sets up specific loaders, such as
1919 # emit_lazyload->_lazyload_reverse
1920 # prop = found_property = attr
1921 prop = attr
1922 path = path[prop]
1923
1924 if path.has_entity:
1925 path = path.entity_path
1926 return path
1927
1928 elif not insp.is_attribute:
1929 # should not reach here;
1930 assert False
1931
1932 # here we assume we have user-passed InstrumentedAttribute
1933 if not orm_util._entity_corresponds_to_use_path_impl(
1934 path[-1], attr.parent
1935 ):
1936 if raiseerr:
1937 if attr_group and attr is not attr_group[0]:
1938 raise sa_exc.ArgumentError(
1939 "Can't apply wildcard ('*') or load_only() "
1940 "loader option to multiple entities in the "
1941 "same option. Use separate options per entity."
1942 )
1943 else:
1944 _raise_for_does_not_link(path, str(attr), attr.parent)
1945 else:
1946 return None
1947
1948 # note the essential logic of this attribute was very different in
1949 # 1.4, where there were caching failures in e.g.
1950 # test_relationship_criteria.py::RelationshipCriteriaTest::
1951 # test_selectinload_nested_criteria[True] if an existing
1952 # "_extra_criteria" on a Load object were replaced with that coming
1953 # from an attribute. This appears to have been an artifact of how
1954 # _UnboundLoad / Load interacted together, which was opaque and
1955 # poorly defined.
1956 if extra_criteria:
1957 assert not attr._extra_criteria
1958 self._extra_criteria = extra_criteria
1959 else:
1960 self._extra_criteria = attr._extra_criteria
1961
1962 if getattr(attr, "_of_type", None):
1963 ac = attr._of_type
1964 ext_info = inspect(ac)
1965 self._of_type = ext_info
1966
1967 self._path_with_polymorphic_path = path.entity_path[prop]
1968
1969 path = path[prop][ext_info]
1970
1971 else:
1972 path = path[prop]
1973
1974 if path.has_entity:
1975 path = path.entity_path
1976
1977 return path
1978
1979 def _generate_extra_criteria(self, context):
1980 """Apply the current bound parameters in a QueryContext to the
1981 immediate "extra_criteria" stored with this Load object.
1982
1983 Load objects are typically pulled from the cached version of
1984 the statement from a QueryContext. The statement currently being
1985 executed will have new values (and keys) for bound parameters in the
1986 extra criteria which need to be applied by loader strategies when
1987 they handle this criteria for a result set.
1988
1989 """
1990
1991 assert (
1992 self._extra_criteria
1993 ), "this should only be called if _extra_criteria is present"
1994
1995 orig_query = context.compile_state.select_statement
1996 current_query = context.query
1997
1998 # NOTE: while it seems like we should not do the "apply" operation
1999 # here if orig_query is current_query, skipping it in the "optimized"
2000 # case causes the query to be different from a cache key perspective,
2001 # because we are creating a copy of the criteria which is no longer
2002 # the same identity of the _extra_criteria in the loader option
2003 # itself. cache key logic produces a different key for
2004 # (A, copy_of_A) vs. (A, A), because in the latter case it shortens
2005 # the second part of the key to just indicate on identity.
2006
2007 # if orig_query is current_query:
2008 # not cached yet. just do the and_()
2009 # return and_(*self._extra_criteria)
2010
2011 k1 = orig_query._generate_cache_key()
2012 k2 = current_query._generate_cache_key()
2013
2014 return k2._apply_params_to_element(k1, and_(*self._extra_criteria))
2015
2016 def _set_of_type_info(self, context, current_path):
2017 assert self._path_with_polymorphic_path
2018
2019 pwpi = self._of_type
2020 assert pwpi
2021 if not pwpi.is_aliased_class:
2022 pwpi = inspect(
2023 orm_util.AliasedInsp._with_polymorphic_factory(
2024 pwpi.mapper.base_mapper,
2025 (pwpi.mapper,),
2026 aliased=True,
2027 _use_mapper_path=True,
2028 )
2029 )
2030 start_path = self._path_with_polymorphic_path
2031 if current_path:
2032 new_path = self._adjust_effective_path_for_current_path(
2033 start_path, current_path
2034 )
2035 if new_path is None:
2036 return
2037 start_path = new_path
2038
2039 key = ("path_with_polymorphic", start_path.natural_path)
2040 if key in context:
2041 existing_aliased_insp = context[key]
2042 this_aliased_insp = pwpi
2043 new_aliased_insp = existing_aliased_insp._merge_with(
2044 this_aliased_insp
2045 )
2046 context[key] = new_aliased_insp
2047 else:
2048 context[key] = pwpi
2049
2050 def _prepare_for_compile_state(
2051 self,
2052 parent_loader,
2053 compile_state,
2054 mapper_entities,
2055 reconciled_lead_entity,
2056 raiseerr,
2057 ):
2058 # _AttributeStrategyLoad
2059
2060 current_path = compile_state.current_path
2061 is_refresh = compile_state.compile_options._for_refresh_state
2062 assert not self.path.is_token
2063
2064 if is_refresh and not self.propagate_to_loaders:
2065 return []
2066
2067 if self._of_type:
2068 # apply additional with_polymorphic alias that may have been
2069 # generated. this has to happen even if this is a defaultload
2070 self._set_of_type_info(compile_state.attributes, current_path)
2071
2072 # omit setting loader attributes for a "defaultload" type of option
2073 if not self.strategy and not self.local_opts:
2074 return []
2075
2076 if raiseerr and not reconciled_lead_entity:
2077 self._raise_for_no_match(parent_loader, mapper_entities)
2078
2079 if self.path.has_entity:
2080 effective_path = self.path.parent
2081 else:
2082 effective_path = self.path
2083
2084 if current_path:
2085 assert effective_path is not None
2086 effective_path = self._adjust_effective_path_for_current_path(
2087 effective_path, current_path
2088 )
2089 if effective_path is None:
2090 return []
2091
2092 return [("loader", cast(PathRegistry, effective_path).natural_path)]
2093
2094 def __getstate__(self):
2095 d = super().__getstate__()
2096
2097 # can't pickle this. See
2098 # test_pickled.py -> test_lazyload_extra_criteria_not_supported
2099 # where we should be emitting a warning for the usual case where this
2100 # would be non-None
2101 d["_extra_criteria"] = ()
2102
2103 if self._path_with_polymorphic_path:
2104 d["_path_with_polymorphic_path"] = (
2105 self._path_with_polymorphic_path.serialize()
2106 )
2107
2108 if self._of_type:
2109 if self._of_type.is_aliased_class:
2110 d["_of_type"] = None
2111 elif self._of_type.is_mapper:
2112 d["_of_type"] = self._of_type.class_
2113 else:
2114 assert False, "unexpected object for _of_type"
2115
2116 return d
2117
2118 def __setstate__(self, state):
2119 super().__setstate__(state)
2120
2121 if state.get("_path_with_polymorphic_path", None):
2122 self._path_with_polymorphic_path = PathRegistry.deserialize(
2123 state["_path_with_polymorphic_path"]
2124 )
2125 else:
2126 self._path_with_polymorphic_path = None
2127
2128 if state.get("_of_type", None):
2129 self._of_type = inspect(state["_of_type"])
2130 else:
2131 self._of_type = None
2132
2133
2134class _TokenStrategyLoad(_LoadElement):
2135 """Loader strategies against wildcard attributes
2136
2137 e.g.::
2138
2139 raiseload("*")
2140 Load(User).lazyload("*")
2141 defer("*")
2142 load_only(User.name, User.email) # will create a defer('*')
2143 joinedload(User.addresses).raiseload("*")
2144
2145 """
2146
2147 __visit_name__ = "token_strategy_load_element"
2148
2149 inherit_cache = True
2150 is_class_strategy = False
2151 is_token_strategy = True
2152
2153 def _init_path(
2154 self, path, attr, wildcard_key, attr_group, raiseerr, extra_criteria
2155 ):
2156 # assert isinstance(attr, str) or attr is None
2157 if attr is not None:
2158 default_token = attr.endswith(_DEFAULT_TOKEN)
2159 if attr.endswith(_WILDCARD_TOKEN) or default_token:
2160 if wildcard_key:
2161 attr = f"{wildcard_key}:{attr}"
2162
2163 path = path.token(attr)
2164 return path
2165 else:
2166 raise sa_exc.ArgumentError(
2167 "Strings are not accepted for attribute names in loader "
2168 "options; please use class-bound attributes directly."
2169 )
2170 return path
2171
2172 def _prepare_for_compile_state(
2173 self,
2174 parent_loader,
2175 compile_state,
2176 mapper_entities,
2177 reconciled_lead_entity,
2178 raiseerr,
2179 ):
2180 # _TokenStrategyLoad
2181
2182 current_path = compile_state.current_path
2183 is_refresh = compile_state.compile_options._for_refresh_state
2184
2185 assert self.path.is_token
2186
2187 if is_refresh and not self.propagate_to_loaders:
2188 return []
2189
2190 # omit setting attributes for a "defaultload" type of option
2191 if not self.strategy and not self.local_opts:
2192 return []
2193
2194 effective_path = self.path
2195 if reconciled_lead_entity:
2196 effective_path = PathRegistry.coerce(
2197 (reconciled_lead_entity,) + effective_path.path[1:]
2198 )
2199
2200 if current_path:
2201 new_effective_path = self._adjust_effective_path_for_current_path(
2202 effective_path, current_path
2203 )
2204 if new_effective_path is None:
2205 return []
2206 effective_path = new_effective_path
2207
2208 # for a wildcard token, expand out the path we set
2209 # to encompass everything from the query entity on
2210 # forward. not clear if this is necessary when current_path
2211 # is set.
2212
2213 return [
2214 ("loader", natural_path)
2215 for natural_path in (
2216 cast(
2217 TokenRegistry, effective_path
2218 )._generate_natural_for_superclasses()
2219 )
2220 ]
2221
2222
2223class _ClassStrategyLoad(_LoadElement):
2224 """Loader strategies that deals with a class as a target, not
2225 an attribute path
2226
2227 e.g.::
2228
2229 q = s.query(Person).options(
2230 selectin_polymorphic(Person, [Engineer, Manager])
2231 )
2232
2233 """
2234
2235 inherit_cache = True
2236 is_class_strategy = True
2237 is_token_strategy = False
2238
2239 __visit_name__ = "class_strategy_load_element"
2240
2241 def _init_path(
2242 self, path, attr, wildcard_key, attr_group, raiseerr, extra_criteria
2243 ):
2244 return path
2245
2246 def _prepare_for_compile_state(
2247 self,
2248 parent_loader,
2249 compile_state,
2250 mapper_entities,
2251 reconciled_lead_entity,
2252 raiseerr,
2253 ):
2254 # _ClassStrategyLoad
2255
2256 current_path = compile_state.current_path
2257 is_refresh = compile_state.compile_options._for_refresh_state
2258
2259 if is_refresh and not self.propagate_to_loaders:
2260 return []
2261
2262 # omit setting attributes for a "defaultload" type of option
2263 if not self.strategy and not self.local_opts:
2264 return []
2265
2266 effective_path = self.path
2267
2268 if current_path:
2269 new_effective_path = self._adjust_effective_path_for_current_path(
2270 effective_path, current_path
2271 )
2272 if new_effective_path is None:
2273 return []
2274 effective_path = new_effective_path
2275
2276 return [("loader", effective_path.natural_path)]
2277
2278
2279def _generate_from_keys(
2280 meth: Callable[..., _AbstractLoad],
2281 keys: Tuple[_AttrType, ...],
2282 chained: bool,
2283 kw: Any,
2284) -> _AbstractLoad:
2285 lead_element: Optional[_AbstractLoad] = None
2286
2287 attr: Any
2288 for is_default, _keys in (True, keys[0:-1]), (False, keys[-1:]):
2289 for attr in _keys:
2290 if isinstance(attr, str):
2291 if attr.startswith("." + _WILDCARD_TOKEN):
2292 util.warn_deprecated(
2293 "The undocumented `.{WILDCARD}` format is "
2294 "deprecated "
2295 "and will be removed in a future version as "
2296 "it is "
2297 "believed to be unused. "
2298 "If you have been using this functionality, "
2299 "please "
2300 "comment on Issue #4390 on the SQLAlchemy project "
2301 "tracker.",
2302 version="1.4",
2303 )
2304 attr = attr[1:]
2305
2306 if attr == _WILDCARD_TOKEN:
2307 if is_default:
2308 raise sa_exc.ArgumentError(
2309 "Wildcard token cannot be followed by "
2310 "another entity",
2311 )
2312
2313 if lead_element is None:
2314 lead_element = _WildcardLoad()
2315
2316 lead_element = meth(lead_element, _DEFAULT_TOKEN, **kw)
2317
2318 else:
2319 raise sa_exc.ArgumentError(
2320 "Strings are not accepted for attribute names in "
2321 "loader options; please use class-bound "
2322 "attributes directly.",
2323 )
2324 else:
2325 if lead_element is None:
2326 _, lead_entity, _ = _parse_attr_argument(attr)
2327 lead_element = Load(lead_entity)
2328
2329 if is_default:
2330 if not chained:
2331 lead_element = lead_element.defaultload(attr)
2332 else:
2333 lead_element = meth(
2334 lead_element, attr, _is_chain=True, **kw
2335 )
2336 else:
2337 lead_element = meth(lead_element, attr, **kw)
2338
2339 assert lead_element
2340 return lead_element
2341
2342
2343def _parse_attr_argument(
2344 attr: _AttrType,
2345) -> Tuple[InspectionAttr, _InternalEntityType[Any], MapperProperty[Any]]:
2346 """parse an attribute or wildcard argument to produce an
2347 :class:`._AbstractLoad` instance.
2348
2349 This is used by the standalone loader strategy functions like
2350 ``joinedload()``, ``defer()``, etc. to produce :class:`_orm.Load` or
2351 :class:`._WildcardLoad` objects.
2352
2353 """
2354 try:
2355 # TODO: need to figure out this None thing being returned by
2356 # inspect(), it should not have None as an option in most cases
2357 # if at all
2358 insp: InspectionAttr = inspect(attr) # type: ignore
2359 except sa_exc.NoInspectionAvailable as err:
2360 raise sa_exc.ArgumentError(
2361 "expected ORM mapped attribute for loader strategy argument"
2362 ) from err
2363
2364 lead_entity: _InternalEntityType[Any]
2365
2366 if insp_is_mapper_property(insp):
2367 lead_entity = insp.parent
2368 prop = insp
2369 elif insp_is_attribute(insp):
2370 lead_entity = insp.parent
2371 prop = insp.prop
2372 else:
2373 raise sa_exc.ArgumentError(
2374 "expected ORM mapped attribute for loader strategy argument"
2375 )
2376
2377 return insp, lead_entity, prop
2378
2379
2380def loader_unbound_fn(fn: _FN) -> _FN:
2381 """decorator that applies docstrings between standalone loader functions
2382 and the loader methods on :class:`._AbstractLoad`.
2383
2384 """
2385 bound_fn = getattr(_AbstractLoad, fn.__name__)
2386 fn_doc = bound_fn.__doc__
2387 bound_fn.__doc__ = f"""Produce a new :class:`_orm.Load` object with the
2388:func:`_orm.{fn.__name__}` option applied.
2389
2390See :func:`_orm.{fn.__name__}` for usage examples.
2391
2392"""
2393
2394 fn.__doc__ = fn_doc
2395 return fn
2396
2397
2398def _expand_column_strategy_attrs(
2399 attrs: Tuple[_AttrType, ...],
2400) -> Tuple[_AttrType, ...]:
2401 return cast(
2402 "Tuple[_AttrType, ...]",
2403 tuple(
2404 a
2405 for attr in attrs
2406 for a in (
2407 cast("QueryableAttribute[Any]", attr)._column_strategy_attrs()
2408 if hasattr(attr, "_column_strategy_attrs")
2409 else (attr,)
2410 )
2411 ),
2412 )
2413
2414
2415# standalone functions follow. docstrings are filled in
2416# by the ``@loader_unbound_fn`` decorator.
2417
2418
2419@loader_unbound_fn
2420def contains_eager(*keys: _AttrType, **kw: Any) -> _AbstractLoad:
2421 return _generate_from_keys(Load.contains_eager, keys, True, kw)
2422
2423
2424@loader_unbound_fn
2425def load_only(*attrs: _AttrType, raiseload: bool = False) -> _AbstractLoad:
2426 # TODO: attrs against different classes. we likely have to
2427 # add some extra state to Load of some kind
2428 attrs = _expand_column_strategy_attrs(attrs)
2429 _, lead_element, _ = _parse_attr_argument(attrs[0])
2430 return Load(lead_element).load_only(*attrs, raiseload=raiseload)
2431
2432
2433@loader_unbound_fn
2434def joinedload(*keys: _AttrType, **kw: Any) -> _AbstractLoad:
2435 return _generate_from_keys(Load.joinedload, keys, False, kw)
2436
2437
2438@loader_unbound_fn
2439def subqueryload(*keys: _AttrType) -> _AbstractLoad:
2440 return _generate_from_keys(Load.subqueryload, keys, False, {})
2441
2442
2443@loader_unbound_fn
2444def selectinload(
2445 *keys: _AttrType, recursion_depth: Optional[int] = None
2446) -> _AbstractLoad:
2447 return _generate_from_keys(
2448 Load.selectinload, keys, False, {"recursion_depth": recursion_depth}
2449 )
2450
2451
2452@loader_unbound_fn
2453def lazyload(*keys: _AttrType) -> _AbstractLoad:
2454 return _generate_from_keys(Load.lazyload, keys, False, {})
2455
2456
2457@loader_unbound_fn
2458def immediateload(
2459 *keys: _AttrType, recursion_depth: Optional[int] = None
2460) -> _AbstractLoad:
2461 return _generate_from_keys(
2462 Load.immediateload, keys, False, {"recursion_depth": recursion_depth}
2463 )
2464
2465
2466@loader_unbound_fn
2467def noload(*keys: _AttrType) -> _AbstractLoad:
2468 return _generate_from_keys(Load.noload, keys, False, {})
2469
2470
2471@loader_unbound_fn
2472def raiseload(*keys: _AttrType, **kw: Any) -> _AbstractLoad:
2473 return _generate_from_keys(Load.raiseload, keys, False, kw)
2474
2475
2476@loader_unbound_fn
2477def defaultload(*keys: _AttrType) -> _AbstractLoad:
2478 return _generate_from_keys(Load.defaultload, keys, False, {})
2479
2480
2481@loader_unbound_fn
2482def defer(
2483 key: _AttrType, *addl_attrs: _AttrType, raiseload: bool = False
2484) -> _AbstractLoad:
2485 if addl_attrs:
2486 util.warn_deprecated(
2487 "The *addl_attrs on orm.defer is deprecated. Please use "
2488 "method chaining in conjunction with defaultload() to "
2489 "indicate a path.",
2490 version="1.3",
2491 )
2492
2493 if raiseload:
2494 kw = {"raiseload": raiseload}
2495 else:
2496 kw = {}
2497
2498 return _generate_from_keys(Load.defer, (key,) + addl_attrs, False, kw)
2499
2500
2501@loader_unbound_fn
2502def undefer(key: _AttrType, *addl_attrs: _AttrType) -> _AbstractLoad:
2503 if addl_attrs:
2504 util.warn_deprecated(
2505 "The *addl_attrs on orm.undefer is deprecated. Please use "
2506 "method chaining in conjunction with defaultload() to "
2507 "indicate a path.",
2508 version="1.3",
2509 )
2510 return _generate_from_keys(Load.undefer, (key,) + addl_attrs, False, {})
2511
2512
2513@loader_unbound_fn
2514def undefer_group(name: str) -> _AbstractLoad:
2515 element = _WildcardLoad()
2516 return element.undefer_group(name)
2517
2518
2519@loader_unbound_fn
2520def with_expression(
2521 key: _AttrType, expression: _ColumnExpressionArgument[Any]
2522) -> _AbstractLoad:
2523 return _generate_from_keys(
2524 Load.with_expression, (key,), False, {"expression": expression}
2525 )
2526
2527
2528@loader_unbound_fn
2529def selectin_polymorphic(
2530 base_cls: _EntityType[Any], classes: Iterable[Type[Any]]
2531) -> _AbstractLoad:
2532 ul = Load(base_cls)
2533 return ul.selectin_polymorphic(classes)
2534
2535
2536def _raise_for_does_not_link(path, attrname, parent_entity):
2537 if len(path) > 1:
2538 path_is_of_type = path[-1].entity is not path[-2].mapper.class_
2539 if insp_is_aliased_class(parent_entity):
2540 parent_entity_str = str(parent_entity)
2541 else:
2542 parent_entity_str = parent_entity.class_.__name__
2543
2544 raise sa_exc.ArgumentError(
2545 f'ORM mapped entity or attribute "{attrname}" does not '
2546 f'link from relationship "{path[-2]}%s".%s'
2547 % (
2548 f".of_type({path[-1]})" if path_is_of_type else "",
2549 (
2550 " Did you mean to use "
2551 f'"{path[-2]}'
2552 f'.of_type({parent_entity_str})" or "loadopt.options('
2553 f"selectin_polymorphic({path[-2].mapper.class_.__name__}, "
2554 f'[{parent_entity_str}]), ...)" ?'
2555 if not path_is_of_type
2556 and not path[-1].is_aliased_class
2557 and orm_util._entity_corresponds_to(
2558 path.entity, inspect(parent_entity).mapper
2559 )
2560 else ""
2561 ),
2562 )
2563 )
2564 else:
2565 raise sa_exc.ArgumentError(
2566 f'ORM mapped attribute "{attrname}" does not '
2567 f'link mapped class "{path[-1]}"'
2568 )