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