codekingpro/portable-devtools
114k
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
16import itertools
17from typing import Any
18from typing import Dict
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
62from ..util.typing import Literal
63
64if TYPE_CHECKING:
65 from .mapper import Mapper
66 from .relationships import RelationshipProperty
67 from ..sql.elements import ColumnElement
68
69
70def _register_attribute(
71 prop,
72 mapper,
73 useobject,
74 compare_function=None,
75 typecallable=None,
76 callable_=None,
77 proxy_property=None,
78 active_history=False,
79 impl_class=None,
80 **kw,
81):
82 listen_hooks = []
83
84 uselist = useobject and prop.uselist
85
86 if useobject and prop.single_parent:
87 listen_hooks.append(single_parent_validator)
88
89 if prop.key in prop.parent.validators:
90 fn, opts = prop.parent.validators[prop.key]
91 listen_hooks.append(
92 lambda desc, prop: orm_util._validator_events(
93 desc, prop.key, fn, **opts
94 )
95 )
96
97 if useobject:
98 listen_hooks.append(unitofwork.track_cascade_events)
99
100 # need to assemble backref listeners
101 # after the singleparentvalidator, mapper validator
102 if useobject:
103 backref = prop.back_populates
104 if backref and prop._effective_sync_backref:
105 listen_hooks.append(
106 lambda desc, prop: attributes.backref_listeners(
107 desc, backref, uselist
108 )
109 )
110
111 # a single MapperProperty is shared down a class inheritance
112 # hierarchy, so we set up attribute instrumentation and backref event
113 # for each mapper down the hierarchy.
114
115 # typically, "mapper" is the same as prop.parent, due to the way
116 # the configure_mappers() process runs, however this is not strongly
117 # enforced, and in the case of a second configure_mappers() run the
118 # mapper here might not be prop.parent; also, a subclass mapper may
119 # be called here before a superclass mapper. That is, can't depend
120 # on mappers not already being set up so we have to check each one.
121
122 for m in mapper.self_and_descendants:
123 if prop is m._props.get(
124 prop.key
125 ) and not m.class_manager._attr_has_impl(prop.key):
126 desc = attributes.register_attribute_impl(
127 m.class_,
128 prop.key,
129 parent_token=prop,
130 uselist=uselist,
131 compare_function=compare_function,
132 useobject=useobject,
133 trackparent=useobject
134 and (
135 prop.single_parent
136 or prop.direction is interfaces.ONETOMANY
137 ),
138 typecallable=typecallable,
139 callable_=callable_,
140 active_history=active_history,
141 impl_class=impl_class,
142 send_modified_events=not useobject or not prop.viewonly,
143 doc=prop.doc,
144 **kw,
145 )
146
147 for hook in listen_hooks:
148 hook(desc, prop)
149
150
151@properties.ColumnProperty.strategy_for(instrument=False, deferred=False)
152class UninstrumentedColumnLoader(LoaderStrategy):
153 """Represent a non-instrumented MapperProperty.
154
155 The polymorphic_on argument of mapper() often results in this,
156 if the argument is against the with_polymorphic selectable.
157
158 """
159
160 __slots__ = ("columns",)
161
162 def __init__(self, parent, strategy_key):
163 super().__init__(parent, strategy_key)
164 self.columns = self.parent_property.columns
165
166 def setup_query(
167 self,
168 compile_state,
169 query_entity,
170 path,
171 loadopt,
172 adapter,
173 column_collection=None,
174 **kwargs,
175 ):
176 for c in self.columns:
177 if adapter:
178 c = adapter.columns[c]
179 compile_state._append_dedupe_col_collection(c, column_collection)
180
181 def create_row_processor(
182 self,
183 context,
184 query_entity,
185 path,
186 loadopt,
187 mapper,
188 result,
189 adapter,
190 populators,
191 ):
192 pass
193
194
195@log.class_logger
196@properties.ColumnProperty.strategy_for(instrument=True, deferred=False)
197class ColumnLoader(LoaderStrategy):
198 """Provide loading behavior for a :class:`.ColumnProperty`."""
199
200 __slots__ = "columns", "is_composite"
201
202 def __init__(self, parent, strategy_key):
203 super().__init__(parent, strategy_key)
204 self.columns = self.parent_property.columns
205 self.is_composite = hasattr(self.parent_property, "composite_class")
206
207 def setup_query(
208 self,
209 compile_state,
210 query_entity,
211 path,
212 loadopt,
213 adapter,
214 column_collection,
215 memoized_populators,
216 check_for_adapt=False,
217 **kwargs,
218 ):
219 for c in self.columns:
220 if adapter:
221 if check_for_adapt:
222 c = adapter.adapt_check_present(c)
223 if c is None:
224 return
225 else:
226 c = adapter.columns[c]
227
228 compile_state._append_dedupe_col_collection(c, column_collection)
229
230 fetch = self.columns[0]
231 if adapter:
232 fetch = adapter.columns[fetch]
233 if fetch is None:
234 # None happens here only for dml bulk_persistence cases
235 # when context.DMLReturningColFilter is used
236 return
237
238 memoized_populators[self.parent_property] = fetch
239
240 def init_class_attribute(self, mapper):
241 self.is_class_level = True
242 coltype = self.columns[0].type
243 # TODO: check all columns ? check for foreign key as well?
244 active_history = (
245 self.parent_property.active_history
246 or self.columns[0].primary_key
247 or (
248 mapper.version_id_col is not None
249 and mapper._columntoproperty.get(mapper.version_id_col, None)
250 is self.parent_property
251 )
252 )
253
254 _register_attribute(
255 self.parent_property,
256 mapper,
257 useobject=False,
258 compare_function=coltype.compare_values,
259 active_history=active_history,
260 )
261
262 def create_row_processor(
263 self,
264 context,
265 query_entity,
266 path,
267 loadopt,
268 mapper,
269 result,
270 adapter,
271 populators,
272 ):
273 # look through list of columns represented here
274 # to see which, if any, is present in the row.
275
276 for col in self.columns:
277 if adapter:
278 col = adapter.columns[col]
279 getter = result._getter(col, False)
280 if getter:
281 populators["quick"].append((self.key, getter))
282 break
283 else:
284 populators["expire"].append((self.key, True))
285
286
287@log.class_logger
288@properties.ColumnProperty.strategy_for(query_expression=True)
289class ExpressionColumnLoader(ColumnLoader):
290 def __init__(self, parent, strategy_key):
291 super().__init__(parent, strategy_key)
292
293 # compare to the "default" expression that is mapped in
294 # the column. If it's sql.null, we don't need to render
295 # unless an expr is passed in the options.
296 null = sql.null().label(None)
297 self._have_default_expression = any(
298 not c.compare(null) for c in self.parent_property.columns
299 )
300
301 def setup_query(
302 self,
303 compile_state,
304 query_entity,
305 path,
306 loadopt,
307 adapter,
308 column_collection,
309 memoized_populators,
310 **kwargs,
311 ):
312 columns = None
313 if loadopt and loadopt._extra_criteria:
314 columns = loadopt._extra_criteria
315
316 elif self._have_default_expression:
317 columns = self.parent_property.columns
318
319 if columns is None:
320 return
321
322 for c in columns:
323 if adapter:
324 c = adapter.columns[c]
325 compile_state._append_dedupe_col_collection(c, column_collection)
326
327 fetch = columns[0]
328 if adapter:
329 fetch = adapter.columns[fetch]
330 if fetch is None:
331 # None is not expected to be the result of any
332 # adapter implementation here, however there may be theoretical
333 # usages of returning() with context.DMLReturningColFilter
334 return
335
336 memoized_populators[self.parent_property] = fetch
337
338 def create_row_processor(
339 self,
340 context,
341 query_entity,
342 path,
343 loadopt,
344 mapper,
345 result,
346 adapter,
347 populators,
348 ):
349 # look through list of columns represented here
350 # to see which, if any, is present in the row.
351 if loadopt and loadopt._extra_criteria:
352 columns = loadopt._extra_criteria
353
354 for col in columns:
355 if adapter:
356 col = adapter.columns[col]
357 getter = result._getter(col, False)
358 if getter:
359 populators["quick"].append((self.key, getter))
360 break
361 else:
362 populators["expire"].append((self.key, True))
363
364 def init_class_attribute(self, mapper):
365 self.is_class_level = True
366
367 _register_attribute(
368 self.parent_property,
369 mapper,
370 useobject=False,
371 compare_function=self.columns[0].type.compare_values,
372 accepts_scalar_loader=False,
373 )
374
375
376@log.class_logger
377@properties.ColumnProperty.strategy_for(deferred=True, instrument=True)
378@properties.ColumnProperty.strategy_for(
379 deferred=True, instrument=True, raiseload=True
380)
381@properties.ColumnProperty.strategy_for(do_nothing=True)
382class DeferredColumnLoader(LoaderStrategy):
383 """Provide loading behavior for a deferred :class:`.ColumnProperty`."""
384
385 __slots__ = "columns", "group", "raiseload"
386
387 def __init__(self, parent, strategy_key):
388 super().__init__(parent, strategy_key)
389 if hasattr(self.parent_property, "composite_class"):
390 raise NotImplementedError(
391 "Deferred loading for composite types not implemented yet"
392 )
393 self.raiseload = self.strategy_opts.get("raiseload", False)
394 self.columns = self.parent_property.columns
395 self.group = self.parent_property.group
396
397 def create_row_processor(
398 self,
399 context,
400 query_entity,
401 path,
402 loadopt,
403 mapper,
404 result,
405 adapter,
406 populators,
407 ):
408 # for a DeferredColumnLoader, this method is only used during a
409 # "row processor only" query; see test_deferred.py ->
410 # tests with "rowproc_only" in their name. As of the 1.0 series,
411 # loading._instance_processor doesn't use a "row processing" function
412 # to populate columns, instead it uses data in the "populators"
413 # dictionary. Normally, the DeferredColumnLoader.setup_query()
414 # sets up that data in the "memoized_populators" dictionary
415 # and "create_row_processor()" here is never invoked.
416
417 if (
418 context.refresh_state
419 and context.query._compile_options._only_load_props
420 and self.key in context.query._compile_options._only_load_props
421 ):
422 self.parent_property._get_strategy(
423 (("deferred", False), ("instrument", True))
424 ).create_row_processor(
425 context,
426 query_entity,
427 path,
428 loadopt,
429 mapper,
430 result,
431 adapter,
432 populators,
433 )
434
435 elif not self.is_class_level:
436 if self.raiseload:
437 set_deferred_for_local_state = (
438 self.parent_property._raise_column_loader
439 )
440 else:
441 set_deferred_for_local_state = (
442 self.parent_property._deferred_column_loader
443 )
444 populators["new"].append((self.key, set_deferred_for_local_state))
445 else:
446 populators["expire"].append((self.key, False))
447
448 def init_class_attribute(self, mapper):
449 self.is_class_level = True
450
451 _register_attribute(
452 self.parent_property,
453 mapper,
454 useobject=False,
455 compare_function=self.columns[0].type.compare_values,
456 callable_=self._load_for_state,
457 load_on_unexpire=False,
458 )
459
460 def setup_query(
461 self,
462 compile_state,
463 query_entity,
464 path,
465 loadopt,
466 adapter,
467 column_collection,
468 memoized_populators,
469 only_load_props=None,
470 **kw,
471 ):
472 if (
473 (
474 compile_state.compile_options._render_for_subquery
475 and self.parent_property._renders_in_subqueries
476 )
477 or (
478 loadopt
479 and set(self.columns).intersection(
480 self.parent._should_undefer_in_wildcard
481 )
482 )
483 or (
484 loadopt
485 and self.group
486 and loadopt.local_opts.get(
487 "undefer_group_%s" % self.group, False
488 )
489 )
490 or (only_load_props and self.key in only_load_props)
491 ):
492 self.parent_property._get_strategy(
493 (("deferred", False), ("instrument", True))
494 ).setup_query(
495 compile_state,
496 query_entity,
497 path,
498 loadopt,
499 adapter,
500 column_collection,
501 memoized_populators,
502 **kw,
503 )
504 elif self.is_class_level:
505 memoized_populators[self.parent_property] = _SET_DEFERRED_EXPIRED
506 elif not self.raiseload:
507 memoized_populators[self.parent_property] = _DEFER_FOR_STATE
508 else:
509 memoized_populators[self.parent_property] = _RAISE_FOR_STATE
510
511 def _load_for_state(self, state, passive):
512 if not state.key:
513 return LoaderCallableStatus.ATTR_EMPTY
514
515 if not passive & PassiveFlag.SQL_OK:
516 return LoaderCallableStatus.PASSIVE_NO_RESULT
517
518 localparent = state.manager.mapper
519
520 if self.group:
521 toload = [
522 p.key
523 for p in localparent.iterate_properties
524 if isinstance(p, StrategizedProperty)
525 and isinstance(p.strategy, DeferredColumnLoader)
526 and p.group == self.group
527 ]
528 else:
529 toload = [self.key]
530
531 # narrow the keys down to just those which have no history
532 group = [k for k in toload if k in state.unmodified]
533
534 session = _state_session(state)
535 if session is None:
536 raise orm_exc.DetachedInstanceError(
537 "Parent instance %s is not bound to a Session; "
538 "deferred load operation of attribute '%s' cannot proceed"
539 % (orm_util.state_str(state), self.key)
540 )
541
542 if self.raiseload:
543 self._invoke_raise_load(state, passive, "raise")
544
545 loading.load_scalar_attributes(
546 state.mapper, state, set(group), PASSIVE_OFF
547 )
548
549 return LoaderCallableStatus.ATTR_WAS_SET
550
551 def _invoke_raise_load(self, state, passive, lazy):
552 raise sa_exc.InvalidRequestError(
553 "'%s' is not available due to raiseload=True" % (self,)
554 )
555
556
557class LoadDeferredColumns:
558 """serializable loader object used by DeferredColumnLoader"""
559
560 def __init__(self, key: str, raiseload: bool = False):
561 self.key = key
562 self.raiseload = raiseload
563
564 def __call__(self, state, passive=attributes.PASSIVE_OFF):
565 key = self.key
566
567 localparent = state.manager.mapper
568 prop = localparent._props[key]
569 if self.raiseload:
570 strategy_key = (
571 ("deferred", True),
572 ("instrument", True),
573 ("raiseload", True),
574 )
575 else:
576 strategy_key = (("deferred", True), ("instrument", True))
577 strategy = prop._get_strategy(strategy_key)
578 return strategy._load_for_state(state, passive)
579
580
581class AbstractRelationshipLoader(LoaderStrategy):
582 """LoaderStratgies which deal with related objects."""
583
584 __slots__ = "mapper", "target", "uselist", "entity"
585
586 def __init__(self, parent, strategy_key):
587 super().__init__(parent, strategy_key)
588 self.mapper = self.parent_property.mapper
589 self.entity = self.parent_property.entity
590 self.target = self.parent_property.target
591 self.uselist = self.parent_property.uselist
592
593 def _immediateload_create_row_processor(
594 self,
595 context,
596 query_entity,
597 path,
598 loadopt,
599 mapper,
600 result,
601 adapter,
602 populators,
603 ):
604 return self.parent_property._get_strategy(
605 (("lazy", "immediate"),)
606 ).create_row_processor(
607 context,
608 query_entity,
609 path,
610 loadopt,
611 mapper,
612 result,
613 adapter,
614 populators,
615 )
616
617
618@log.class_logger
619@relationships.RelationshipProperty.strategy_for(do_nothing=True)
620class DoNothingLoader(LoaderStrategy):
621 """Relationship loader that makes no change to the object's state.
622
623 Compared to NoLoader, this loader does not initialize the
624 collection/attribute to empty/none; the usual default LazyLoader will
625 take effect.
626
627 """
628
629
630@log.class_logger
631@relationships.RelationshipProperty.strategy_for(lazy="noload")
632@relationships.RelationshipProperty.strategy_for(lazy=None)
633class NoLoader(AbstractRelationshipLoader):
634 """Provide loading behavior for a :class:`.Relationship`
635 with "lazy=None".
636
637 """
638
639 __slots__ = ()
640
641 def init_class_attribute(self, mapper):
642 self.is_class_level = True
643
644 _register_attribute(
645 self.parent_property,
646 mapper,
647 useobject=True,
648 typecallable=self.parent_property.collection_class,
649 )
650
651 def create_row_processor(
652 self,
653 context,
654 query_entity,
655 path,
656 loadopt,
657 mapper,
658 result,
659 adapter,
660 populators,
661 ):
662 def invoke_no_load(state, dict_, row):
663 if self.uselist:
664 attributes.init_state_collection(state, dict_, self.key)
665 else:
666 dict_[self.key] = None
667
668 populators["new"].append((self.key, invoke_no_load))
669
670
671@log.class_logger
672@relationships.RelationshipProperty.strategy_for(lazy=True)
673@relationships.RelationshipProperty.strategy_for(lazy="select")
674@relationships.RelationshipProperty.strategy_for(lazy="raise")
675@relationships.RelationshipProperty.strategy_for(lazy="raise_on_sql")
676@relationships.RelationshipProperty.strategy_for(lazy="baked_select")
677class LazyLoader(
678 AbstractRelationshipLoader, util.MemoizedSlots, log.Identified
679):
680 """Provide loading behavior for a :class:`.Relationship`
681 with "lazy=True", that is loads when first accessed.
682
683 """
684
685 __slots__ = (
686 "_lazywhere",
687 "_rev_lazywhere",
688 "_lazyload_reverse_option",
689 "_order_by",
690 "use_get",
691 "is_aliased_class",
692 "_bind_to_col",
693 "_equated_columns",
694 "_rev_bind_to_col",
695 "_rev_equated_columns",
696 "_simple_lazy_clause",
697 "_raise_always",
698 "_raise_on_sql",
699 )
700
701 _lazywhere: ColumnElement[bool]
702 _bind_to_col: Dict[str, ColumnElement[Any]]
703 _rev_lazywhere: ColumnElement[bool]
704 _rev_bind_to_col: Dict[str, ColumnElement[Any]]
705
706 parent_property: RelationshipProperty[Any]
707
708 def __init__(
709 self, parent: RelationshipProperty[Any], strategy_key: Tuple[Any, ...]
710 ):
711 super().__init__(parent, strategy_key)
712 self._raise_always = self.strategy_opts["lazy"] == "raise"
713 self._raise_on_sql = self.strategy_opts["lazy"] == "raise_on_sql"
714
715 self.is_aliased_class = inspect(self.entity).is_aliased_class
716
717 join_condition = self.parent_property._join_condition
718 (
719 self._lazywhere,
720 self._bind_to_col,
721 self._equated_columns,
722 ) = join_condition.create_lazy_clause()
723
724 (
725 self._rev_lazywhere,
726 self._rev_bind_to_col,
727 self._rev_equated_columns,
728 ) = join_condition.create_lazy_clause(reverse_direction=True)
729
730 if self.parent_property.order_by:
731 self._order_by = [
732 sql_util._deep_annotate(elem, {"_orm_adapt": True})
733 for elem in util.to_list(self.parent_property.order_by)
734 ]
735 else:
736 self._order_by = None
737
738 self.logger.info("%s lazy loading clause %s", self, self._lazywhere)
739
740 # determine if our "lazywhere" clause is the same as the mapper's
741 # get() clause. then we can just use mapper.get()
742 #
743 # TODO: the "not self.uselist" can be taken out entirely; a m2o
744 # load that populates for a list (very unusual, but is possible with
745 # the API) can still set for "None" and the attribute system will
746 # populate as an empty list.
747 self.use_get = (
748 not self.is_aliased_class
749 and not self.uselist
750 and self.entity._get_clause[0].compare(
751 self._lazywhere,
752 use_proxies=True,
753 compare_keys=False,
754 equivalents=self.mapper._equivalent_columns,
755 )
756 )
757
758 if self.use_get:
759 for col in list(self._equated_columns):
760 if col in self.mapper._equivalent_columns:
761 for c in self.mapper._equivalent_columns[col]:
762 self._equated_columns[c] = self._equated_columns[col]
763
764 self.logger.info(
765 "%s will use Session.get() to optimize instance loads", self
766 )
767
768 def init_class_attribute(self, mapper):
769 self.is_class_level = True
770
771 _legacy_inactive_history_style = (
772 self.parent_property._legacy_inactive_history_style
773 )
774
775 if self.parent_property.active_history:
776 active_history = True
777 _deferred_history = False
778
779 elif (
780 self.parent_property.direction is not interfaces.MANYTOONE
781 or not self.use_get
782 ):
783 if _legacy_inactive_history_style:
784 active_history = True
785 _deferred_history = False
786 else:
787 active_history = False
788 _deferred_history = True
789 else:
790 active_history = _deferred_history = False
791
792 _register_attribute(
793 self.parent_property,
794 mapper,
795 useobject=True,
796 callable_=self._load_for_state,
797 typecallable=self.parent_property.collection_class,
798 active_history=active_history,
799 _deferred_history=_deferred_history,
800 )
801
802 def _memoized_attr__simple_lazy_clause(self):
803 lazywhere = sql_util._deep_annotate(
804 self._lazywhere, {"_orm_adapt": True}
805 )
806
807 criterion, bind_to_col = (lazywhere, self._bind_to_col)
808
809 params = []
810
811 def visit_bindparam(bindparam):
812 bindparam.unique = False
813
814 visitors.traverse(criterion, {}, {"bindparam": visit_bindparam})
815
816 def visit_bindparam(bindparam):
817 if bindparam._identifying_key in bind_to_col:
818 params.append(
819 (
820 bindparam.key,
821 bind_to_col[bindparam._identifying_key],
822 None,
823 )
824 )
825 elif bindparam.callable is None:
826 params.append((bindparam.key, None, bindparam.value))
827
828 criterion = visitors.cloned_traverse(
829 criterion, {}, {"bindparam": visit_bindparam}
830 )
831
832 return criterion, params
833
834 def _generate_lazy_clause(self, state, passive):
835 criterion, param_keys = self._simple_lazy_clause
836
837 if state is None:
838 return sql_util.adapt_criterion_to_null(
839 criterion, [key for key, ident, value in param_keys]
840 )
841
842 mapper = self.parent_property.parent
843
844 o = state.obj() # strong ref
845 dict_ = attributes.instance_dict(o)
846
847 if passive & PassiveFlag.INIT_OK:
848 passive ^= PassiveFlag.INIT_OK
849
850 params = {}
851 for key, ident, value in param_keys:
852 if ident is not None:
853 if passive and passive & PassiveFlag.LOAD_AGAINST_COMMITTED:
854 value = mapper._get_committed_state_attr_by_column(
855 state, dict_, ident, passive
856 )
857 else:
858 value = mapper._get_state_attr_by_column(
859 state, dict_, ident, passive
860 )
861
862 params[key] = value
863
864 return criterion, params
865
866 def _invoke_raise_load(self, state, passive, lazy):
867 raise sa_exc.InvalidRequestError(
868 "'%s' is not available due to lazy='%s'" % (self, lazy)
869 )
870
871 def _load_for_state(
872 self,
873 state,
874 passive,
875 loadopt=None,
876 extra_criteria=(),
877 extra_options=(),
878 alternate_effective_path=None,
879 execution_options=util.EMPTY_DICT,
880 ):
881 if not state.key and (
882 (
883 not self.parent_property.load_on_pending
884 and not state._load_pending
885 )
886 or not state.session_id
887 ):
888 return LoaderCallableStatus.ATTR_EMPTY
889
890 pending = not state.key
891 primary_key_identity = None
892
893 use_get = self.use_get and (not loadopt or not loadopt._extra_criteria)
894
895 if (not passive & PassiveFlag.SQL_OK and not use_get) or (
896 not passive & attributes.NON_PERSISTENT_OK and pending
897 ):
898 return LoaderCallableStatus.PASSIVE_NO_RESULT
899
900 if (
901 # we were given lazy="raise"
902 self._raise_always
903 # the no_raise history-related flag was not passed
904 and not passive & PassiveFlag.NO_RAISE
905 and (
906 # if we are use_get and related_object_ok is disabled,
907 # which means we are at most looking in the identity map
908 # for history purposes or otherwise returning
909 # PASSIVE_NO_RESULT, don't raise. This is also a
910 # history-related flag
911 not use_get
912 or passive & PassiveFlag.RELATED_OBJECT_OK
913 )
914 ):
915 self._invoke_raise_load(state, passive, "raise")
916
917 session = _state_session(state)
918 if not session:
919 if passive & PassiveFlag.NO_RAISE:
920 return LoaderCallableStatus.PASSIVE_NO_RESULT
921
922 raise orm_exc.DetachedInstanceError(
923 "Parent instance %s is not bound to a Session; "
924 "lazy load operation of attribute '%s' cannot proceed"
925 % (orm_util.state_str(state), self.key)
926 )
927
928 # if we have a simple primary key load, check the
929 # identity map without generating a Query at all
930 if use_get:
931 primary_key_identity = self._get_ident_for_use_get(
932 session, state, passive
933 )
934 if LoaderCallableStatus.PASSIVE_NO_RESULT in primary_key_identity:
935 return LoaderCallableStatus.PASSIVE_NO_RESULT
936 elif LoaderCallableStatus.NEVER_SET in primary_key_identity:
937 return LoaderCallableStatus.NEVER_SET
938
939 # test for None alone in primary_key_identity based on
940 # allow_partial_pks preference. PASSIVE_NO_RESULT and NEVER_SET
941 # have already been tested above
942 if not self.mapper.allow_partial_pks:
943 if _none_only_set.intersection(primary_key_identity):
944 return None
945 else:
946 if _none_only_set.issuperset(primary_key_identity):
947 return None
948
949 if (
950 self.key in state.dict
951 and not passive & PassiveFlag.DEFERRED_HISTORY_LOAD
952 ):
953 return LoaderCallableStatus.ATTR_WAS_SET
954
955 # look for this identity in the identity map. Delegate to the
956 # Query class in use, as it may have special rules for how it
957 # does this, including how it decides what the correct
958 # identity_token would be for this identity.
959
960 instance = session._identity_lookup(
961 self.entity,
962 primary_key_identity,
963 passive=passive,
964 lazy_loaded_from=state,
965 )
966
967 if instance is not None:
968 if instance is LoaderCallableStatus.PASSIVE_CLASS_MISMATCH:
969 return None
970 else:
971 return instance
972 elif (
973 not passive & PassiveFlag.SQL_OK
974 or not passive & PassiveFlag.RELATED_OBJECT_OK
975 ):
976 return LoaderCallableStatus.PASSIVE_NO_RESULT
977
978 return self._emit_lazyload(
979 session,
980 state,
981 primary_key_identity,
982 passive,
983 loadopt,
984 extra_criteria,
985 extra_options,
986 alternate_effective_path,
987 execution_options,
988 )
989
990 def _get_ident_for_use_get(self, session, state, passive):
991 instance_mapper = state.manager.mapper
992
993 if passive & PassiveFlag.LOAD_AGAINST_COMMITTED:
994 get_attr = instance_mapper._get_committed_state_attr_by_column
995 else:
996 get_attr = instance_mapper._get_state_attr_by_column
997
998 dict_ = state.dict
999
1000 return [
1001 get_attr(state, dict_, self._equated_columns[pk], passive=passive)
1002 for pk in self.mapper.primary_key
1003 ]
1004
1005 @util.preload_module("sqlalchemy.orm.strategy_options")
1006 def _emit_lazyload(
1007 self,
1008 session,
1009 state,
1010 primary_key_identity,
1011 passive,
1012 loadopt,
1013 extra_criteria,
1014 extra_options,
1015 alternate_effective_path,
1016 execution_options,
1017 ):
1018 strategy_options = util.preloaded.orm_strategy_options
1019
1020 clauseelement = self.entity.__clause_element__()
1021 stmt = Select._create_raw_select(
1022 _raw_columns=[clauseelement],
1023 _propagate_attrs=clauseelement._propagate_attrs,
1024 _label_style=LABEL_STYLE_TABLENAME_PLUS_COL,
1025 _compile_options=ORMCompileState.default_compile_options,
1026 )
1027 load_options = QueryContext.default_load_options
1028
1029 load_options += {
1030 "_invoke_all_eagers": False,
1031 "_lazy_loaded_from": state,
1032 }
1033
1034 if self.parent_property.secondary is not None:
1035 stmt = stmt.select_from(
1036 self.mapper, self.parent_property.secondary
1037 )
1038
1039 pending = not state.key
1040
1041 # don't autoflush on pending
1042 if pending or passive & attributes.NO_AUTOFLUSH:
1043 stmt._execution_options = util.immutabledict({"autoflush": False})
1044
1045 use_get = self.use_get
1046
1047 if state.load_options or (loadopt and loadopt._extra_criteria):
1048 if alternate_effective_path is None:
1049 effective_path = state.load_path[self.parent_property]
1050 else:
1051 effective_path = alternate_effective_path[self.parent_property]
1052
1053 opts = state.load_options
1054
1055 if loadopt and loadopt._extra_criteria:
1056 use_get = False
1057 opts += (
1058 orm_util.LoaderCriteriaOption(self.entity, extra_criteria),
1059 )
1060
1061 stmt._with_options = opts
1062 elif alternate_effective_path is None:
1063 # this path is used if there are not already any options
1064 # in the query, but an event may want to add them
1065 effective_path = state.mapper._path_registry[self.parent_property]
1066 else:
1067 # added by immediateloader
1068 effective_path = alternate_effective_path[self.parent_property]
1069
1070 if extra_options:
1071 stmt._with_options += extra_options
1072
1073 stmt._compile_options += {"_current_path": effective_path}
1074
1075 if use_get:
1076 if self._raise_on_sql and not passive & PassiveFlag.NO_RAISE:
1077 self._invoke_raise_load(state, passive, "raise_on_sql")
1078
1079 return loading.load_on_pk_identity(
1080 session,
1081 stmt,
1082 primary_key_identity,
1083 load_options=load_options,
1084 execution_options=execution_options,
1085 )
1086
1087 if self._order_by:
1088 stmt._order_by_clauses = self._order_by
1089
1090 def _lazyload_reverse(compile_context):
1091 for rev in self.parent_property._reverse_property:
1092 # reverse props that are MANYTOONE are loading *this*
1093 # object from get(), so don't need to eager out to those.
1094 if (
1095 rev.direction is interfaces.MANYTOONE
1096 and rev._use_get
1097 and not isinstance(rev.strategy, LazyLoader)
1098 ):
1099 strategy_options.Load._construct_for_existing_path(
1100 compile_context.compile_options._current_path[
1101 rev.parent
1102 ]
1103 ).lazyload(rev).process_compile_state(compile_context)
1104
1105 stmt._with_context_options += (
1106 (_lazyload_reverse, self.parent_property),
1107 )
1108
1109 lazy_clause, params = self._generate_lazy_clause(state, passive)
1110
1111 if execution_options:
1112 execution_options = util.EMPTY_DICT.merge_with(
1113 execution_options,
1114 {
1115 "_sa_orm_load_options": load_options,
1116 },
1117 )
1118 else:
1119 execution_options = {
1120 "_sa_orm_load_options": load_options,
1121 }
1122
1123 if (
1124 self.key in state.dict
1125 and not passive & PassiveFlag.DEFERRED_HISTORY_LOAD
1126 ):
1127 return LoaderCallableStatus.ATTR_WAS_SET
1128
1129 if pending:
1130 if util.has_intersection(orm_util._none_set, params.values()):
1131 return None
1132
1133 elif util.has_intersection(orm_util._never_set, params.values()):
1134 return None
1135
1136 if self._raise_on_sql and not passive & PassiveFlag.NO_RAISE:
1137 self._invoke_raise_load(state, passive, "raise_on_sql")
1138
1139 stmt._where_criteria = (lazy_clause,)
1140
1141 result = session.execute(
1142 stmt, params, execution_options=execution_options
1143 )
1144
1145 result = result.unique().scalars().all()
1146
1147 if self.uselist:
1148 return result
1149 else:
1150 l = len(result)
1151 if l:
1152 if l > 1:
1153 util.warn(
1154 "Multiple rows returned with "
1155 "uselist=False for lazily-loaded attribute '%s' "
1156 % self.parent_property
1157 )
1158
1159 return result[0]
1160 else:
1161 return None
1162
1163 def create_row_processor(
1164 self,
1165 context,
1166 query_entity,
1167 path,
1168 loadopt,
1169 mapper,
1170 result,
1171 adapter,
1172 populators,
1173 ):
1174 key = self.key
1175
1176 if (
1177 context.load_options._is_user_refresh
1178 and context.query._compile_options._only_load_props
1179 and self.key in context.query._compile_options._only_load_props
1180 ):
1181 return self._immediateload_create_row_processor(
1182 context,
1183 query_entity,
1184 path,
1185 loadopt,
1186 mapper,
1187 result,
1188 adapter,
1189 populators,
1190 )
1191
1192 if not self.is_class_level or (loadopt and loadopt._extra_criteria):
1193 # we are not the primary manager for this attribute
1194 # on this class - set up a
1195 # per-instance lazyloader, which will override the
1196 # class-level behavior.
1197 # this currently only happens when using a
1198 # "lazyload" option on a "no load"
1199 # attribute - "eager" attributes always have a
1200 # class-level lazyloader installed.
