summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy/orm/strategies.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/sqlalchemy/orm/strategies.py')
-rw-r--r--lib/sqlalchemy/orm/strategies.py263
1 files changed, 215 insertions, 48 deletions
diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py
index c4c0fb180..db9dcffdc 100644
--- a/lib/sqlalchemy/orm/strategies.py
+++ b/lib/sqlalchemy/orm/strategies.py
@@ -43,6 +43,7 @@ from .interfaces import LoaderStrategy
from .interfaces import StrategizedProperty
from .session import _state_session
from .state import InstanceState
+from .strategy_options import Load
from .util import _none_set
from .util import AliasedClass
from .. import event
@@ -830,7 +831,16 @@ class LazyLoader(
"'%s' is not available due to lazy='%s'" % (self, lazy)
)
- def _load_for_state(self, state, passive, loadopt=None, extra_criteria=()):
+ def _load_for_state(
+ self,
+ state,
+ passive,
+ loadopt=None,
+ extra_criteria=(),
+ extra_options=(),
+ alternate_effective_path=None,
+ execution_options=util.EMPTY_DICT,
+ ):
if not state.key and (
(
not self.parent_property.load_on_pending
@@ -929,6 +939,9 @@ class LazyLoader(
passive,
loadopt,
extra_criteria,
+ extra_options,
+ alternate_effective_path,
+ execution_options,
)
def _get_ident_for_use_get(self, session, state, passive):
@@ -955,6 +968,9 @@ class LazyLoader(
passive,
loadopt,
extra_criteria,
+ extra_options,
+ alternate_effective_path,
+ execution_options,
):
strategy_options = util.preloaded.orm_strategy_options
@@ -986,7 +1002,10 @@ class LazyLoader(
use_get = self.use_get
if state.load_options or (loadopt and loadopt._extra_criteria):
- effective_path = state.load_path[self.parent_property]
+ if alternate_effective_path is None:
+ effective_path = state.load_path[self.parent_property]
+ else:
+ effective_path = alternate_effective_path[self.parent_property]
opts = state.load_options
@@ -997,10 +1016,16 @@ class LazyLoader(
)
stmt._with_options = opts
- else:
+ elif alternate_effective_path is None:
# this path is used if there are not already any options
# in the query, but an event may want to add them
effective_path = state.mapper._path_registry[self.parent_property]
+ else:
+ # added by immediateloader
+ effective_path = alternate_effective_path[self.parent_property]
+
+ if extra_options:
+ stmt._with_options += extra_options
stmt._compile_options += {"_current_path": effective_path}
@@ -1009,7 +1034,11 @@ class LazyLoader(
self._invoke_raise_load(state, passive, "raise_on_sql")
return loading.load_on_pk_identity(
- session, stmt, primary_key_identity, load_options=load_options
+ session,
+ stmt,
+ primary_key_identity,
+ load_options=load_options,
+ execution_options=execution_options,
)
if self._order_by:
@@ -1036,9 +1065,18 @@ class LazyLoader(
lazy_clause, params = self._generate_lazy_clause(state, passive)
- execution_options = {
- "_sa_orm_load_options": load_options,
- }
+ if execution_options:
+
+ execution_options = util.EMPTY_DICT.merge_with(
+ execution_options,
+ {
+ "_sa_orm_load_options": load_options,
+ },
+ )
+ else:
+ execution_options = {
+ "_sa_orm_load_options": load_options,
+ }
if (
self.key in state.dict
@@ -1191,15 +1229,54 @@ class PostLoader(AbstractRelationshipLoader):
__slots__ = ()
- def _check_recursive_postload(self, context, path, join_depth=None):
+ def _setup_for_recursion(self, context, path, loadopt, join_depth=None):
+
effective_path = (
context.compile_state.current_path or orm_util.PathRegistry.root
) + path
+ top_level_context = context._get_top_level_context()
+ execution_options = util.immutabledict(
+ {"sa_top_level_orm_context": top_level_context}
+ )
+
+ if loadopt:
+ recursion_depth = loadopt.local_opts.get("recursion_depth", None)
+ unlimited_recursion = recursion_depth == -1
+ else:
+ recursion_depth = None
+ unlimited_recursion = False
+
+ if recursion_depth is not None:
+ if not self.parent_property._is_self_referential:
+ raise sa_exc.InvalidRequestError(
+ f"recursion_depth option on relationship "
+ f"{self.parent_property} not valid for "
+ "non-self-referential relationship"
+ )
+ recursion_depth = context.execution_options.get(
+ f"_recursion_depth_{id(self)}", recursion_depth
+ )
+
+ if not unlimited_recursion and recursion_depth < 0:
+ return (
+ effective_path,
+ False,
+ execution_options,
+ recursion_depth,
+ )
+
+ if not unlimited_recursion:
+ execution_options = execution_options.union(
+ {
+ f"_recursion_depth_{id(self)}": recursion_depth - 1,
+ }
+ )
+
if loading.PostLoad.path_exists(
context, effective_path, self.parent_property
):
- return True
+ return effective_path, False, execution_options, recursion_depth
path_w_prop = path[self.parent_property]
effective_path_w_prop = effective_path[self.parent_property]
@@ -1207,11 +1284,21 @@ class PostLoader(AbstractRelationshipLoader):
if not path_w_prop.contains(context.attributes, "loader"):
if join_depth:
if effective_path_w_prop.length / 2 > join_depth:
- return True
+ return (
+ effective_path,
+ False,
+ execution_options,
+ recursion_depth,
+ )
elif effective_path_w_prop.contains_mapper(self.mapper):
- return True
+ return (
+ effective_path,
+ False,
+ execution_options,
+ recursion_depth,
+ )
- return False
+ return effective_path, True, execution_options, recursion_depth
def _immediateload_create_row_processor(
self,
@@ -1258,10 +1345,14 @@ class ImmediateLoader(PostLoader):
adapter,
populators,
):
- def load_immediate(state, dict_, row):
- state.get_impl(self.key).get(state, dict_, flags)
- if self._check_recursive_postload(context, path):
+ (
+ effective_path,
+ run_loader,
+ execution_options,
+ recursion_depth,
+ ) = self._setup_for_recursion(context, path, loadopt)
+ if not run_loader:
# this will not emit SQL and will only emit for a many-to-one
# "use get" load. the "_RELATED" part means it may return
# instance even if its expired, since this is a mutually-recursive
@@ -1270,7 +1361,57 @@ class ImmediateLoader(PostLoader):
else:
flags = attributes.PASSIVE_OFF | PassiveFlag.NO_RAISE
- populators["delayed"].append((self.key, load_immediate))
+ loading.PostLoad.callable_for_path(
+ context,
+ effective_path,
+ self.parent,
+ self.parent_property,
+ self._load_for_path,
+ loadopt,
+ flags,
+ recursion_depth,
+ execution_options,
+ )
+
+ def _load_for_path(
+ self,
+ context,
+ path,
+ states,
+ load_only,
+ loadopt,
+ flags,
+ recursion_depth,
+ execution_options,
+ ):
+
+ if recursion_depth:
+ new_opt = Load(loadopt.path.entity)
+ new_opt.context = (
+ loadopt,
+ loadopt._recurse(),
+ )
+ alternate_effective_path = path._truncate_recursive()
+ extra_options = (new_opt,)
+ else:
+ new_opt = None
+ alternate_effective_path = path
+ extra_options = ()
+
+ key = self.key
+ lazyloader = self.parent_property._get_strategy((("lazy", "select"),))
+ for state, overwrite in states:
+ dict_ = state.dict
+
+ if overwrite or key not in dict_:
+ value = lazyloader._load_for_state(
+ state,
+ flags,
+ extra_options=extra_options,
+ alternate_effective_path=alternate_effective_path,
+ execution_options=execution_options,
+ )
+ state.get_impl(key).set_committed_value(state, dict_, value)
@log.class_logger
@@ -1677,24 +1818,6 @@ class SubqueryLoader(PostLoader):
subq_path = subq_path + path
rewritten_path = rewritten_path + path
- # if not via query option, check for
- # a cycle
- # TODO: why is this here??? this is now handled
- # by the _check_recursive_postload call
- if not path.contains(compile_state.attributes, "loader"):
- if self.join_depth:
- if (
- (
- compile_state.current_path.length
- if compile_state.current_path
- else 0
- )
- + path.length
- ) / 2 > self.join_depth:
- return
- elif subq_path.contains_mapper(self.mapper):
- return
-
# use the current query being invoked, not the compile state
# one. this is so that we get the current parameters. however,
# it means we can't use the existing compile state, we have to make
@@ -1814,11 +1937,14 @@ class SubqueryLoader(PostLoader):
adapter,
populators,
)
- # the subqueryloader does a similar check in setup_query() unlike
- # the other post loaders, however we have this here for consistency
- elif self._check_recursive_postload(context, path, self.join_depth):
+
+ _, run_loader, _, _ = self._setup_for_recursion(
+ context, path, loadopt, self.join_depth
+ )
+ if not run_loader:
return
- elif not isinstance(context.compile_state, ORMSelectCompileState):
+
+ if not isinstance(context.compile_state, ORMSelectCompileState):
# issue 7505 - subqueryload() in 1.3 and previous would silently
# degrade for from_statement() without warning. this behavior
# is restored here
@@ -2787,7 +2913,16 @@ class SelectInLoader(PostLoader, util.MemoizedSlots):
adapter,
populators,
)
- elif self._check_recursive_postload(context, path, self.join_depth):
+
+ (
+ effective_path,
+ run_loader,
+ execution_options,
+ recursion_depth,
+ ) = self._setup_for_recursion(
+ context, path, loadopt, join_depth=self.join_depth
+ )
+ if not run_loader:
return
if not self.parent.class_manager[self.key].impl.supports_population:
@@ -2806,9 +2941,7 @@ class SelectInLoader(PostLoader, util.MemoizedSlots):
elif not orm_util._entity_isa(path[-1], self.parent):
return
- selectin_path = (
- context.compile_state.current_path or orm_util.PathRegistry.root
- ) + path
+ selectin_path = effective_path
path_w_prop = path[self.parent_property]
@@ -2830,10 +2963,20 @@ class SelectInLoader(PostLoader, util.MemoizedSlots):
self._load_for_path,
effective_entity,
loadopt,
+ recursion_depth,
+ execution_options,
)
def _load_for_path(
- self, context, path, states, load_only, effective_entity, loadopt
+ self,
+ context,
+ path,
+ states,
+ load_only,
+ effective_entity,
+ loadopt,
+ recursion_depth,
+ execution_options,
):
if load_only and self.key not in load_only:
return
@@ -3003,9 +3146,13 @@ class SelectInLoader(PostLoader, util.MemoizedSlots):
),
)
+ if recursion_depth is not None:
+ effective_path = effective_path._truncate_recursive()
+
q = q.options(*new_options)._update_compile_options(
{"_current_path": effective_path}
)
+
if user_defined_options:
q = q.options(*user_defined_options)
@@ -3034,12 +3181,27 @@ class SelectInLoader(PostLoader, util.MemoizedSlots):
if query_info.load_only_child:
self._load_via_child(
- our_states, none_states, query_info, q, context
+ our_states,
+ none_states,
+ query_info,
+ q,
+ context,
+ execution_options,
)
else:
- self._load_via_parent(our_states, query_info, q, context)
+ self._load_via_parent(
+ our_states, query_info, q, context, execution_options
+ )
- def _load_via_child(self, our_states, none_states, query_info, q, context):
+ def _load_via_child(
+ self,
+ our_states,
+ none_states,
+ query_info,
+ q,
+ context,
+ execution_options,
+ ):
uselist = self.uselist
# this sort is really for the benefit of the unit tests
@@ -3057,6 +3219,7 @@ class SelectInLoader(PostLoader, util.MemoizedSlots):
for key in chunk
]
},
+ execution_options=execution_options,
).unique()
}
@@ -3085,7 +3248,9 @@ class SelectInLoader(PostLoader, util.MemoizedSlots):
# collection will be populated
state.get_impl(self.key).set_committed_value(state, dict_, None)
- def _load_via_parent(self, our_states, query_info, q, context):
+ def _load_via_parent(
+ self, our_states, query_info, q, context, execution_options
+ ):
uselist = self.uselist
_empty_result = () if uselist else None
@@ -3101,7 +3266,9 @@ class SelectInLoader(PostLoader, util.MemoizedSlots):
data = collections.defaultdict(list)
for k, v in itertools.groupby(
context.session.execute(
- q, params={"primary_keys": primary_keys}
+ q,
+ params={"primary_keys": primary_keys},
+ execution_options=execution_options,
).unique(),
lambda x: x[0],
):