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