1# orm/strategies.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: ignore-errors
8
9
10"""sqlalchemy.orm.interfaces.LoaderStrategy
11implementations, and related MapperOptions."""
12
13from __future__ import annotations
14
15import collections
16from typing import Any
17from typing import Dict
18from typing import Literal
19from typing import Optional
20from typing import Tuple
21from typing import TYPE_CHECKING
22from typing import Union
23
24from . import attributes
25from . import exc as orm_exc
26from . import interfaces
27from . import loading
28from . import path_registry
29from . import properties
30from . import query
31from . import relationships
32from . import unitofwork
33from . import util as orm_util
34from .base import _DEFER_FOR_STATE
35from .base import _RAISE_FOR_STATE
36from .base import _SET_DEFERRED_EXPIRED
37from .base import ATTR_WAS_SET
38from .base import LoaderCallableStatus
39from .base import PASSIVE_OFF
40from .base import PassiveFlag
41from .context import _column_descriptions
42from .context import _ORMCompileState
43from .context import _ORMSelectCompileState
44from .context import QueryContext
45from .interfaces import LoaderStrategy
46from .interfaces import StrategizedProperty
47from .session import _state_session
48from .state import InstanceState
49from .strategy_options import Load
50from .util import _none_only_set
51from .util import AliasedClass
52from .. import event
53from .. import exc as sa_exc
54from .. import inspect
55from .. import log
56from .. import sql
57from .. import util
58from ..sql import util as sql_util
59from ..sql import visitors
60from ..sql.selectable import LABEL_STYLE_TABLENAME_PLUS_COL
61from ..sql.selectable import Select
62
63if TYPE_CHECKING:
64 from .mapper import Mapper
65 from .relationships import RelationshipProperty
66 from ..sql.elements import ColumnElement
67
68
69def _register_attribute(
70 prop,
71 mapper,
72 useobject,
73 compare_function=None,
74 typecallable=None,
75 callable_=None,
76 proxy_property=None,
77 active_history=False,
78 impl_class=None,
79 default_scalar_value=None,
80 **kw,
81):
82
83 # event-hook registration functions organized into "pre validate" and "post
84 # validate" collections. Each hook registration function generates a new
85 # AttributeEvents registration for this attribute. What we are controlling
86 # here is the order in which these attribute events are established. Hook
87 # registration functions invoked in the order of first pre-validate, then
88 # user-specific validation i.e. `@validates`, then post-validate.
89 # within `@validates` we are scanning for hooks from child-most classes
90 # first (this suits the feature added in #2943).
91 pre_validate_hooks = []
92 post_validate_hooks = []
93
94 uselist = useobject and prop.uselist
95
96 if useobject and prop.single_parent:
97 pre_validate_hooks.append(_single_parent_validator)
98
99 if useobject:
100 post_validate_hooks.append(unitofwork._track_cascade_events)
101
102 # need to assemble backref listeners
103 # after the singleparentvalidator, mapper validator
104 if useobject:
105 backref = prop.back_populates
106 if backref and prop._effective_sync_backref:
107 post_validate_hooks.append(
108 lambda desc, prop: attributes._backref_listeners(
109 desc, backref, uselist
110 )
111 )
112
113 # a single MapperProperty is shared down a class inheritance
114 # hierarchy, so we set up attribute instrumentation and backref event
115 # for each mapper down the hierarchy.
116
117 # typically, "mapper" is the same as prop.parent, due to the way
118 # the configure_mappers() process runs, however this is not strongly
119 # enforced, and in the case of a second configure_mappers() run the
120 # mapper here might not be prop.parent; also, a subclass mapper may
121 # be called here before a superclass mapper. That is, can't depend
122 # on mappers not already being set up so we have to check each one.
123
124 for m in mapper.self_and_descendants:
125 if prop is m._props.get(
126 prop.key
127 ) and not m.class_manager._attr_has_impl(prop.key):
128 desc = attributes._register_attribute_impl(
129 m.class_,
130 prop.key,
131 parent_token=prop,
132 uselist=uselist,
133 compare_function=compare_function,
134 useobject=useobject,
135 trackparent=useobject
136 and (
137 prop.single_parent
138 or prop.direction is interfaces.ONETOMANY
139 ),
140 typecallable=typecallable,
141 callable_=callable_,
142 active_history=active_history,
143 default_scalar_value=default_scalar_value,
144 impl_class=impl_class,
145 send_modified_events=not useobject or not prop.viewonly,
146 doc=prop.doc,
147 **kw,
148 )
149
150 for hook in pre_validate_hooks:
151 hook(desc, prop)
152
153 for super_m in m.iterate_to_root():
154 if prop.key in super_m.validators:
155 fn, opts = super_m.validators[prop.key]
156 orm_util._validator_events(desc, prop.key, fn, **opts)
157 break
158
159 for hook in post_validate_hooks:
160 hook(desc, prop)
161
162
163@properties.ColumnProperty.strategy_for(instrument=False, deferred=False)
164class _UninstrumentedColumnLoader(LoaderStrategy):
165 """Represent a non-instrumented MapperProperty.
166
167 The polymorphic_on argument of mapper() often results in this,
168 if the argument is against the with_polymorphic selectable.
169
170 """
171
172 __slots__ = ("columns",)
173
174 def __init__(self, parent, strategy_key):
175 super().__init__(parent, strategy_key)
176 self.columns = self.parent_property.columns
177
178 def setup_query(
179 self,
180 compile_state,
181 query_entity,
182 path,
183 loadopt,
184 adapter,
185 column_collection=None,
186 **kwargs,
187 ):
188 for c in self.columns:
189 if adapter:
190 c = adapter.columns[c]
191 compile_state._append_dedupe_col_collection(c, column_collection)
192
193 def create_row_processor(
194 self,
195 context,
196 query_entity,
197 path,
198 loadopt,
199 mapper,
200 result,
201 adapter,
202 populators,
203 ):
204 pass
205
206
207@log.class_logger
208@properties.ColumnProperty.strategy_for(instrument=True, deferred=False)
209class _ColumnLoader(LoaderStrategy):
210 """Provide loading behavior for a :class:`.ColumnProperty`."""
211
212 __slots__ = "columns", "is_composite"
213
214 def __init__(self, parent, strategy_key):
215 super().__init__(parent, strategy_key)
216 self.columns = self.parent_property.columns
217 self.is_composite = hasattr(self.parent_property, "composite_class")
218
219 def setup_query(
220 self,
221 compile_state,
222 query_entity,
223 path,
224 loadopt,
225 adapter,
226 column_collection,
227 memoized_populators,
228 check_for_adapt=False,
229 **kwargs,
230 ):
231 for c in self.columns:
232 if adapter:
233 if check_for_adapt:
234 c = adapter.adapt_check_present(c)
235 if c is None:
236 return
237 else:
238 c = adapter.columns[c]
239
240 compile_state._append_dedupe_col_collection(c, column_collection)
241
242 fetch = self.columns[0]
243 if adapter:
244 fetch = adapter.columns[fetch]
245 if fetch is None:
246 # None happens here only for dml bulk_persistence cases
247 # when context.DMLReturningColFilter is used
248 return
249
250 memoized_populators[self.parent_property] = fetch
251
252 def init_class_attribute(self, mapper):
253 self.is_class_level = True
254 coltype = self.columns[0].type
255 # TODO: check all columns ? check for foreign key as well?
256 active_history = (
257 self.parent_property.active_history
258 or self.columns[0].primary_key
259 or (
260 mapper.version_id_col is not None
261 and mapper._columntoproperty.get(mapper.version_id_col, None)
262 is self.parent_property
263 )
264 )
265
266 _register_attribute(
267 self.parent_property,
268 mapper,
269 useobject=False,
270 compare_function=coltype.compare_values,
271 active_history=active_history,
272 default_scalar_value=self.parent_property._default_scalar_value,
273 )
274
275 def create_row_processor(
276 self,
277 context,
278 query_entity,
279 path,
280 loadopt,
281 mapper,
282 result,
283 adapter,
284 populators,
285 ):
286 # look through list of columns represented here
287 # to see which, if any, is present in the row.
288
289 for col in self.columns:
290 if adapter:
291 col = adapter.columns[col]
292 getter = result._getter(col, False)
293 if getter:
294 populators["quick"].append((self.key, getter))
295 break
296 else:
297 populators["expire"].append((self.key, True))
298
299
300@log.class_logger
301@properties.ColumnProperty.strategy_for(query_expression=True)
302class _ExpressionColumnLoader(_ColumnLoader):
303 def __init__(self, parent, strategy_key):
304 super().__init__(parent, strategy_key)
305
306 # compare to the "default" expression that is mapped in
307 # the column. If it's sql.null, we don't need to render
308 # unless an expr is passed in the options.
309 null = sql.null().label(None)
310 self._have_default_expression = any(
311 not c.compare(null) for c in self.parent_property.columns
312 )
313
314 def setup_query(
315 self,
316 compile_state,
317 query_entity,
318 path,
319 loadopt,
320 adapter,
321 column_collection,
322 memoized_populators,
323 **kwargs,
324 ):
325 columns = None
326 if loadopt and loadopt._extra_criteria:
327 columns = loadopt._extra_criteria
328
329 elif self._have_default_expression:
330 columns = self.parent_property.columns
331
332 if columns is None:
333 return
334
335 for c in columns:
336 if adapter:
337 c = adapter.columns[c]
338 compile_state._append_dedupe_col_collection(c, column_collection)
339
340 fetch = columns[0]
341 if adapter:
342 fetch = adapter.columns[fetch]
343 if fetch is None:
344 # None is not expected to be the result of any
345 # adapter implementation here, however there may be theoretical
346 # usages of returning() with context.DMLReturningColFilter
347 return
348
349 memoized_populators[self.parent_property] = fetch
350
351 # if the column being loaded is the polymorphic discriminator,
352 # and we have a with_expression() providing the actual column,
353 # update the query_entity to use the actual column instead of
354 # the default expression
355 if (
356 query_entity._polymorphic_discriminator is self.columns[0]
357 and loadopt
358 and loadopt._extra_criteria
359 ):
360 query_entity._polymorphic_discriminator = columns[0]
361
362 def create_row_processor(
363 self,
364 context,
365 query_entity,
366 path,
367 loadopt,
368 mapper,
369 result,
370 adapter,
371 populators,
372 ):
373 # look through list of columns represented here
374 # to see which, if any, is present in the row.
375 if loadopt and loadopt._extra_criteria:
376 columns = loadopt._extra_criteria
377
378 for col in columns:
379 if adapter:
380 col = adapter.columns[col]
381 getter = result._getter(col, False)
382 if getter:
383 populators["quick"].append((self.key, getter))
384 break
385 else:
386 populators["expire"].append((self.key, True))
387
388 def init_class_attribute(self, mapper):
389 self.is_class_level = True
390
391 _register_attribute(
392 self.parent_property,
393 mapper,
394 useobject=False,
395 compare_function=self.columns[0].type.compare_values,
396 accepts_scalar_loader=False,
397 default_scalar_value=self.parent_property._default_scalar_value,
398 )
399
400
401@log.class_logger
402@properties.ColumnProperty.strategy_for(deferred=True, instrument=True)
403@properties.ColumnProperty.strategy_for(
404 deferred=True, instrument=True, raiseload=True
405)
406@properties.ColumnProperty.strategy_for(do_nothing=True)
407class _DeferredColumnLoader(LoaderStrategy):
408 """Provide loading behavior for a deferred :class:`.ColumnProperty`."""
409
410 __slots__ = "columns", "group", "raiseload"
411
412 def __init__(self, parent, strategy_key):
413 super().__init__(parent, strategy_key)
414 if hasattr(self.parent_property, "composite_class"):
415 raise NotImplementedError(
416 "Deferred loading for composite types not implemented yet"
417 )
418 self.raiseload = self.strategy_opts.get("raiseload", False)
419 self.columns = self.parent_property.columns
420 self.group = self.parent_property.group
421
422 def create_row_processor(
423 self,
424 context,
425 query_entity,
426 path,
427 loadopt,
428 mapper,
429 result,
430 adapter,
431 populators,
432 ):
433 # for a DeferredColumnLoader, this method is only used during a
434 # "row processor only" query; see test_deferred.py ->
435 # tests with "rowproc_only" in their name. As of the 1.0 series,
436 # loading._instance_processor doesn't use a "row processing" function
437 # to populate columns, instead it uses data in the "populators"
438 # dictionary. Normally, the DeferredColumnLoader.setup_query()
439 # sets up that data in the "memoized_populators" dictionary
440 # and "create_row_processor()" here is never invoked.
441
442 if (
443 context.refresh_state
444 and context.query._compile_options._only_load_props
445 and self.key in context.query._compile_options._only_load_props
446 ):
447 self.parent_property._get_strategy(
448 (("deferred", False), ("instrument", True))
449 ).create_row_processor(
450 context,
451 query_entity,
452 path,
453 loadopt,
454 mapper,
455 result,
456 adapter,
457 populators,
458 )
459
460 elif not self.is_class_level:
461 if self.raiseload:
462 set_deferred_for_local_state = (
463 self.parent_property._raise_column_loader
464 )
465 else:
466 set_deferred_for_local_state = (
467 self.parent_property._deferred_column_loader
468 )
469 populators["new"].append((self.key, set_deferred_for_local_state))
470 else:
471 populators["expire"].append((self.key, False))
472
473 def init_class_attribute(self, mapper):
474 self.is_class_level = True
475
476 _register_attribute(
477 self.parent_property,
478 mapper,
479 useobject=False,
480 compare_function=self.columns[0].type.compare_values,
481 callable_=self._load_for_state,
482 load_on_unexpire=False,
483 default_scalar_value=self.parent_property._default_scalar_value,
484 )
485
486 def setup_query(
487 self,
488 compile_state,
489 query_entity,
490 path,
491 loadopt,
492 adapter,
493 column_collection,
494 memoized_populators,
495 only_load_props=None,
496 **kw,
497 ):
498 if (
499 (
500 compile_state.compile_options._render_for_subquery
501 and self.parent_property._renders_in_subqueries
502 )
503 or (
504 loadopt
505 and set(self.columns).intersection(
506 self.parent._should_undefer_in_wildcard
507 )
508 )
509 or (
510 loadopt
511 and self.group
512 and loadopt.local_opts.get(
513 "undefer_group_%s" % self.group, False
514 )
515 )
516 or (only_load_props and self.key in only_load_props)
517 ):
518 self.parent_property._get_strategy(
519 (("deferred", False), ("instrument", True))
520 ).setup_query(
521 compile_state,
522 query_entity,
523 path,
524 loadopt,
525 adapter,
526 column_collection,
527 memoized_populators,
528 **kw,
529 )
530 elif self.is_class_level:
531 memoized_populators[self.parent_property] = _SET_DEFERRED_EXPIRED
532 elif not self.raiseload:
533 memoized_populators[self.parent_property] = _DEFER_FOR_STATE
534 else:
535 memoized_populators[self.parent_property] = _RAISE_FOR_STATE
536
537 def _load_for_state(self, state, passive):
538 if not state.key:
539 return LoaderCallableStatus.ATTR_EMPTY
540
541 if not passive & PassiveFlag.SQL_OK:
542 return LoaderCallableStatus.PASSIVE_NO_RESULT
543
544 localparent = state.manager.mapper
545
546 if self.group:
547 toload = [
548 p.key
549 for p in localparent.iterate_properties
550 if isinstance(p, StrategizedProperty)
551 and isinstance(p.strategy, _DeferredColumnLoader)
552 and p.group == self.group
553 ]
554 else:
555 toload = [self.key]
556
557 # narrow the keys down to just those which have no history
558 group = [k for k in toload if k in state.unmodified]
559
560 session = _state_session(state)
561 if session is None:
562 raise orm_exc.DetachedInstanceError(
563 "Parent instance %s is not bound to a Session; "
564 "deferred load operation of attribute '%s' cannot proceed"
565 % (orm_util.state_str(state), self.key)
566 )
567
568 if self.raiseload:
569 self._invoke_raise_load(state, passive, "raise")
570
571 loading._load_scalar_attributes(
572 state.mapper, state, set(group), PASSIVE_OFF
573 )
574
575 return LoaderCallableStatus.ATTR_WAS_SET
576
577 def _invoke_raise_load(self, state, passive, lazy):
578 raise sa_exc.InvalidRequestError(
579 "'%s' is not available due to raiseload=True" % (self,)
580 )
581
582
583class _LoadDeferredColumns:
584 """serializable loader object used by DeferredColumnLoader"""
585
586 def __init__(self, key: str, raiseload: bool = False):
587 self.key = key
588 self.raiseload = raiseload
589
590 def __call__(self, state, passive=attributes.PASSIVE_OFF):
591 key = self.key
592
593 localparent = state.manager.mapper
594 prop = localparent._props[key]
595 if self.raiseload:
596 strategy_key = (
597 ("deferred", True),
598 ("instrument", True),
599 ("raiseload", True),
600 )
601 else:
602 strategy_key = (("deferred", True), ("instrument", True))
603 strategy = prop._get_strategy(strategy_key)
604 return strategy._load_for_state(state, passive)
605
606
607class _AbstractRelationshipLoader(LoaderStrategy):
608 """LoaderStratgies which deal with related objects."""
609
610 __slots__ = "mapper", "target", "uselist", "entity"
611
612 def __init__(self, parent, strategy_key):
613 super().__init__(parent, strategy_key)
614 self.mapper = self.parent_property.mapper
615 self.entity = self.parent_property.entity
616 self.target = self.parent_property.target
617 self.uselist = self.parent_property.uselist
618
619 def _immediateload_create_row_processor(
620 self,
621 context,
622 query_entity,
623 path,
624 loadopt,
625 mapper,
626 result,
627 adapter,
628 populators,
629 ):
630 return self.parent_property._get_strategy(
631 (("lazy", "immediate"),)
632 ).create_row_processor(
633 context,
634 query_entity,
635 path,
636 loadopt,
637 mapper,
638 result,
639 adapter,
640 populators,
641 )
642
643
644@log.class_logger
645@relationships.RelationshipProperty.strategy_for(do_nothing=True)
646class _DoNothingLoader(LoaderStrategy):
647 """Relationship loader that makes no change to the object's state.
648
649 Compared to NoLoader, this loader does not initialize the
650 collection/attribute to empty/none; the usual default LazyLoader will
651 take effect.
652
653 """
654
655
656@log.class_logger
657@relationships.RelationshipProperty.strategy_for(lazy="noload")
658@relationships.RelationshipProperty.strategy_for(lazy=None)
659class _NoLoader(_AbstractRelationshipLoader):
660 """Provide loading behavior for a :class:`.Relationship`
661 with "lazy=None".
662
663 """
664
665 __slots__ = ()
666
667 @util.deprecated(
668 "2.1",
669 "The ``noload`` loader strategy is deprecated and will be removed "
670 "in a future release. This option "
671 "produces incorrect results by returning ``None`` for related "
672 "items.",
673 )
674 def init_class_attribute(self, mapper):
675 self.is_class_level = True
676
677 _register_attribute(
678 self.parent_property,
679 mapper,
680 useobject=True,
681 typecallable=self.parent_property.collection_class,
682 )
683
684 def create_row_processor(
685 self,
686 context,
687 query_entity,
688 path,
689 loadopt,
690 mapper,
691 result,
692 adapter,
693 populators,
694 ):
695 def invoke_no_load(state, dict_, row):
696 if self.uselist:
697 attributes.init_state_collection(state, dict_, self.key)
698 else:
699 dict_[self.key] = None
700
701 populators["new"].append((self.key, invoke_no_load))
702
703
704@log.class_logger
705@relationships.RelationshipProperty.strategy_for(lazy=True)
706@relationships.RelationshipProperty.strategy_for(lazy="select")
707@relationships.RelationshipProperty.strategy_for(lazy="raise")
708@relationships.RelationshipProperty.strategy_for(lazy="raise_on_sql")
709@relationships.RelationshipProperty.strategy_for(lazy="baked_select")
710class _LazyLoader(
711 _AbstractRelationshipLoader, util.MemoizedSlots, log.Identified
712):
713 """Provide loading behavior for a :class:`.Relationship`
714 with "lazy=True", that is loads when first accessed.
715
716 """
717
718 __slots__ = (
719 "_lazywhere",
720 "_rev_lazywhere",
721 "_lazyload_reverse_option",
722 "_order_by",
723 "use_get",
724 "is_aliased_class",
725 "_bind_to_col",
726 "_equated_columns",
727 "_rev_bind_to_col",
728 "_rev_equated_columns",
729 "_simple_lazy_clause",
730 "_raise_always",
731 "_raise_on_sql",
732 )
733
734 _lazywhere: ColumnElement[bool]
735 _bind_to_col: Dict[str, ColumnElement[Any]]
736 _rev_lazywhere: ColumnElement[bool]
737 _rev_bind_to_col: Dict[str, ColumnElement[Any]]
738
739 parent_property: RelationshipProperty[Any]
740
741 def __init__(
742 self, parent: RelationshipProperty[Any], strategy_key: Tuple[Any, ...]
743 ):
744 super().__init__(parent, strategy_key)
745 self._raise_always = self.strategy_opts["lazy"] == "raise"
746 self._raise_on_sql = self.strategy_opts["lazy"] == "raise_on_sql"
747
748 self.is_aliased_class = inspect(self.entity).is_aliased_class
749
750 join_condition = self.parent_property._join_condition
751 (
752 self._lazywhere,
753 self._bind_to_col,
754 self._equated_columns,
755 ) = join_condition.create_lazy_clause()
756
757 (
758 self._rev_lazywhere,
759 self._rev_bind_to_col,
760 self._rev_equated_columns,
761 ) = join_condition.create_lazy_clause(reverse_direction=True)
762
763 if self.parent_property.order_by:
764 self._order_by = util.to_list(self.parent_property.order_by)
765 else:
766 self._order_by = None
767
768 self.logger.info("%s lazy loading clause %s", self, self._lazywhere)
769
770 # determine if our "lazywhere" clause is the same as the mapper's
771 # get() clause. then we can just use mapper.get()
772 #
773 # TODO: the "not self.uselist" can be taken out entirely; a m2o
774 # load that populates for a list (very unusual, but is possible with
775 # the API) can still set for "None" and the attribute system will
776 # populate as an empty list.
777 self.use_get = (
778 not self.is_aliased_class
779 and not self.uselist
780 and self.entity._get_clause[0].compare(
781 self._lazywhere,
782 use_proxies=True,
783 compare_keys=False,
784 equivalents=self.mapper._equivalent_columns,
785 )
786 )
787
788 if self.use_get:
789 for col in list(self._equated_columns):
790 if col in self.mapper._equivalent_columns:
791 for c in self.mapper._equivalent_columns[col]:
792 self._equated_columns[c] = self._equated_columns[col]
793
794 self.logger.info(
795 "%s will use Session.get() to optimize instance loads", self
796 )
797
798 def init_class_attribute(self, mapper):
799 self.is_class_level = True
800
801 _legacy_inactive_history_style = (
802 self.parent_property._legacy_inactive_history_style
803 )
804
805 if self.parent_property.active_history:
806 active_history = True
807 _deferred_history = False
808
809 elif (
810 self.parent_property.direction is not interfaces.MANYTOONE
811 or not self.use_get
812 ):
813 if _legacy_inactive_history_style:
814 active_history = True
815 _deferred_history = False
816 else:
817 active_history = False
818 _deferred_history = True
819 else:
820 active_history = _deferred_history = False
821
822 _register_attribute(
823 self.parent_property,
824 mapper,
825 useobject=True,
826 callable_=self._load_for_state,
827 typecallable=self.parent_property.collection_class,
828 active_history=active_history,
829 _deferred_history=_deferred_history,
830 )
831
832 def _memoized_attr__simple_lazy_clause(self):
833 lazywhere = self._lazywhere
834
835 criterion, bind_to_col = (lazywhere, self._bind_to_col)
836
837 params = []
838
839 def visit_bindparam(bindparam):
840 bindparam.unique = False
841
842 visitors.traverse(criterion, {}, {"bindparam": visit_bindparam})
843
844 def visit_bindparam(bindparam):
845 if bindparam._identifying_key in bind_to_col:
846 params.append(
847 (
848 bindparam.key,
849 bind_to_col[bindparam._identifying_key],
850 None,
851 )
852 )
853 elif bindparam.callable is None:
854 params.append((bindparam.key, None, bindparam.value))
855
856 criterion = visitors.cloned_traverse(
857 criterion, {}, {"bindparam": visit_bindparam}
858 )
859
860 return criterion, params
861
862 def _generate_lazy_clause(self, state, passive):
863 criterion, param_keys = self._simple_lazy_clause
864
865 if state is None:
866 return sql_util.adapt_criterion_to_null(
867 criterion, [key for key, ident, value in param_keys]
868 )
869
870 mapper = self.parent_property.parent
871
872 o = state.obj() # strong ref
873 dict_ = attributes.instance_dict(o)
874
875 if passive & PassiveFlag.INIT_OK:
876 passive ^= PassiveFlag.INIT_OK
877
878 params = {}
879 for key, ident, value in param_keys:
880 if ident is not None:
881 if passive and passive & PassiveFlag.LOAD_AGAINST_COMMITTED:
882 value = mapper._get_committed_state_attr_by_column(
883 state, dict_, ident, passive
884 )
885 else:
886 value = mapper._get_state_attr_by_column(
887 state, dict_, ident, passive
888 )
889
890 params[key] = value
891
892 return criterion, params
893
894 def _invoke_raise_load(self, state, passive, lazy):
895 raise sa_exc.InvalidRequestError(
896 "'%s' is not available due to lazy='%s'" % (self, lazy)
897 )
898
899 def _load_for_state(
900 self,
901 state,
902 passive,
903 loadopt=None,
904 extra_criteria=(),
905 extra_options=(),
906 alternate_effective_path=None,
907 execution_options=util.EMPTY_DICT,
908 ):
909 if not state.key and (
910 (
911 not self.parent_property.load_on_pending
912 and not state._load_pending
913 )
914 or not state.session_id
915 ):
916 return LoaderCallableStatus.ATTR_EMPTY
917
918 pending = not state.key
919 primary_key_identity = None
920
921 use_get = self.use_get and (not loadopt or not loadopt._extra_criteria)
922
923 if (not passive & PassiveFlag.SQL_OK and not use_get) or (
924 not passive & attributes.NON_PERSISTENT_OK and pending
925 ):
926 return LoaderCallableStatus.PASSIVE_NO_RESULT
927
928 if (
929 # we were given lazy="raise"
930 self._raise_always
931 # the no_raise history-related flag was not passed
932 and not passive & PassiveFlag.NO_RAISE
933 and (
934 # if we are use_get and related_object_ok is disabled,
935 # which means we are at most looking in the identity map
936 # for history purposes or otherwise returning
937 # PASSIVE_NO_RESULT, don't raise. This is also a
938 # history-related flag
939 not use_get
940 or passive & PassiveFlag.RELATED_OBJECT_OK
941 )
942 ):
943 self._invoke_raise_load(state, passive, "raise")
944
945 session = _state_session(state)
946 if not session:
947 if passive & PassiveFlag.NO_RAISE:
948 return LoaderCallableStatus.PASSIVE_NO_RESULT
949
950 raise orm_exc.DetachedInstanceError(
951 "Parent instance %s is not bound to a Session; "
952 "lazy load operation of attribute '%s' cannot proceed"
953 % (orm_util.state_str(state), self.key)
954 )
955
956 # if we have a simple primary key load, check the
957 # identity map without generating a Query at all
958 if use_get:
959 primary_key_identity = self._get_ident_for_use_get(
960 session, state, passive
961 )
962 if LoaderCallableStatus.PASSIVE_NO_RESULT in primary_key_identity:
963 return LoaderCallableStatus.PASSIVE_NO_RESULT
964 elif LoaderCallableStatus.NEVER_SET in primary_key_identity:
965 return LoaderCallableStatus.NEVER_SET
966
967 # test for None alone in primary_key_identity based on
968 # allow_partial_pks preference. PASSIVE_NO_RESULT and NEVER_SET
969 # have already been tested above
970 if not self.mapper.allow_partial_pks:
971 if _none_only_set.intersection(primary_key_identity):
972 return None
973 else:
974 if _none_only_set.issuperset(primary_key_identity):
975 return None
976
977 if (
978 self.key in state.dict
979 and not passive & PassiveFlag.DEFERRED_HISTORY_LOAD
980 ):
981 return LoaderCallableStatus.ATTR_WAS_SET
982
983 # look for this identity in the identity map. Delegate to the
984 # Query class in use, as it may have special rules for how it
985 # does this, including how it decides what the correct
986 # identity_token would be for this identity.
987
988 instance = session._identity_lookup(
989 self.entity,
990 primary_key_identity,
991 passive=passive,
992 lazy_loaded_from=state,
993 )
994
995 if instance is not None:
996 if instance is LoaderCallableStatus.PASSIVE_CLASS_MISMATCH:
997 return None
998 else:
999 return instance
1000 elif (
1001 not passive & PassiveFlag.SQL_OK
1002 or not passive & PassiveFlag.RELATED_OBJECT_OK
1003 ):
1004 return LoaderCallableStatus.PASSIVE_NO_RESULT
1005
1006 return self._emit_lazyload(
1007 session,
1008 state,
1009 primary_key_identity,
1010 passive,
1011 loadopt,
1012 extra_criteria,
1013 extra_options,
1014 alternate_effective_path,
1015 execution_options,
1016 )
1017
1018 def _get_ident_for_use_get(self, session, state, passive):
1019 instance_mapper = state.manager.mapper
1020
1021 if passive & PassiveFlag.LOAD_AGAINST_COMMITTED:
1022 get_attr = instance_mapper._get_committed_state_attr_by_column
1023 else:
1024 get_attr = instance_mapper._get_state_attr_by_column
1025
1026 dict_ = state.dict
1027
1028 return [
1029 get_attr(state, dict_, self._equated_columns[pk], passive=passive)
1030 for pk in self.mapper.primary_key
1031 ]
1032
1033 @util.preload_module("sqlalchemy.orm.strategy_options")
1034 def _emit_lazyload(
1035 self,
1036 session,
1037 state,
1038 primary_key_identity,
1039 passive,
1040 loadopt,
1041 extra_criteria,
1042 extra_options,
1043 alternate_effective_path,
1044 execution_options,
1045 ):
1046 strategy_options = util.preloaded.orm_strategy_options
1047
1048 clauseelement = self.entity.__clause_element__()
1049 stmt = Select._create_raw_select(
1050 _raw_columns=[clauseelement],
1051 _propagate_attrs=clauseelement._propagate_attrs,
1052 _compile_options=_ORMCompileState.default_compile_options,
1053 )
1054 load_options = QueryContext.default_load_options
1055
1056 load_options += {
1057 "_invoke_all_eagers": False,
1058 "_lazy_loaded_from": state,
1059 }
1060
1061 if self.parent_property.secondary is not None:
1062 stmt = stmt.select_from(
1063 self.mapper, self.parent_property.secondary
1064 )
1065
1066 pending = not state.key
1067
1068 # don't autoflush on pending
1069 if pending or passive & attributes.NO_AUTOFLUSH:
1070 stmt._execution_options = util.immutabledict({"autoflush": False})
1071
1072 use_get = self.use_get
1073
1074 if state.load_options or (loadopt and loadopt._extra_criteria):
1075 if alternate_effective_path is None:
1076 effective_path = state.load_path[self.parent_property]
1077 else:
1078 effective_path = alternate_effective_path[self.parent_property]
1079
1080 opts = state.load_options
1081
1082 if loadopt and loadopt._extra_criteria:
1083 use_get = False
1084 opts += (
1085 orm_util.LoaderCriteriaOption(self.entity, extra_criteria),
1086 )
1087
1088 stmt._with_options = opts
1089 elif alternate_effective_path is None:
1090 # this path is used if there are not already any options
1091 # in the query, but an event may want to add them
1092 effective_path = state.mapper._path_registry[self.parent_property]
1093 else:
1094 # added by immediateloader
1095 effective_path = alternate_effective_path[self.parent_property]
1096
1097 if extra_options:
1098 stmt._with_options += extra_options
1099
1100 stmt._compile_options += {"_current_path": effective_path}
1101
1102 if use_get:
1103 if self._raise_on_sql and not passive & PassiveFlag.NO_RAISE:
1104 self._invoke_raise_load(state, passive, "raise_on_sql")
1105
1106 return loading._load_on_pk_identity(
1107 session,
1108 stmt,
1109 primary_key_identity,
1110 load_options=load_options,
1111 execution_options=execution_options,
1112 )
1113
1114 if self._order_by:
1115 stmt._order_by_clauses = self._order_by
1116
1117 def _lazyload_reverse(compile_context):
1118 for rev in self.parent_property._reverse_property:
1119 # reverse props that are MANYTOONE are loading *this*
1120 # object from get(), so don't need to eager out to those.
1121 if (
1122 rev.direction is interfaces.MANYTOONE
1123 and rev._use_get
1124 and not isinstance(rev.strategy, _LazyLoader)
1125 ):
1126 strategy_options.Load._construct_for_existing_path(
1127 compile_context.compile_options._current_path[
1128 rev.parent
1129 ]
1130 ).lazyload(rev).process_compile_state(compile_context)
1131
1132 stmt = stmt._add_compile_state_func(
1133 _lazyload_reverse, self.parent_property
1134 )
1135
1136 lazy_clause, params = self._generate_lazy_clause(state, passive)
1137
1138 if execution_options:
1139 execution_options = util.EMPTY_DICT.merge_with(
1140 execution_options, {"_sa_orm_load_options": load_options}
1141 )
1142 else:
1143 execution_options = {
1144 "_sa_orm_load_options": load_options,
1145 }
1146
1147 if (
1148 self.key in state.dict
1149 and not passive & PassiveFlag.DEFERRED_HISTORY_LOAD
1150 ):
1151 return LoaderCallableStatus.ATTR_WAS_SET
1152
1153 if pending:
1154 if util.has_intersection(orm_util._none_set, params.values()):
1155 return None
1156
1157 elif util.has_intersection(orm_util._never_set, params.values()):
1158 return None
1159
1160 if self._raise_on_sql and not passive & PassiveFlag.NO_RAISE:
1161 self._invoke_raise_load(state, passive, "raise_on_sql")
1162
1163 stmt._where_criteria = (lazy_clause,)
1164
1165 result = session.execute(
1166 stmt, params, execution_options=execution_options
1167 )
1168
1169 result = result.unique().scalars().all()
1170
1171 if self.uselist:
1172 return result
1173 else:
1174 l = len(result)
1175 if l:
1176 if l > 1:
1177 util.warn(
1178 "Multiple rows returned with "
1179 "uselist=False for lazily-loaded attribute '%s' "
1180 % self.parent_property
1181 )
1182
1183 return result[0]
1184 else:
1185 return None
1186
1187 def create_row_processor(
1188 self,
1189 context,
1190 query_entity,
1191 path,
1192 loadopt,
1193 mapper,
1194 result,
1195 adapter,
1196 populators,
1197 ):
1198 key = self.key
1199
1200 if (
1201 context.load_options._is_user_refresh
1202 and context.query._compile_options._only_load_props
1203 and self.key in context.query._compile_options._only_load_props
1204 ):
1205 return self._immediateload_create_row_processor(
1206 context,
1207 query_entity,
1208 path,
1209 loadopt,
1210 mapper,
1211 result,
1212 adapter,
1213 populators,
1214 )
1215
1216 if not self.is_class_level or (loadopt and loadopt._extra_criteria):
1217 # we are not the primary manager for this attribute
1218 # on this class - set up a
1219 # per-instance lazyloader, which will override the
1220 # class-level behavior.
1221 # this currently only happens when using a
1222 # "lazyload" option on a "no load"
1223 # attribute - "eager" attributes always have a
1224 # class-level lazyloader installed.
1225 set_lazy_callable = (
1226 InstanceState._instance_level_callable_processor
1227 )(
1228 mapper.class_manager,
1229 _LoadLazyAttribute(
1230 key,
1231 self,
1232 loadopt,
1233 (
1234 loadopt._generate_extra_criteria(context)
1235 if loadopt._extra_criteria
1236 else None
1237 ),
1238 ),
1239 key,
1240 )
1241
1242 populators["new"].append((self.key, set_lazy_callable))
1243 elif context.populate_existing or mapper.always_refresh:
1244
1245 def reset_for_lazy_callable(state, dict_, row):
1246 # we are the primary manager for this attribute on
1247 # this class - reset its
1248 # per-instance attribute state, so that the class-level
1249 # lazy loader is
1250 # executed when next referenced on this instance.
1251 # this is needed in
1252 # populate_existing() types of scenarios to reset
1253 # any existing state.
1254 state._reset(dict_, key)
1255
1256 populators["new"].append((self.key, reset_for_lazy_callable))
1257
1258
1259class _LoadLazyAttribute:
1260 """semi-serializable loader object used by LazyLoader
1261
1262 Historically, this object would be carried along with instances that
1263 needed to run lazyloaders, so it had to be serializable to support
1264 cached instances.
1265
1266 this is no longer a general requirement, and the case where this object
1267 is used is exactly the case where we can't really serialize easily,
1268 which is when extra criteria in the loader option is present.
1269
1270 We can't reliably serialize that as it refers to mapped entities and
1271 AliasedClass objects that are local to the current process, which would
1272 need to be matched up on deserialize e.g. the sqlalchemy.ext.serializer
1273 approach.
1274
1275 """
1276
1277 def __init__(self, key, initiating_strategy, loadopt, extra_criteria):
1278 self.key = key
1279 self.strategy_key = initiating_strategy.strategy_key
1280 self.loadopt = loadopt
1281 self.extra_criteria = extra_criteria
1282
1283 def __getstate__(self):
1284 if self.extra_criteria is not None:
1285 util.warn(
1286 "Can't reliably serialize a lazyload() option that "
1287 "contains additional criteria; please use eager loading "
1288 "for this case"
1289 )
1290 return {
1291 "key": self.key,
1292 "strategy_key": self.strategy_key,
1293 "loadopt": self.loadopt,
1294 "extra_criteria": (),
1295 }
1296
1297 def __call__(self, state, passive=attributes.PASSIVE_OFF):
1298 key = self.key
1299 instance_mapper = state.manager.mapper
1300 prop = instance_mapper._props[key]
1301 strategy = prop._strategies[self.strategy_key]
1302
1303 return strategy._load_for_state(
1304 state,
1305 passive,
1306 loadopt=self.loadopt,
1307 extra_criteria=self.extra_criteria,
1308 )
1309
1310
1311class _PostLoader(_AbstractRelationshipLoader):
1312 """A relationship loader that emits a second SELECT statement."""
1313
1314 __slots__ = ()
1315
1316 def _setup_for_recursion(self, context, path, loadopt, join_depth=None):
1317 effective_path = (
1318 context.compile_state.current_path or orm_util.PathRegistry.root
1319 ) + path
1320
1321 top_level_context = context._get_top_level_context()
1322 execution_options = util.immutabledict(
1323 {"sa_top_level_orm_context": top_level_context}
1324 )
1325
1326 if loadopt:
1327 recursion_depth = loadopt.local_opts.get("recursion_depth", None)
1328 unlimited_recursion = recursion_depth == -1
1329 else:
1330 recursion_depth = None
1331 unlimited_recursion = False
1332
1333 if recursion_depth is not None:
1334 if not self.parent_property._is_self_referential:
1335 raise sa_exc.InvalidRequestError(
1336 f"recursion_depth option on relationship "
1337 f"{self.parent_property} not valid for "
1338 "non-self-referential relationship"
1339 )
1340 recursion_depth = context.execution_options.get(
1341 f"_recursion_depth_{id(self)}", recursion_depth
1342 )
1343
1344 if not unlimited_recursion and recursion_depth < 0:
1345 return (
1346 effective_path,
1347 False,
1348 execution_options,
1349 recursion_depth,
1350 )
1351
1352 if not unlimited_recursion:
1353 execution_options = execution_options.union(
1354 {
1355 f"_recursion_depth_{id(self)}": recursion_depth - 1,
1356 }
1357 )
1358
1359 if loading._PostLoad.path_exists(
1360 context, effective_path, self.parent_property
1361 ):
1362 return effective_path, False, execution_options, recursion_depth
1363
1364 path_w_prop = path[self.parent_property]
1365 effective_path_w_prop = effective_path[self.parent_property]
1366
1367 if not path_w_prop.contains(context.attributes, "loader"):
1368 if join_depth:
1369 if effective_path_w_prop.length / 2 > join_depth:
1370 return (
1371 effective_path,
1372 False,
1373 execution_options,
1374 recursion_depth,
1375 )
1376 elif effective_path_w_prop.contains_mapper(self.mapper):
1377 return (
1378 effective_path,
1379 False,
1380 execution_options,
1381 recursion_depth,
1382 )
1383
1384 return effective_path, True, execution_options, recursion_depth
1385
1386
1387@relationships.RelationshipProperty.strategy_for(lazy="immediate")
1388class _ImmediateLoader(_PostLoader):
1389 __slots__ = ("join_depth",)
1390
1391 def __init__(self, parent, strategy_key):
1392 super().__init__(parent, strategy_key)
1393 self.join_depth = self.parent_property.join_depth
1394
1395 def init_class_attribute(self, mapper):
1396 self.parent_property._get_strategy(
1397 (("lazy", "select"),)
1398 ).init_class_attribute(mapper)
1399
1400 def create_row_processor(
1401 self,
1402 context,
1403 query_entity,
1404 path,
1405 loadopt,
1406 mapper,
1407 result,
1408 adapter,
1409 populators,
1410 ):
1411 if not context.compile_state.compile_options._enable_eagerloads:
1412 return
1413
1414 (
1415 effective_path,
1416 run_loader,
1417 execution_options,
1418 recursion_depth,
1419 ) = self._setup_for_recursion(context, path, loadopt, self.join_depth)
1420
1421 if not run_loader:
1422 # this will not emit SQL and will only emit for a many-to-one
1423 # "use get" load. the "_RELATED" part means it may return
1424 # instance even if its expired, since this is a mutually-recursive
1425 # load operation.
1426 flags = attributes.PASSIVE_NO_FETCH_RELATED | PassiveFlag.NO_RAISE
1427 else:
1428 flags = attributes.PASSIVE_OFF | PassiveFlag.NO_RAISE
1429
1430 loading._PostLoad.callable_for_path(
1431 context,
1432 effective_path,
1433 self.parent,
1434 self.parent_property,
1435 self._load_for_path,
1436 loadopt,
1437 flags,
1438 recursion_depth,
1439 execution_options,
1440 )
1441
1442 def _load_for_path(
1443 self,
1444 context,
1445 path,
1446 states,
1447 load_only,
1448 loadopt,
1449 flags,
1450 recursion_depth,
1451 execution_options,
1452 ):
1453 if recursion_depth:
1454 new_opt = Load(loadopt.path.entity)
1455 new_opt.context = (
1456 loadopt,
1457 loadopt._recurse(),
1458 )
1459 alternate_effective_path = path._truncate_recursive()
1460 extra_options = (new_opt,)
1461 else:
1462 alternate_effective_path = path
1463 extra_options = ()
1464
1465 key = self.key
1466 lazyloader = self.parent_property._get_strategy((("lazy", "select"),))
1467 for state, overwrite in states:
1468 dict_ = state.dict
1469
1470 if overwrite or key not in dict_:
1471 value = lazyloader._load_for_state(
1472 state,
1473 flags,
1474 extra_options=extra_options,
1475 alternate_effective_path=alternate_effective_path,
1476 execution_options=execution_options,
1477 )
1478 if value not in (
1479 ATTR_WAS_SET,
1480 LoaderCallableStatus.PASSIVE_NO_RESULT,
1481 ):
1482 state.get_impl(key).set_committed_value(
1483 state, dict_, value
1484 )
1485
1486
1487@log.class_logger
1488@relationships.RelationshipProperty.strategy_for(lazy="subquery")
1489class _SubqueryLoader(_PostLoader):
1490 __slots__ = ("join_depth",)
1491
1492 def __init__(self, parent, strategy_key):
1493 super().__init__(parent, strategy_key)
1494 self.join_depth = self.parent_property.join_depth
1495
1496 def init_class_attribute(self, mapper):
1497 self.parent_property._get_strategy(
1498 (("lazy", "select"),)
1499 ).init_class_attribute(mapper)
1500
1501 def _get_leftmost(
1502 self,
1503 orig_query_entity_index,
1504 subq_path,
1505 current_compile_state,
1506 is_root,
1507 ):
1508 given_subq_path = subq_path
1509 subq_path = subq_path.path
1510 subq_mapper = orm_util._class_to_mapper(subq_path[0])
1511
1512 # determine attributes of the leftmost mapper
1513 if (
1514 self.parent.isa(subq_mapper)
1515 and self.parent_property is subq_path[1]
1516 ):
1517 leftmost_mapper, leftmost_prop = self.parent, self.parent_property
1518 else:
1519 leftmost_mapper, leftmost_prop = subq_mapper, subq_path[1]
1520
1521 if is_root:
1522 # the subq_path is also coming from cached state, so when we start
1523 # building up this path, it has to also be converted to be in terms
1524 # of the current state. this is for the specific case of the entity
1525 # is an AliasedClass against a subquery that's not otherwise going
1526 # to adapt
1527 new_subq_path = current_compile_state._entities[
1528 orig_query_entity_index
1529 ].entity_zero._path_registry[leftmost_prop]
1530 additional = len(subq_path) - len(new_subq_path)
1531 if additional:
1532 new_subq_path += path_registry.PathRegistry.coerce(
1533 subq_path[-additional:]
1534 )
1535 else:
1536 new_subq_path = given_subq_path
1537
1538 leftmost_cols = leftmost_prop.local_columns
1539
1540 leftmost_attr = [
1541 getattr(
1542 new_subq_path.path[0].entity,
1543 leftmost_mapper._columntoproperty[c].key,
1544 )
1545 for c in leftmost_cols
1546 ]
1547
1548 return leftmost_mapper, leftmost_attr, leftmost_prop, new_subq_path
1549
1550 def _generate_from_original_query(
1551 self,
1552 orig_compile_state,
1553 orig_query,
1554 leftmost_mapper,
1555 leftmost_attr,
1556 leftmost_relationship,
1557 orig_entity,
1558 ):
1559 # reformat the original query
1560 # to look only for significant columns
1561 q = orig_query._clone().correlate(None)
1562
1563 # LEGACY: make a Query back from the select() !!
1564 # This suits at least two legacy cases:
1565 # 1. applications which expect before_compile() to be called
1566 # below when we run .subquery() on this query (Keystone)
1567 # 2. applications which are doing subqueryload with complex
1568 # from_self() queries, as query.subquery() / .statement
1569 # has to do the full compile context for multiply-nested
1570 # from_self() (Neutron) - see test_subqload_from_self
1571 # for demo.
1572 q2 = query.Query.__new__(query.Query)
1573 q2.__dict__.update(q.__dict__)
1574 q = q2
1575
1576 # set the query's "FROM" list explicitly to what the
1577 # FROM list would be in any case, as we will be limiting
1578 # the columns in the SELECT list which may no longer include
1579 # all entities mentioned in things like WHERE, JOIN, etc.
1580 if not q._from_obj:
1581 q._enable_assertions = False
1582 q.select_from.non_generative(
1583 q,
1584 *{
1585 ent["entity"]
1586 for ent in _column_descriptions(
1587 orig_query, compile_state=orig_compile_state
1588 )
1589 if ent["entity"] is not None
1590 },
1591 )
1592
1593 # select from the identity columns of the outer (specifically, these
1594 # are the 'local_cols' of the property). This will remove other
1595 # columns from the query that might suggest the right entity which is
1596 # why we do set select_from above. The attributes we have are
1597 # coerced and adapted using the original query's adapter, which is
1598 # needed only for the case of adapting a subclass column to
1599 # that of a polymorphic selectable, e.g. we have
1600 # Engineer.primary_language and the entity is Person. All other
1601 # adaptations, e.g. from_self, select_entity_from(), will occur
1602 # within the new query when it compiles, as the compile_state we are
1603 # using here is only a partial one. If the subqueryload is from a
1604 # with_polymorphic() or other aliased() object, left_attr will already
1605 # be the correct attributes so no adaptation is needed.
1606 target_cols = orig_compile_state._adapt_col_list(
1607 [
1608 sql.coercions.expect(sql.roles.ColumnsClauseRole, o)
1609 for o in leftmost_attr
1610 ],
1611 orig_compile_state._get_current_adapter(),
1612 )
1613 q._raw_columns = target_cols
1614
1615 distinct_target_key = leftmost_relationship.distinct_target_key
1616
1617 if distinct_target_key is True:
1618 q._distinct = True
1619 elif distinct_target_key is None:
1620 # if target_cols refer to a non-primary key or only
1621 # part of a composite primary key, set the q as distinct
1622 for t in {c.table for c in target_cols}:
1623 if not set(target_cols).issuperset(t.primary_key):
1624 q._distinct = True
1625 break
1626
1627 # don't need ORDER BY if no limit/offset
1628 if not q._has_row_limiting_clause:
1629 q._order_by_clauses = ()
1630
1631 if q._distinct is True and q._order_by_clauses:
1632 # the logic to automatically add the order by columns to the query
1633 # when distinct is True is deprecated in the query
1634 to_add = sql_util.expand_column_list_from_order_by(
1635 target_cols, q._order_by_clauses
1636 )
1637 if to_add:
1638 q._set_entities(target_cols + to_add)
1639
1640 # the original query now becomes a subquery
1641 # which we'll join onto.
1642 # LEGACY: as "q" is a Query, the before_compile() event is invoked
1643 # here.
1644 embed_q = q.set_label_style(LABEL_STYLE_TABLENAME_PLUS_COL).subquery()
1645 left_alias = orm_util.AliasedClass(
1646 leftmost_mapper, embed_q, use_mapper_path=True
1647 )
1648 return left_alias
1649
1650 def _prep_for_joins(self, left_alias, subq_path):
1651 # figure out what's being joined. a.k.a. the fun part
1652 to_join = []
1653 pairs = list(subq_path.pairs())
1654
1655 for i, (mapper, prop) in enumerate(pairs):
1656 if i > 0:
1657 # look at the previous mapper in the chain -
1658 # if it is as or more specific than this prop's
1659 # mapper, use that instead.
1660 # note we have an assumption here that
1661 # the non-first element is always going to be a mapper,
1662 # not an AliasedClass
1663
1664 prev_mapper = pairs[i - 1][1].mapper
1665 to_append = prev_mapper if prev_mapper.isa(mapper) else mapper
1666 else:
1667 to_append = mapper
1668
1669 to_join.append((to_append, prop.key))
1670
1671 # determine the immediate parent class we are joining from,
1672 # which needs to be aliased.
1673
1674 if len(to_join) < 2:
1675 # in the case of a one level eager load, this is the
1676 # leftmost "left_alias".
1677 parent_alias = left_alias
1678 else:
1679 info = inspect(to_join[-1][0])
1680 if info.is_aliased_class:
1681 parent_alias = info.entity
1682 else:
1683 # alias a plain mapper as we may be
1684 # joining multiple times
1685 parent_alias = orm_util.AliasedClass(
1686 info.entity, use_mapper_path=True
1687 )
1688
1689 local_cols = self.parent_property.local_columns
1690
1691 local_attr = [
1692 getattr(parent_alias, self.parent._columntoproperty[c].key)
1693 for c in local_cols
1694 ]
1695 return to_join, local_attr, parent_alias
1696
1697 def _apply_joins(
1698 self, q, to_join, left_alias, parent_alias, effective_entity
1699 ):
1700 ltj = len(to_join)
1701 if ltj == 1:
1702 to_join = [
1703 getattr(left_alias, to_join[0][1]).of_type(effective_entity)
1704 ]
1705 elif ltj == 2:
1706 to_join = [
1707 getattr(left_alias, to_join[0][1]).of_type(parent_alias),
1708 getattr(parent_alias, to_join[-1][1]).of_type(
1709 effective_entity
1710 ),
1711 ]
1712 elif ltj > 2:
1713 middle = [
1714 (
1715 (
1716 orm_util.AliasedClass(item[0])
1717 if not inspect(item[0]).is_aliased_class
1718 else item[0].entity
1719 ),
1720 item[1],
1721 )
1722 for item in to_join[1:-1]
1723 ]
1724 inner = []
1725
1726 while middle:
1727 item = middle.pop(0)
1728 attr = getattr(item[0], item[1])
1729 if middle:
1730 attr = attr.of_type(middle[0][0])
1731 else:
1732 attr = attr.of_type(parent_alias)
1733
1734 inner.append(attr)
1735
1736 to_join = (
1737 [getattr(left_alias, to_join[0][1]).of_type(inner[0].parent)]
1738 + inner
1739 + [
1740 getattr(parent_alias, to_join[-1][1]).of_type(
1741 effective_entity
1742 )
1743 ]
1744 )
1745
1746 for attr in to_join:
1747 q = q.join(attr)
1748
1749 return q
1750
1751 def _setup_options(
1752 self,
1753 context,
1754 q,
1755 subq_path,
1756 rewritten_path,
1757 orig_query,
1758 effective_entity,
1759 loadopt,
1760 ):
1761 # note that because the subqueryload object
1762 # does not reuse the cached query, instead always making
1763 # use of the current invoked query, while we have two queries
1764 # here (orig and context.query), they are both non-cached
1765 # queries and we can transfer the options as is without
1766 # adjusting for new criteria. Some work on #6881 / #6889
1767 # brought this into question.
1768 new_options = orig_query._with_options
1769
1770 if loadopt and loadopt._extra_criteria:
1771 new_options += (
1772 orm_util.LoaderCriteriaOption(
1773 effective_entity,
1774 loadopt._generate_extra_criteria(context),
1775 ),
1776 )
1777
1778 # propagate loader options etc. to the new query.
1779 # these will fire relative to subq_path.
1780 q = q._with_current_path(rewritten_path)
1781 q = q.options(*new_options)
1782
1783 return q
1784
1785 def _setup_outermost_orderby(self, q):
1786 if self.parent_property.order_by:
1787
1788 def _setup_outermost_orderby(compile_context):
1789 compile_context.eager_order_by += tuple(
1790 util.to_list(self.parent_property.order_by)
1791 )
1792
1793 q = q._add_compile_state_func(
1794 _setup_outermost_orderby, self.parent_property
1795 )
1796
1797 return q
1798
1799 class _SubqCollections:
1800 """Given a :class:`_query.Query` used to emit the "subquery load",
1801 provide a load interface that executes the query at the
1802 first moment a value is needed.
1803
1804 """
1805
1806 __slots__ = (
1807 "session",
1808 "execution_options",
1809 "load_options",
1810 "params",
1811 "subq",
1812 "_data",
1813 )
1814
1815 def __init__(self, context, subq):
1816 # avoid creating a cycle by storing context
1817 # even though that's preferable
1818 self.session = context.session
1819 self.execution_options = context.execution_options
1820 self.load_options = context.load_options
1821 self.params = context.params or {}
1822 self.subq = subq
1823 self._data = None
1824
1825 def get(self, key, default):
1826 if self._data is None:
1827 self._load()
1828 return self._data.get(key, default)
1829
1830 def _load(self):
1831 self._data = collections.defaultdict(list)
1832
1833 q = self.subq
1834 assert q.session is None
1835
1836 q = q.with_session(self.session)
1837
1838 if self.load_options._populate_existing:
1839 q = q.populate_existing()
1840 # to work with baked query, the parameters may have been
1841 # updated since this query was created, so take these into account
1842
1843 data = self._data
1844 for row in q.params(self.params):
1845 # group plain tuples rather than Row slices, which would
1846 # incur Row construction and Row.__eq__ per row
1847 tup = row._to_tuple_instance()
1848 data[tup[1:]].append(tup[0])
1849
1850 def loader(self, state, dict_, row):
1851 if self._data is None:
1852 self._load()
1853
1854 def _setup_query_from_rowproc(
1855 self,
1856 context,
1857 query_entity,
1858 path,
1859 entity,
1860 loadopt,
1861 adapter,
1862 ):
1863 compile_state = context.compile_state
1864 if (
1865 not compile_state.compile_options._enable_eagerloads
1866 or compile_state.compile_options._for_refresh_state
1867 ):
1868 return
1869
1870 orig_query_entity_index = compile_state._entities.index(query_entity)
1871 context.loaders_require_buffering = True
1872
1873 path = path[self.parent_property]
1874
1875 # build up a path indicating the path from the leftmost
1876 # entity to the thing we're subquery loading.
1877 with_poly_entity = path.get(
1878 compile_state.attributes, "path_with_polymorphic", None
1879 )
1880 if with_poly_entity is not None:
1881 effective_entity = with_poly_entity
1882 else:
1883 effective_entity = self.entity
1884
1885 subq_path, rewritten_path = context.query._execution_options.get(
1886 ("subquery_paths", None),
1887 (orm_util.PathRegistry.root, orm_util.PathRegistry.root),
1888 )
1889 is_root = subq_path is orm_util.PathRegistry.root
1890 subq_path = subq_path + path
1891 rewritten_path = rewritten_path + path
1892
1893 # use the current query being invoked, not the compile state
1894 # one. this is so that we get the current parameters. however,
1895 # it means we can't use the existing compile state, we have to make
1896 # a new one. other approaches include possibly using the
1897 # compiled query but swapping the params, seems only marginally
1898 # less time spent but more complicated
1899 orig_query = context.query._execution_options.get(
1900 ("orig_query", _SubqueryLoader), context.query
1901 )
1902
1903 # make a new compile_state for the query that's probably cached, but
1904 # we're sort of undoing a bit of that caching :(
1905 compile_state_cls = _ORMCompileState._get_plugin_class_for_plugin(
1906 orig_query, "orm"
1907 )
1908
1909 if orig_query._is_lambda_element:
1910 if context.load_options._lazy_loaded_from is None:
1911 util.warn(
1912 'subqueryloader for "%s" must invoke lambda callable '
1913 "at %r in "
1914 "order to produce a new query, decreasing the efficiency "
1915 "of caching for this statement. Consider using "
1916 "selectinload() for more effective full-lambda caching"
1917 % (self, orig_query)
1918 )
1919 orig_query = orig_query._resolved
1920
1921 # this is the more "quick" version, however it's not clear how
1922 # much of this we need. in particular I can't get a test to
1923 # fail if the "set_base_alias" is missing and not sure why that is.
1924 orig_compile_state = compile_state_cls._create_entities_collection(
1925 orig_query, legacy=False
1926 )
1927
1928 (
1929 leftmost_mapper,
1930 leftmost_attr,
1931 leftmost_relationship,
1932 rewritten_path,
1933 ) = self._get_leftmost(
1934 orig_query_entity_index,
1935 rewritten_path,
1936 orig_compile_state,
1937 is_root,
1938 )
1939
1940 # generate a new Query from the original, then
1941 # produce a subquery from it.
1942 left_alias = self._generate_from_original_query(
1943 orig_compile_state,
1944 orig_query,
1945 leftmost_mapper,
1946 leftmost_attr,
1947 leftmost_relationship,
1948 entity,
1949 )
1950
1951 # generate another Query that will join the
1952 # left alias to the target relationships.
1953 # basically doing a longhand
1954 # "from_self()". (from_self() itself not quite industrial
1955 # strength enough for all contingencies...but very close)
1956
1957 q = query.Query(effective_entity)
1958
1959 q._execution_options = context.query._execution_options.merge_with(
1960 context.execution_options,
1961 {
1962 ("orig_query", _SubqueryLoader): orig_query,
1963 ("subquery_paths", None): (subq_path, rewritten_path),
1964 },
1965 )
1966
1967 q = q._set_enable_single_crit(False)
1968 to_join, local_attr, parent_alias = self._prep_for_joins(
1969 left_alias, subq_path
1970 )
1971
1972 q = q.add_columns(*local_attr)
1973 q = self._apply_joins(
1974 q, to_join, left_alias, parent_alias, effective_entity
1975 )
1976
1977 q = self._setup_options(
1978 context,
1979 q,
1980 subq_path,
1981 rewritten_path,
1982 orig_query,
1983 effective_entity,
1984 loadopt,
1985 )
1986 q = self._setup_outermost_orderby(q)
1987
1988 return q
1989
1990 def create_row_processor(
1991 self,
1992 context,
1993 query_entity,
1994 path,
1995 loadopt,
1996 mapper,
1997 result,
1998 adapter,
1999 populators,
2000 ):
2001 if (
2002 loadopt
2003 and context.compile_state.statement is not None
2004 and context.compile_state.statement.is_dml
2005 ):
2006 util.warn_deprecated(
2007 "The subqueryload loader option is not compatible with DML "
2008 "statements such as INSERT, UPDATE. Only SELECT may be used."
2009 "This warning will become an exception in a future release.",
2010 "2.0",
2011 )
2012
2013 if context.refresh_state:
2014 return self._immediateload_create_row_processor(
2015 context,
2016 query_entity,
2017 path,
2018 loadopt,
2019 mapper,
2020 result,
2021 adapter,
2022 populators,
2023 )
2024
2025 _, run_loader, _, _ = self._setup_for_recursion(
2026 context, path, loadopt, self.join_depth
2027 )
2028 if not run_loader:
2029 return
2030
2031 if not isinstance(context.compile_state, _ORMSelectCompileState):
2032 # issue 7505 - subqueryload() in 1.3 and previous would silently
2033 # degrade for from_statement() without warning. this behavior
2034 # is restored here
2035 return
2036
2037 if not self.parent.class_manager[self.key].impl.supports_population:
2038 raise sa_exc.InvalidRequestError(
2039 "'%s' does not support object "
2040 "population - eager loading cannot be applied." % self
2041 )
2042
2043 # a little dance here as the "path" is still something that only
2044 # semi-tracks the exact series of things we are loading, still not
2045 # telling us about with_polymorphic() and stuff like that when it's at
2046 # the root.. the initial MapperEntity is more accurate for this case.
2047 if len(path) == 1:
2048 if not orm_util._entity_isa(query_entity.entity_zero, self.parent):
2049 return
2050 elif not orm_util._entity_isa(
2051 path[-1], self.parent
2052 ) and not self.parent.isa(path[-1].mapper):
2053 # second check accommodates a polymorphic entity where
2054 # the path has been normalized to the base mapper but
2055 # self.parent is a subclass mapper. Fixes #13209.
2056 return
2057
2058 subq = self._setup_query_from_rowproc(
2059 context,
2060 query_entity,
2061 path,
2062 path[-1],
2063 loadopt,
2064 adapter,
2065 )
2066
2067 if subq is None:
2068 return
2069
2070 assert subq.session is None
2071
2072 path = path[self.parent_property]
2073
2074 local_cols = self.parent_property.local_columns
2075
2076 # cache the loaded collections in the context
2077 # so that inheriting mappers don't re-load when they
2078 # call upon create_row_processor again
2079 collections = path.get(context.attributes, "collections")
2080 if collections is None:
2081 collections = self._SubqCollections(context, subq)
2082 path.set(context.attributes, "collections", collections)
2083
2084 if adapter:
2085 local_cols = [adapter.columns[c] for c in local_cols]
2086
2087 if self.uselist:
2088 self._create_collection_loader(
2089 context, result, collections, local_cols, populators
2090 )
2091 else:
2092 self._create_scalar_loader(
2093 context, result, collections, local_cols, populators
2094 )
2095
2096 def _create_collection_loader(
2097 self, context, result, collections, local_cols, populators
2098 ):
2099 tuple_getter = result._tuple_getter(local_cols)
2100
2101 def load_collection_from_subq(state, dict_, row):
2102 collection = collections.get(tuple_getter(row), ())
2103 state.get_impl(self.key).set_committed_value(
2104 state, dict_, collection
2105 )
2106
2107 def load_collection_from_subq_existing_row(state, dict_, row):
2108 if self.key not in dict_:
2109 load_collection_from_subq(state, dict_, row)
2110
2111 populators["new"].append((self.key, load_collection_from_subq))
2112 populators["existing"].append(
2113 (self.key, load_collection_from_subq_existing_row)
2114 )
2115
2116 if context.invoke_all_eagers:
2117 populators["eager"].append((self.key, collections.loader))
2118
2119 def _create_scalar_loader(
2120 self, context, result, collections, local_cols, populators
2121 ):
2122 tuple_getter = result._tuple_getter(local_cols)
2123
2124 def load_scalar_from_subq(state, dict_, row):
2125 collection = collections.get(tuple_getter(row), (None,))
2126 if len(collection) > 1:
2127 util.warn(
2128 "Multiple rows returned with "
2129 "uselist=False for eagerly-loaded attribute '%s' " % self
2130 )
2131
2132 scalar = collection[0]
2133 state.get_impl(self.key).set_committed_value(state, dict_, scalar)
2134
2135 def load_scalar_from_subq_existing_row(state, dict_, row):
2136 if self.key not in dict_:
2137 load_scalar_from_subq(state, dict_, row)
2138
2139 populators["new"].append((self.key, load_scalar_from_subq))
2140 populators["existing"].append(
2141 (self.key, load_scalar_from_subq_existing_row)
2142 )
2143 if context.invoke_all_eagers:
2144 populators["eager"].append((self.key, collections.loader))
2145
2146
2147@log.class_logger
2148@relationships.RelationshipProperty.strategy_for(lazy="joined")
2149@relationships.RelationshipProperty.strategy_for(lazy=False)
2150class _JoinedLoader(_AbstractRelationshipLoader):
2151 """Provide loading behavior for a :class:`.Relationship`
2152 using joined eager loading.
2153
2154 """
2155
2156 __slots__ = "join_depth"
2157
2158 def __init__(self, parent, strategy_key):
2159 super().__init__(parent, strategy_key)
2160 self.join_depth = self.parent_property.join_depth
2161
2162 def init_class_attribute(self, mapper):
2163 self.parent_property._get_strategy(
2164 (("lazy", "select"),)
2165 ).init_class_attribute(mapper)
2166
2167 def setup_query(
2168 self,
2169 compile_state,
2170 query_entity,
2171 path,
2172 loadopt,
2173 adapter,
2174 column_collection=None,
2175 parentmapper=None,
2176 chained_from_outerjoin=False,
2177 **kwargs,
2178 ):
2179 """Add a left outer join to the statement that's being constructed."""
2180
2181 if not compile_state.compile_options._enable_eagerloads:
2182 return
2183 elif (
2184 loadopt
2185 and compile_state.statement is not None
2186 and compile_state.statement.is_dml
2187 ):
2188 util.warn_deprecated(
2189 "The joinedload loader option is not compatible with DML "
2190 "statements such as INSERT, UPDATE. Only SELECT may be used."
2191 "This warning will become an exception in a future release.",
2192 "2.0",
2193 )
2194 elif self.uselist:
2195 compile_state.multi_row_eager_loaders = True
2196
2197 path = path[self.parent_property]
2198
2199 user_defined_adapter = (
2200 self._init_user_defined_eager_proc(
2201 loadopt, compile_state, compile_state.attributes
2202 )
2203 if loadopt
2204 else False
2205 )
2206
2207 if user_defined_adapter is not False:
2208 # setup an adapter but dont create any JOIN, assume it's already
2209 # in the query
2210 (
2211 clauses,
2212 adapter,
2213 add_to_collection,
2214 ) = self._setup_query_on_user_defined_adapter(
2215 compile_state,
2216 query_entity,
2217 path,
2218 adapter,
2219 user_defined_adapter,
2220 )
2221
2222 # don't do "wrap" for multi-row, we want to wrap
2223 # limited/distinct SELECT,
2224 # because we want to put the JOIN on the outside.
2225
2226 else:
2227 # if not via query option, check for
2228 # a cycle
2229 if not path.contains(compile_state.attributes, "loader"):
2230 if self.join_depth:
2231 if path.length / 2 > self.join_depth:
2232 return
2233 elif path.contains_mapper(self.mapper):
2234 return
2235
2236 # add the JOIN and create an adapter
2237 (
2238 clauses,
2239 adapter,
2240 add_to_collection,
2241 chained_from_outerjoin,
2242 ) = self._generate_row_adapter(
2243 compile_state,
2244 query_entity,
2245 path,
2246 loadopt,
2247 adapter,
2248 column_collection,
2249 parentmapper,
2250 chained_from_outerjoin,
2251 )
2252
2253 # for multi-row, we want to wrap limited/distinct SELECT,
2254 # because we want to put the JOIN on the outside.
2255 compile_state.eager_adding_joins = True
2256
2257 with_poly_entity = path.get(
2258 compile_state.attributes, "path_with_polymorphic", None
2259 )
2260 if with_poly_entity is not None:
2261 with_polymorphic = inspect(
2262 with_poly_entity
2263 ).with_polymorphic_mappers
2264 else:
2265 with_polymorphic = None
2266
2267 path = path[self.entity]
2268
2269 loading._setup_entity_query(
2270 compile_state,
2271 self.mapper,
2272 query_entity,
2273 path,
2274 clauses,
2275 add_to_collection,
2276 with_polymorphic=with_polymorphic,
2277 parentmapper=self.mapper,
2278 chained_from_outerjoin=chained_from_outerjoin,
2279 )
2280
2281 has_nones = util.NONE_SET.intersection(compile_state.secondary_columns)
2282
2283 if has_nones:
2284 if with_poly_entity is not None:
2285 raise sa_exc.InvalidRequestError(
2286 "Detected unaliased columns when generating joined "
2287 "load. Make sure to use aliased=True or flat=True "
2288 "when using joined loading with with_polymorphic()."
2289 )
2290 else:
2291 compile_state.secondary_columns = [
2292 c for c in compile_state.secondary_columns if c is not None
2293 ]
2294
2295 def _init_user_defined_eager_proc(
2296 self, loadopt, compile_state, target_attributes
2297 ):
2298 # check if the opt applies at all
2299 if "eager_from_alias" not in loadopt.local_opts:
2300 # nope
2301 return False
2302
2303 path = loadopt.path.parent
2304
2305 # the option applies. check if the "user_defined_eager_row_processor"
2306 # has been built up.
2307 adapter = path.get(
2308 compile_state.attributes, "user_defined_eager_row_processor", False
2309 )
2310 if adapter is not False:
2311 # just return it
2312 return adapter
2313
2314 # otherwise figure it out.
2315 alias = loadopt.local_opts["eager_from_alias"]
2316 root_mapper, prop = path[-2:]
2317
2318 if alias is not None:
2319 if isinstance(alias, str):
2320 alias = prop.target.alias(alias)
2321 adapter = orm_util.ORMAdapter(
2322 orm_util._TraceAdaptRole.JOINEDLOAD_USER_DEFINED_ALIAS,
2323 prop.mapper,
2324 selectable=alias,
2325 equivalents=prop.mapper._equivalent_columns,
2326 limit_on_entity=False,
2327 )
2328 else:
2329 if path.contains(
2330 compile_state.attributes, "path_with_polymorphic"
2331 ):
2332 with_poly_entity = path.get(
2333 compile_state.attributes, "path_with_polymorphic"
2334 )
2335 adapter = orm_util.ORMAdapter(
2336 orm_util._TraceAdaptRole.JOINEDLOAD_PATH_WITH_POLYMORPHIC,
2337 with_poly_entity,
2338 equivalents=prop.mapper._equivalent_columns,
2339 )
2340 else:
2341 adapter = compile_state._polymorphic_adapters.get(
2342 prop.mapper, None
2343 )
2344 path.set(
2345 target_attributes,
2346 "user_defined_eager_row_processor",
2347 adapter,
2348 )
2349
2350 return adapter
2351
2352 def _setup_query_on_user_defined_adapter(
2353 self, context, entity, path, adapter, user_defined_adapter
2354 ):
2355 # apply some more wrapping to the "user defined adapter"
2356 # if we are setting up the query for SQL render.
2357 adapter = entity._get_entity_clauses(context)
2358
2359 if adapter and user_defined_adapter:
2360 user_defined_adapter = user_defined_adapter.wrap(adapter)
2361 path.set(
2362 context.attributes,
2363 "user_defined_eager_row_processor",
2364 user_defined_adapter,
2365 )
2366 elif adapter:
2367 user_defined_adapter = adapter
2368 path.set(
2369 context.attributes,
2370 "user_defined_eager_row_processor",
2371 user_defined_adapter,
2372 )
2373
2374 add_to_collection = context.primary_columns
2375 return user_defined_adapter, adapter, add_to_collection
2376
2377 def _generate_row_adapter(
2378 self,
2379 compile_state,
2380 entity,
2381 path,
2382 loadopt,
2383 adapter,
2384 column_collection,
2385 parentmapper,
2386 chained_from_outerjoin,
2387 ):
2388 with_poly_entity = path.get(
2389 compile_state.attributes, "path_with_polymorphic", None
2390 )
2391 if with_poly_entity:
2392 to_adapt = with_poly_entity
2393 else:
2394 insp = inspect(self.entity)
2395 if insp.is_aliased_class:
2396 alt_selectable = insp.selectable
2397 else:
2398 alt_selectable = None
2399
2400 to_adapt = orm_util.AliasedClass(
2401 self.mapper,
2402 alias=(
2403 alt_selectable._anonymous_fromclause(flat=True)
2404 if alt_selectable is not None
2405 else None
2406 ),
2407 flat=True,
2408 use_mapper_path=True,
2409 )
2410
2411 to_adapt_insp = inspect(to_adapt)
2412
2413 clauses = to_adapt_insp._memo(
2414 ("joinedloader_ormadapter", self),
2415 orm_util.ORMAdapter,
2416 orm_util._TraceAdaptRole.JOINEDLOAD_MEMOIZED_ADAPTER,
2417 to_adapt_insp,
2418 equivalents=self.mapper._equivalent_columns,
2419 adapt_required=True,
2420 allow_label_resolve=False,
2421 anonymize_labels=True,
2422 )
2423
2424 assert clauses.is_aliased_class
2425
2426 innerjoin = (
2427 loadopt.local_opts.get("innerjoin", self.parent_property.innerjoin)
2428 if loadopt is not None
2429 else self.parent_property.innerjoin
2430 )
2431
2432 if not innerjoin:
2433 # if this is an outer join, all non-nested eager joins from
2434 # this path must also be outer joins
2435 chained_from_outerjoin = True
2436
2437 compile_state.create_eager_joins.append(
2438 (
2439 self._create_eager_join,
2440 entity,
2441 path,
2442 adapter,
2443 parentmapper,
2444 clauses,
2445 innerjoin,
2446 chained_from_outerjoin,
2447 loadopt._extra_criteria if loadopt else (),
2448 )
2449 )
2450
2451 add_to_collection = compile_state.secondary_columns
2452 path.set(compile_state.attributes, "eager_row_processor", clauses)
2453
2454 return clauses, adapter, add_to_collection, chained_from_outerjoin
2455
2456 def _create_eager_join(
2457 self,
2458 compile_state,
2459 query_entity,
2460 path,
2461 adapter,
2462 parentmapper,
2463 clauses,
2464 innerjoin,
2465 chained_from_outerjoin,
2466 extra_criteria,
2467 ):
2468 if parentmapper is None:
2469 localparent = query_entity.mapper
2470 else:
2471 localparent = parentmapper
2472
2473 # whether or not the Query will wrap the selectable in a subquery,
2474 # and then attach eager load joins to that (i.e., in the case of
2475 # LIMIT/OFFSET etc.)
2476 should_nest_selectable = compile_state._should_nest_selectable
2477
2478 query_entity_key = None
2479
2480 if (
2481 query_entity not in compile_state.eager_joins
2482 and not should_nest_selectable
2483 and compile_state.from_clauses
2484 ):
2485 indexes = sql_util.find_left_clause_that_matches_given(
2486 compile_state.from_clauses, query_entity.selectable
2487 )
2488
2489 if len(indexes) > 1:
2490 # for the eager load case, I can't reproduce this right
2491 # now. For query.join() I can.
2492 raise sa_exc.InvalidRequestError(
2493 "Can't identify which query entity in which to joined "
2494 "eager load from. Please use an exact match when "
2495 "specifying the join path."
2496 )
2497
2498 if indexes:
2499 clause = compile_state.from_clauses[indexes[0]]
2500 # join to an existing FROM clause on the query.
2501 # key it to its list index in the eager_joins dict.
2502 # Query._compile_context will adapt as needed and
2503 # append to the FROM clause of the select().
2504 query_entity_key, default_towrap = indexes[0], clause
2505
2506 if query_entity_key is None:
2507 query_entity_key, default_towrap = (
2508 query_entity,
2509 query_entity.selectable,
2510 )
2511
2512 towrap = compile_state.eager_joins.setdefault(
2513 query_entity_key, default_towrap
2514 )
2515
2516 if adapter:
2517 if getattr(adapter, "is_aliased_class", False):
2518 # joining from an adapted entity. The adapted entity
2519 # might be a "with_polymorphic", so resolve that to our
2520 # specific mapper's entity before looking for our attribute
2521 # name on it.
2522 efm = adapter.aliased_insp._entity_for_mapper(
2523 localparent
2524 if localparent.isa(self.parent)
2525 else self.parent
2526 )
2527
2528 # look for our attribute on the adapted entity, else fall back
2529 # to our straight property
2530 onclause = getattr(efm.entity, self.key, self.parent_property)
2531 else:
2532 onclause = getattr(
2533 orm_util.AliasedClass(
2534 self.parent, adapter.selectable, use_mapper_path=True
2535 ),
2536 self.key,
2537 self.parent_property,
2538 )
2539
2540 else:
2541 onclause = self.parent_property
2542
2543 assert clauses.is_aliased_class
2544
2545 attach_on_outside = (
2546 not chained_from_outerjoin
2547 or not innerjoin
2548 or innerjoin == "unnested"
2549 or query_entity.entity_zero.represents_outer_join
2550 )
2551
2552 extra_join_criteria = extra_criteria
2553 additional_entity_criteria = compile_state.global_attributes.get(
2554 ("additional_entity_criteria", self.mapper), ()
2555 )
2556 if additional_entity_criteria:
2557 extra_join_criteria += tuple(
2558 ae._resolve_where_criteria(self.mapper)
2559 for ae in additional_entity_criteria
2560 if ae.propagate_to_loaders
2561 )
2562
2563 if attach_on_outside:
2564 # this is the "classic" eager join case.
2565 eagerjoin = orm_util._ORMJoin(
2566 towrap,
2567 clauses.aliased_insp,
2568 onclause,
2569 isouter=not innerjoin
2570 or query_entity.entity_zero.represents_outer_join
2571 or (chained_from_outerjoin and isinstance(towrap, sql.Join)),
2572 _left_memo=self.parent,
2573 _right_memo=path[self.mapper],
2574 _extra_criteria=extra_join_criteria,
2575 )
2576 else:
2577 # all other cases are innerjoin=='nested' approach
2578 eagerjoin = self._splice_nested_inner_join(
2579 path, path[-2], towrap, clauses, onclause, extra_join_criteria
2580 )
2581
2582 compile_state.eager_joins[query_entity_key] = eagerjoin
2583
2584 # send a hint to the Query as to where it may "splice" this join
2585 eagerjoin.stop_on = query_entity.selectable
2586
2587 if not parentmapper:
2588 # for parentclause that is the non-eager end of the join,
2589 # ensure all the parent cols in the primaryjoin are actually
2590 # in the
2591 # columns clause (i.e. are not deferred), so that aliasing applied
2592 # by the Query propagates those columns outward.
2593 # This has the effect
2594 # of "undefering" those columns.
2595 for col in sql_util._find_columns(
2596 self.parent_property.primaryjoin
2597 ):
2598 if localparent.persist_selectable.c.contains_column(col):
2599 if adapter:
2600 col = adapter.columns[col]
2601 compile_state._append_dedupe_col_collection(
2602 col, compile_state.primary_columns
2603 )
2604
2605 if self.parent_property.order_by:
2606 compile_state.eager_order_by += tuple(
2607 (eagerjoin._target_adapter.copy_and_process)(
2608 util.to_list(self.parent_property.order_by)
2609 )
2610 )
2611
2612 def _splice_nested_inner_join(
2613 self,
2614 path,
2615 entity_we_want_to_splice_onto,
2616 join_obj,
2617 clauses,
2618 onclause,
2619 extra_criteria,
2620 entity_inside_join_structure: Union[
2621 Mapper, None, Literal[False]
2622 ] = False,
2623 detected_existing_path: Optional[path_registry.PathRegistry] = None,
2624 ):
2625 # recursive fn to splice a nested join into an existing one.
2626 # entity_inside_join_structure=False means this is the outermost call,
2627 # and it should return a value. entity_inside_join_structure=<mapper>
2628 # indicates we've descended into a join and are looking at a FROM
2629 # clause representing this mapper; if this is not
2630 # entity_we_want_to_splice_onto then return None to end the recursive
2631 # branch
2632
2633 assert entity_we_want_to_splice_onto is path[-2]
2634
2635 if entity_inside_join_structure is False:
2636 assert isinstance(join_obj, orm_util._ORMJoin)
2637
2638 if isinstance(join_obj, sql.selectable.FromGrouping):
2639 # FromGrouping - continue descending into the structure
2640 return self._splice_nested_inner_join(
2641 path,
2642 entity_we_want_to_splice_onto,
2643 join_obj.element,
2644 clauses,
2645 onclause,
2646 extra_criteria,
2647 entity_inside_join_structure,
2648 )
2649 elif isinstance(join_obj, orm_util._ORMJoin):
2650 # _ORMJoin - continue descending into the structure
2651
2652 join_right_path = join_obj._right_memo
2653
2654 # see if right side of join is viable
2655 target_join = self._splice_nested_inner_join(
2656 path,
2657 entity_we_want_to_splice_onto,
2658 join_obj.right,
2659 clauses,
2660 onclause,
2661 extra_criteria,
2662 entity_inside_join_structure=(
2663 join_right_path[-1].mapper
2664 if join_right_path is not None
2665 else None
2666 ),
2667 )
2668
2669 if target_join is not None:
2670 # for a right splice, attempt to flatten out
2671 # a JOIN b JOIN c JOIN .. to avoid needless
2672 # parenthesis nesting
2673 if not join_obj.isouter and not target_join.isouter:
2674 eagerjoin = join_obj._splice_into_center(target_join)
2675 else:
2676 eagerjoin = orm_util._ORMJoin(
2677 join_obj.left,
2678 target_join,
2679 join_obj.onclause,
2680 isouter=join_obj.isouter,
2681 _left_memo=join_obj._left_memo,
2682 )
2683
2684 eagerjoin._target_adapter = target_join._target_adapter
2685 return eagerjoin
2686
2687 else:
2688 # see if left side of join is viable
2689 target_join = self._splice_nested_inner_join(
2690 path,
2691 entity_we_want_to_splice_onto,
2692 join_obj.left,
2693 clauses,
2694 onclause,
2695 extra_criteria,
2696 entity_inside_join_structure=join_obj._left_memo,
2697 detected_existing_path=join_right_path,
2698 )
2699
2700 if target_join is not None:
2701 eagerjoin = orm_util._ORMJoin(
2702 target_join,
2703 join_obj.right,
2704 join_obj.onclause,
2705 isouter=join_obj.isouter,
2706 _right_memo=join_obj._right_memo,
2707 )
2708 eagerjoin._target_adapter = target_join._target_adapter
2709 return eagerjoin
2710
2711 # neither side viable, return None, or fail if this was the top
2712 # most call
2713 if entity_inside_join_structure is False:
2714 assert (
2715 False
2716 ), "assertion failed attempting to produce joined eager loads"
2717 return None
2718
2719 # reached an endpoint (e.g. a table that's mapped, or an alias of that
2720 # table). determine if we can use this endpoint to splice onto
2721
2722 # is this the entity we want to splice onto in the first place?
2723 if not entity_we_want_to_splice_onto.isa(entity_inside_join_structure):
2724 return None
2725
2726 # path check. if we know the path how this join endpoint got here,
2727 # lets look at our path we are satisfying and see if we're in the
2728 # wrong place. This is specifically for when our entity may
2729 # appear more than once in the path, issue #11449
2730 # updated in issue #11965.
2731 if detected_existing_path and len(detected_existing_path) > 2:
2732 # this assertion is currently based on how this call is made,
2733 # where given a join_obj, the call will have these parameters as
2734 # entity_inside_join_structure=join_obj._left_memo
2735 # and entity_inside_join_structure=join_obj._right_memo.mapper
2736 assert detected_existing_path[-3] is entity_inside_join_structure
2737
2738 # from that, see if the path we are targeting matches the
2739 # "existing" path of this join all the way up to the midpoint
2740 # of this join object (e.g. the relationship).
2741 # if not, then this is not our target
2742 #
2743 # a test condition where this test is false looks like:
2744 #
2745 # desired splice: Node->kind->Kind
2746 # path of desired splice: NodeGroup->nodes->Node->kind
2747 # path we've located: NodeGroup->nodes->Node->common_node->Node
2748 #
2749 # above, because we want to splice kind->Kind onto
2750 # NodeGroup->nodes->Node, this is not our path because it actually
2751 # goes more steps than we want into self-referential
2752 # ->common_node->Node
2753 #
2754 # a test condition where this test is true looks like:
2755 #
2756 # desired splice: B->c2s->C2
2757 # path of desired splice: A->bs->B->c2s
2758 # path we've located: A->bs->B->c1s->C1
2759 #
2760 # above, we want to splice c2s->C2 onto B, and the located path
2761 # shows that the join ends with B->c1s->C1. so we will
2762 # add another join onto that, which would create a "branch" that
2763 # we might represent in a pseudopath as:
2764 #
2765 # B->c1s->C1
2766 # ->c2s->C2
2767 #
2768 # i.e. A JOIN B ON <bs> JOIN C1 ON <c1s>
2769 # JOIN C2 ON <c2s>
2770 #
2771
2772 if detected_existing_path[0:-2] != path.path[0:-1]:
2773 return None
2774
2775 return orm_util._ORMJoin(
2776 join_obj,
2777 clauses.aliased_insp,
2778 onclause,
2779 isouter=False,
2780 _left_memo=entity_inside_join_structure,
2781 _right_memo=path[path[-1].mapper],
2782 _extra_criteria=extra_criteria,
2783 )
2784
2785 def _create_eager_adapter(self, context, result, adapter, path, loadopt):
2786 compile_state = context.compile_state
2787
2788 user_defined_adapter = (
2789 self._init_user_defined_eager_proc(
2790 loadopt, compile_state, context.attributes
2791 )
2792 if loadopt
2793 else False
2794 )
2795
2796 if user_defined_adapter is not False:
2797 decorator = user_defined_adapter
2798 # user defined eagerloads are part of the "primary"
2799 # portion of the load.
2800 # the adapters applied to the Query should be honored.
2801 if compile_state.compound_eager_adapter and decorator:
2802 decorator = decorator.wrap(
2803 compile_state.compound_eager_adapter
2804 )
2805 elif compile_state.compound_eager_adapter:
2806 decorator = compile_state.compound_eager_adapter
2807 else:
2808 decorator = path.get(
2809 compile_state.attributes, "eager_row_processor"
2810 )
2811 if decorator is None:
2812 return False
2813
2814 if self.mapper._result_has_identity_key(result, decorator):
2815 return decorator
2816 else:
2817 # no identity key - don't return a row
2818 # processor, will cause a degrade to lazy
2819 return False
2820
2821 def create_row_processor(
2822 self,
2823 context,
2824 query_entity,
2825 path,
2826 loadopt,
2827 mapper,
2828 result,
2829 adapter,
2830 populators,
2831 ):
2832
2833 if not context.compile_state.compile_options._enable_eagerloads:
2834 return
2835
2836 if not self.parent.class_manager[self.key].impl.supports_population:
2837 raise sa_exc.InvalidRequestError(
2838 "'%s' does not support object "
2839 "population - eager loading cannot be applied." % self
2840 )
2841
2842 if self.uselist:
2843 context.loaders_require_uniquing = True
2844
2845 our_path = path[self.parent_property]
2846
2847 eager_adapter = self._create_eager_adapter(
2848 context, result, adapter, our_path, loadopt
2849 )
2850
2851 if eager_adapter is not False:
2852 key = self.key
2853
2854 _instance = loading._instance_processor(
2855 query_entity,
2856 self.mapper,
2857 context,
2858 result,
2859 our_path[self.entity],
2860 eager_adapter,
2861 )
2862
2863 if not self.uselist:
2864 self._create_scalar_loader(context, key, _instance, populators)
2865 else:
2866 self._create_collection_loader(
2867 context, key, _instance, populators
2868 )
2869 else:
2870 self.parent_property._get_strategy(
2871 (("lazy", "select"),)
2872 ).create_row_processor(
2873 context,
2874 query_entity,
2875 path,
2876 loadopt,
2877 mapper,
2878 result,
2879 adapter,
2880 populators,
2881 )
2882
2883 def _create_collection_loader(self, context, key, _instance, populators):
2884 def load_collection_from_joined_new_row(state, dict_, row):
2885 # note this must unconditionally clear out any existing collection.
2886 # an existing collection would be present only in the case of
2887 # populate_existing().
2888 collection = attributes.init_state_collection(state, dict_, key)
2889 result_list = util.UniqueAppender(
2890 collection, "append_without_event"
2891 )
2892 context.attributes[(state, key)] = result_list
2893 inst = _instance(row)
2894 if inst is not None:
2895 result_list.append(inst)
2896
2897 def load_collection_from_joined_existing_row(state, dict_, row):
2898 if (state, key) in context.attributes:
2899 result_list = context.attributes[(state, key)]
2900 else:
2901 # appender_key can be absent from context.attributes
2902 # with isnew=False when self-referential eager loading
2903 # is used; the same instance may be present in two
2904 # distinct sets of result columns
2905 collection = attributes.init_state_collection(
2906 state, dict_, key
2907 )
2908 result_list = util.UniqueAppender(
2909 collection, "append_without_event"
2910 )
2911 context.attributes[(state, key)] = result_list
2912 inst = _instance(row)
2913 if inst is not None:
2914 result_list.append(inst)
2915
2916 def load_collection_from_joined_exec(state, dict_, row):
2917 _instance(row)
2918
2919 populators["new"].append(
2920 (self.key, load_collection_from_joined_new_row)
2921 )
2922 populators["existing"].append(
2923 (self.key, load_collection_from_joined_existing_row)
2924 )
2925 if context.invoke_all_eagers:
2926 populators["eager"].append(
2927 (self.key, load_collection_from_joined_exec)
2928 )
2929
2930 def _create_scalar_loader(self, context, key, _instance, populators):
2931 def load_scalar_from_joined_new_row(state, dict_, row):
2932 # set a scalar object instance directly on the parent
2933 # object, bypassing InstrumentedAttribute event handlers.
2934 dict_[key] = _instance(row)
2935
2936 def load_scalar_from_joined_existing_row(state, dict_, row):
2937 # call _instance on the row, even though the object has
2938 # been created, so that we further descend into properties
2939 existing = _instance(row)
2940
2941 # conflicting value already loaded, this shouldn't happen
2942 if key in dict_:
2943 if existing is not dict_[key]:
2944 util.warn(
2945 "Multiple rows returned with "
2946 "uselist=False for eagerly-loaded attribute '%s' "
2947 % self
2948 )
2949 else:
2950 # this case is when one row has multiple loads of the
2951 # same entity (e.g. via aliasing), one has an attribute
2952 # that the other doesn't.
2953 dict_[key] = existing
2954
2955 def load_scalar_from_joined_exec(state, dict_, row):
2956 _instance(row)
2957
2958 populators["new"].append((self.key, load_scalar_from_joined_new_row))
2959 populators["existing"].append(
2960 (self.key, load_scalar_from_joined_existing_row)
2961 )
2962 if context.invoke_all_eagers:
2963 populators["eager"].append(
2964 (self.key, load_scalar_from_joined_exec)
2965 )
2966
2967
2968@log.class_logger
2969@relationships.RelationshipProperty.strategy_for(lazy="selectin")
2970class _SelectInLoader(_PostLoader, util.MemoizedSlots):
2971 __slots__ = (
2972 "join_depth",
2973 "omit_join",
2974 "_parent_alias",
2975 "_query_info",
2976 "_fallback_query_info",
2977 )
2978
2979 query_info = collections.namedtuple(
2980 "queryinfo",
2981 [
2982 "load_only_child",
2983 "load_with_join",
2984 "in_expr",
2985 "pk_cols",
2986 "zero_idx",
2987 "n_pk",
2988 "child_lookup_cols",
2989 ],
2990 )
2991
2992 _chunksize = 500
2993
2994 @classmethod
2995 def _set_chunksize(cls, loadopt) -> int:
2996 if loadopt is None or hasattr(loadopt, "local_opts") is None:
2997 return cls._chunksize
2998
2999 user_input = loadopt.local_opts.get("chunksize", None)
3000 if user_input is None:
3001 return cls._chunksize
3002 elif not isinstance(user_input, int) or user_input < 1:
3003 raise sa_exc.ArgumentError(
3004 f"'chunksize={user_input}' is not an appropriate input, "
3005 f"please use a positive non-zero integer."
3006 )
3007 return user_input
3008
3009 def __init__(self, parent, strategy_key):
3010 super().__init__(parent, strategy_key)
3011 self.join_depth = self.parent_property.join_depth
3012 is_m2o = self.parent_property.direction is interfaces.MANYTOONE
3013 is_m2m = self.parent_property.direction is interfaces.MANYTOMANY
3014
3015 if self.parent_property.omit_join is not None:
3016 self.omit_join = self.parent_property.omit_join
3017 else:
3018 lazyloader = self.parent_property._get_strategy(
3019 (("lazy", "select"),)
3020 )
3021 if is_m2o:
3022 self.omit_join = lazyloader.use_get
3023 elif is_m2m and not self.parent_property._is_self_referential:
3024 join_cond = self.parent_property._join_condition
3025 self.omit_join = join_cond.secondary_covers_parent_primary_key
3026 else:
3027 self.omit_join = self.parent._get_clause[0].compare(
3028 lazyloader._rev_lazywhere,
3029 use_proxies=True,
3030 compare_keys=False,
3031 equivalents=self.parent._equivalent_columns,
3032 )
3033
3034 if self.omit_join:
3035 if is_m2o:
3036 self._query_info = self._init_for_omit_join_m2o()
3037 self._fallback_query_info = self._init_for_join()
3038 else:
3039 self._query_info = self._init_for_omit_join()
3040 else:
3041 self._query_info = self._init_for_join()
3042
3043 def _init_for_omit_join(self):
3044 pk_to_fk = dict(
3045 self.parent_property._join_condition.local_remote_pairs
3046 )
3047 pk_to_fk.update(
3048 (equiv, pk_to_fk[k])
3049 for k in list(pk_to_fk)
3050 for equiv in self.parent._equivalent_columns.get(k, ())
3051 )
3052
3053 pk_cols = fk_cols = [
3054 pk_to_fk[col] for col in self.parent.primary_key if col in pk_to_fk
3055 ]
3056 if len(fk_cols) > 1:
3057 in_expr = sql.tuple_(*fk_cols)
3058 zero_idx = False
3059 else:
3060 in_expr = fk_cols[0]
3061 zero_idx = True
3062
3063 return self.query_info(
3064 False, False, in_expr, pk_cols, zero_idx, len(pk_cols), None
3065 )
3066
3067 def _init_for_omit_join_m2o(self):
3068 pk_cols = self.mapper.primary_key
3069 if len(pk_cols) > 1:
3070 in_expr = sql.tuple_(*pk_cols)
3071 zero_idx = False
3072 else:
3073 in_expr = pk_cols[0]
3074 zero_idx = True
3075
3076 lazyloader = self.parent_property._get_strategy((("lazy", "select"),))
3077 lookup_cols = [lazyloader._equated_columns[pk] for pk in pk_cols]
3078
3079 return self.query_info(
3080 True, False, in_expr, pk_cols, zero_idx, len(pk_cols), lookup_cols
3081 )
3082
3083 def _init_for_join(self):
3084 self._parent_alias = AliasedClass(self.parent.class_)
3085 pa_insp = inspect(self._parent_alias)
3086 pk_cols = [
3087 pa_insp._adapt_element(col) for col in self.parent.primary_key
3088 ]
3089 if len(pk_cols) > 1:
3090 in_expr = sql.tuple_(*pk_cols)
3091 zero_idx = False
3092 else:
3093 in_expr = pk_cols[0]
3094 zero_idx = True
3095 return self.query_info(
3096 False, True, in_expr, pk_cols, zero_idx, len(pk_cols), None
3097 )
3098
3099 def init_class_attribute(self, mapper):
3100 self.parent_property._get_strategy(
3101 (("lazy", "select"),)
3102 ).init_class_attribute(mapper)
3103
3104 def create_row_processor(
3105 self,
3106 context,
3107 query_entity,
3108 path,
3109 loadopt,
3110 mapper,
3111 result,
3112 adapter,
3113 populators,
3114 ):
3115 if context.refresh_state:
3116 return self._immediateload_create_row_processor(
3117 context,
3118 query_entity,
3119 path,
3120 loadopt,
3121 mapper,
3122 result,
3123 adapter,
3124 populators,
3125 )
3126
3127 (
3128 effective_path,
3129 run_loader,
3130 execution_options,
3131 recursion_depth,
3132 ) = self._setup_for_recursion(
3133 context, path, loadopt, join_depth=self.join_depth
3134 )
3135
3136 if not run_loader:
3137 return
3138
3139 if not context.compile_state.compile_options._enable_eagerloads:
3140 return
3141
3142 if not self.parent.class_manager[self.key].impl.supports_population:
3143 raise sa_exc.InvalidRequestError(
3144 "'%s' does not support object "
3145 "population - eager loading cannot be applied." % self
3146 )
3147
3148 # a little dance here as the "path" is still something that only
3149 # semi-tracks the exact series of things we are loading, still not
3150 # telling us about with_polymorphic() and stuff like that when it's at
3151 # the root.. the initial MapperEntity is more accurate for this case.
3152 if len(path) == 1:
3153 if not orm_util._entity_isa(query_entity.entity_zero, self.parent):
3154 return
3155 elif not orm_util._entity_isa(
3156 path[-1], self.parent
3157 ) and not self.parent.isa(path[-1].mapper):
3158 # second check accommodates a polymorphic entity where
3159 # the path has been normalized to the base mapper but
3160 # self.parent is a subclass mapper, e.g.
3161 # joinedload(A.b.of_type(poly)).selectinload(poly.Sub.rel)
3162 # Fixes #13209.
3163 return
3164
3165 selectin_path = effective_path
3166
3167 path_w_prop = path[self.parent_property]
3168
3169 # build up a path indicating the path from the leftmost
3170 # entity to the thing we're subquery loading.
3171 with_poly_entity = path_w_prop.get(
3172 context.attributes, "path_with_polymorphic", None
3173 )
3174 if with_poly_entity is not None:
3175 effective_entity = inspect(with_poly_entity)
3176 else:
3177 effective_entity = self.entity
3178
3179 loading._PostLoad.callable_for_path(
3180 context,
3181 selectin_path,
3182 self.parent,
3183 self.parent_property,
3184 self._load_for_path,
3185 effective_entity,
3186 loadopt,
3187 recursion_depth,
3188 execution_options,
3189 )
3190
3191 def _load_for_path(
3192 self,
3193 context,
3194 path,
3195 states,
3196 load_only,
3197 effective_entity,
3198 loadopt,
3199 recursion_depth,
3200 execution_options,
3201 ):
3202 if load_only and self.key not in load_only:
3203 return
3204
3205 query_info = self._query_info
3206
3207 if query_info.load_only_child:
3208 our_states = collections.defaultdict(list)
3209 none_states = []
3210
3211 mapper = self.parent
3212
3213 # attribute keys for the lookup columns; when these are
3214 # present in a state's dict, reading them directly is
3215 # equivalent to the PASSIVE_NO_FETCH attribute lookup below.
3216 # whether or not a key is present can vary per state, e.g.
3217 # individual instances may have the attribute expired or
3218 # deferred, so this is determined state-by-state
3219 get_related_ident = mapper._state_ident_getter(
3220 query_info.child_lookup_cols,
3221 passive=attributes.PASSIVE_NO_FETCH,
3222 )
3223
3224 for state, overwrite in states:
3225 state_dict = state.dict
3226 related_ident = get_related_ident(state, state_dict)
3227 # if the loaded parent objects do not have the foreign key
3228 # to the related item loaded, then degrade into the joined
3229 # version of selectinload
3230 if LoaderCallableStatus.PASSIVE_NO_RESULT in related_ident:
3231 query_info = self._fallback_query_info
3232 break
3233
3234 # organize states into lists keyed to particular foreign
3235 # key values.
3236 if None not in related_ident:
3237 our_states[related_ident].append(
3238 (state, state_dict, overwrite)
3239 )
3240 else:
3241 # For FK values that have None, add them to a
3242 # separate collection that will be populated separately
3243 none_states.append((state, state_dict, overwrite))
3244
3245 # note the above conditional may have changed query_info
3246 if not query_info.load_only_child:
3247 our_states = [
3248 (state.key[1], state, state.dict, overwrite)
3249 for state, overwrite in states
3250 ]
3251
3252 pk_cols = query_info.pk_cols
3253 in_expr = query_info.in_expr
3254
3255 if not query_info.load_with_join:
3256 # in "omit join" mode, the primary key column and the
3257 # "in" expression are in terms of the related entity. So
3258 # if the related entity is polymorphic or otherwise aliased,
3259 # we need to adapt our "pk_cols" and "in_expr" to that
3260 # entity. in non-"omit join" mode, these are against the
3261 # parent entity and do not need adaption.
3262 if effective_entity.is_aliased_class:
3263 pk_cols = [
3264 effective_entity._adapt_element(col) for col in pk_cols
3265 ]
3266 in_expr = effective_entity._adapt_element(in_expr)
3267
3268 entity_sql = effective_entity.__clause_element__()
3269 q = Select._create_raw_select(
3270 _raw_columns=[*pk_cols, entity_sql],
3271 _compile_options=_ORMCompileState.default_compile_options,
3272 _propagate_attrs={
3273 "compile_state_plugin": "orm",
3274 "plugin_subject": effective_entity,
3275 },
3276 )
3277
3278 if (
3279 self.parent_property.secondary is not None
3280 and self.omit_join is True
3281 ):
3282 # The secondaryjoin condition is used to connect the
3283 # secondary table to the related entity,
3284 # and is required for composite foreign keys where SQLAlchemy
3285 # cannot determine the join condition.
3286 q = q.select_from(self.parent_property.secondary).join(
3287 entity_sql, self.parent_property._join_condition.secondaryjoin
3288 )
3289 elif not query_info.load_with_join:
3290 # the pk columns in the "omit_join" case are raw, non-annotated
3291 # columns, so to ensure the Query knows its primary entity, we
3292 # add it explicitly. Using annotated columns here would hit a
3293 # performance issue detailed in issue #4347.
3294 q = q.select_from(effective_entity)
3295 else:
3296 # in the non-omit_join case, the pk columns are against the
3297 # annotated/mapped column of the parent entity, but the #4347
3298 # issue does not occur in this case.
3299 q = q.select_from(self._parent_alias).join(
3300 getattr(self._parent_alias, self.parent_property.key).of_type(
3301 effective_entity
3302 )
3303 )
3304
3305 q = q.filter(in_expr.in_(sql.bindparam("primary_keys")))
3306
3307 # a test which exercises what these comments talk about is
3308 # test_selectin_relations.py -> test_twolevel_selectin_w_polymorphic
3309 #
3310 # effective_entity above is given to us in terms of the cached
3311 # statement, namely this one:
3312 orig_query = context.compile_state.select_statement
3313
3314 # the actual statement that was requested is this one:
3315 # context_query = context.user_passed_query
3316 #
3317 # that's not the cached one, however. So while it is of the identical
3318 # structure, if it has entities like AliasedInsp, which we get from
3319 # aliased() or with_polymorphic(), the AliasedInsp will likely be a
3320 # different object identity each time, and will not match up
3321 # hashing-wise to the corresponding AliasedInsp that's in the
3322 # cached query, meaning it won't match on paths and loader lookups
3323 # and loaders like this one will be skipped if it is used in options.
3324 #
3325 # as it turns out, standard loader options like selectinload(),
3326 # lazyload() that have a path need
3327 # to come from the cached query so that the AliasedInsp etc. objects
3328 # that are in the query line up with the object that's in the path
3329 # of the strategy object. however other options like
3330 # with_loader_criteria() that doesn't have a path (has a fixed entity)
3331 # and needs to have access to the latest closure state in order to
3332 # be correct, we need to use the uncached one.
3333 #
3334 # as of #8399 we let the loader option itself figure out what it
3335 # wants to do given cached and uncached version of itself.
3336
3337 effective_path = path[self.parent_property]
3338
3339 if orig_query is context.user_passed_query:
3340 new_options = orig_query._with_options
3341 else:
3342 cached_options = orig_query._with_options
3343 uncached_options = context.user_passed_query._with_options
3344
3345 # propagate compile state options from the original query,
3346 # updating their "extra_criteria" as necessary.
3347 # note this will create a different cache key than
3348 # "orig" options if extra_criteria is present, because the copy
3349 # of extra_criteria will have different boundparam than that of
3350 # the QueryableAttribute in the path
3351 new_options = [
3352 orig_opt._adapt_cached_option_to_uncached_option(
3353 context, uncached_opt
3354 )
3355 for orig_opt, uncached_opt in zip(
3356 cached_options, uncached_options
3357 )
3358 ]
3359
3360 if loadopt and loadopt._extra_criteria:
3361 new_options += (
3362 orm_util.LoaderCriteriaOption(
3363 effective_entity,
3364 loadopt._generate_extra_criteria(context),
3365 ),
3366 )
3367
3368 if recursion_depth is not None:
3369 effective_path = effective_path._truncate_recursive()
3370
3371 q = q.options(*new_options)
3372
3373 q = q._update_compile_options({"_current_path": effective_path})
3374 if context.populate_existing:
3375 q = q.execution_options(populate_existing=True)
3376
3377 if self.parent_property.order_by:
3378 if not query_info.load_with_join:
3379 eager_order_by = self.parent_property.order_by
3380 if effective_entity.is_aliased_class:
3381 eager_order_by = [
3382 effective_entity._adapt_element(elem)
3383 for elem in eager_order_by
3384 ]
3385 q = q.order_by(*eager_order_by)
3386 else:
3387
3388 def _setup_outermost_orderby(compile_context):
3389 compile_context.eager_order_by += tuple(
3390 util.to_list(self.parent_property.order_by)
3391 )
3392
3393 q = q._add_compile_state_func(
3394 _setup_outermost_orderby, self.parent_property
3395 )
3396
3397 chunksize = self._set_chunksize(loadopt)
3398
3399 if query_info.load_only_child:
3400 self._load_via_child(
3401 our_states,
3402 none_states,
3403 query_info,
3404 q,
3405 context,
3406 execution_options,
3407 chunksize,
3408 )
3409 else:
3410 self._load_via_parent(
3411 our_states,
3412 query_info,
3413 q,
3414 context,
3415 execution_options,
3416 chunksize,
3417 )
3418
3419 def _load_via_child(
3420 self,
3421 our_states,
3422 none_states,
3423 query_info,
3424 q,
3425 context,
3426 execution_options,
3427 chunksize,
3428 ):
3429 uselist = self.uselist
3430 n_pk = query_info.n_pk
3431 zero_idx = query_info.zero_idx
3432
3433 # historically this used sorted instead of list to add determinism in
3434 # tests. Since dicts are now ordered it's likely no longer needed
3435 our_keys = list(our_states)
3436 while our_keys:
3437 chunk = our_keys[0:chunksize]
3438 our_keys = our_keys[chunksize:]
3439 primary_keys = [key[0] if zero_idx else key for key in chunk]
3440 result = context.session.execute(
3441 q,
3442 params={"primary_keys": primary_keys},
3443 execution_options=execution_options,
3444 )
3445 if result.context is not None and result.context.requires_uniquing:
3446 rows = result.unique()
3447 else:
3448 rows = result._raw_all_tuples()
3449 data = {row[:n_pk]: row[n_pk] for row in rows}
3450
3451 for key in chunk:
3452 # for a real foreign key and no concurrent changes to the
3453 # DB while running this method, "key" is always present in
3454 # data. However, for primaryjoins without real foreign keys
3455 # a non-None primaryjoin condition may still refer to no
3456 # related object.
3457 related_obj = data.get(key, None)
3458 for state, dict_, overwrite in our_states[key]:
3459 if not overwrite and self.key in dict_:
3460 continue
3461
3462 state.get_impl(self.key).set_committed_value(
3463 state,
3464 dict_,
3465 related_obj if not uselist else [related_obj],
3466 )
3467 # populate none states with empty value / collection
3468 for state, dict_, overwrite in none_states:
3469 if not overwrite and self.key in dict_:
3470 continue
3471
3472 # note it's OK if this is a uselist=True attribute, the empty
3473 # collection will be populated
3474 state.get_impl(self.key).set_committed_value(state, dict_, None)
3475
3476 def _load_via_parent(
3477 self, our_states, query_info, q, context, execution_options, chunksize
3478 ):
3479 uselist = self.uselist
3480 n_pk = query_info.n_pk
3481 zero_idx = query_info.zero_idx
3482 _empty_result = () if uselist else None
3483
3484 while our_states:
3485 chunk = our_states[0:chunksize]
3486 our_states = our_states[chunksize:]
3487
3488 primary_keys = [
3489 item[0][0] if zero_idx else item[0] for item in chunk
3490 ]
3491
3492 result = context.session.execute(
3493 q,
3494 params={"primary_keys": primary_keys},
3495 execution_options=execution_options,
3496 )
3497 if result.context is not None and result.context.requires_uniquing:
3498 rows = result.unique()
3499 else:
3500 rows = result._raw_all_tuples()
3501 data = collections.defaultdict(list)
3502 for row in rows:
3503 data[row[:n_pk]].append(row[n_pk])
3504
3505 for key, state, state_dict, overwrite in chunk:
3506 if not overwrite and self.key in state_dict:
3507 continue
3508
3509 collection = data.get(key, _empty_result)
3510
3511 if not uselist and collection:
3512 if len(collection) > 1:
3513 util.warn(
3514 "Multiple rows returned with "
3515 "uselist=False for eagerly-loaded "
3516 "attribute '%s' " % self
3517 )
3518 state.get_impl(self.key).set_committed_value(
3519 state, state_dict, collection[0]
3520 )
3521 else:
3522 # note that empty tuple set on uselist=False sets the
3523 # value to None
3524 state.get_impl(self.key).set_committed_value(
3525 state, state_dict, collection
3526 )
3527
3528
3529def _single_parent_validator(desc, prop):
3530 def _do_check(state, value, oldvalue, initiator):
3531 if value is not None and initiator.key == prop.key:
3532 hasparent = initiator.hasparent(attributes.instance_state(value))
3533 if hasparent and oldvalue is not value:
3534 raise sa_exc.InvalidRequestError(
3535 "Instance %s is already associated with an instance "
3536 "of %s via its %s attribute, and is only allowed a "
3537 "single parent."
3538 % (orm_util.instance_str(value), state.class_, prop),
3539 code="bbf1",
3540 )
3541 return value
3542
3543 def append(state, value, initiator):
3544 return _do_check(state, value, None, initiator)
3545
3546 def set_(state, value, oldvalue, initiator):
3547 return _do_check(state, value, oldvalue, initiator)
3548
3549 event.listen(
3550 desc, "append", append, raw=True, retval=True, active_history=True
3551 )
3552 event.listen(desc, "set", set_, raw=True, retval=True, active_history=True)