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