summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy/orm
diff options
context:
space:
mode:
Diffstat (limited to 'lib/sqlalchemy/orm')
-rw-r--r--lib/sqlalchemy/orm/attributes.py30
-rw-r--r--lib/sqlalchemy/orm/context.py37
-rw-r--r--lib/sqlalchemy/orm/descriptor_props.py1
-rw-r--r--lib/sqlalchemy/orm/interfaces.py32
-rw-r--r--lib/sqlalchemy/orm/loading.py11
-rw-r--r--lib/sqlalchemy/orm/mapper.py2
-rw-r--r--lib/sqlalchemy/orm/path_registry.py8
-rw-r--r--lib/sqlalchemy/orm/persistence.py9
-rw-r--r--lib/sqlalchemy/orm/properties.py1
-rw-r--r--lib/sqlalchemy/orm/query.py20
-rw-r--r--lib/sqlalchemy/orm/relationships.py1
-rw-r--r--lib/sqlalchemy/orm/strategies.py500
-rw-r--r--lib/sqlalchemy/orm/util.py8
13 files changed, 455 insertions, 205 deletions
diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py
index 262a1efc9..bf07061c6 100644
--- a/lib/sqlalchemy/orm/attributes.py
+++ b/lib/sqlalchemy/orm/attributes.py
@@ -85,16 +85,16 @@ class QueryableAttribute(
self,
class_,
key,
+ parententity,
impl=None,
comparator=None,
- parententity=None,
of_type=None,
):
self.class_ = class_
self.key = key
+ self._parententity = parententity
self.impl = impl
self.comparator = comparator
- self._parententity = parententity
self._of_type = of_type
manager = manager_of_class(class_)
@@ -197,10 +197,14 @@ class QueryableAttribute(
@util.memoized_property
def expression(self):
return self.comparator.__clause_element__()._annotate(
- {"orm_key": self.key}
+ {"orm_key": self.key, "entity_namespace": self._entity_namespace}
)
@property
+ def _entity_namespace(self):
+ return self._parententity
+
+ @property
def _annotations(self):
return self.__clause_element__()._annotations
@@ -230,9 +234,9 @@ class QueryableAttribute(
return QueryableAttribute(
self.class_,
self.key,
- self.impl,
- self.comparator.of_type(entity),
self._parententity,
+ impl=self.impl,
+ comparator=self.comparator.of_type(entity),
of_type=inspection.inspect(entity),
)
@@ -301,6 +305,8 @@ class InstrumentedAttribute(QueryableAttribute):
"""
+ inherit_cache = True
+
def __set__(self, instance, value):
self.impl.set(
instance_state(instance), instance_dict(instance), value, None
@@ -320,6 +326,11 @@ class InstrumentedAttribute(QueryableAttribute):
return self.impl.get(instance_state(instance), dict_)
+HasEntityNamespace = util.namedtuple(
+ "HasEntityNamespace", ["entity_namespace"]
+)
+
+
def create_proxied_attribute(descriptor):
"""Create an QueryableAttribute / user descriptor hybrid.
@@ -365,6 +376,15 @@ def create_proxied_attribute(descriptor):
)
@property
+ def _entity_namespace(self):
+ if hasattr(self._comparator, "_parententity"):
+ return self._comparator._parententity
+ else:
+ # used by hybrid attributes which try to remain
+ # agnostic of any ORM concepts like mappers
+ return HasEntityNamespace(self.class_)
+
+ @property
def property(self):
return self.comparator.property
diff --git a/lib/sqlalchemy/orm/context.py b/lib/sqlalchemy/orm/context.py
index a16db66f6..588b83571 100644
--- a/lib/sqlalchemy/orm/context.py
+++ b/lib/sqlalchemy/orm/context.py
@@ -63,6 +63,8 @@ class QueryContext(object):
"post_load_paths",
"identity_token",
"yield_per",
+ "loaders_require_buffering",
+ "loaders_require_uniquing",
)
class default_load_options(Options):
@@ -80,21 +82,23 @@ class QueryContext(object):
def __init__(
self,
compile_state,
+ statement,
session,
load_options,
execution_options=None,
bind_arguments=None,
):
-
self.load_options = load_options
self.execution_options = execution_options or _EMPTY_DICT
self.bind_arguments = bind_arguments or _EMPTY_DICT
self.compile_state = compile_state
- self.query = query = compile_state.select_statement
+ self.query = statement
self.session = session
+ self.loaders_require_buffering = False
+ self.loaders_require_uniquing = False
self.propagated_loader_options = {
- o for o in query._with_options if o.propagate_to_loaders
+ o for o in statement._with_options if o.propagate_to_loaders
}
self.attributes = dict(compile_state.attributes)
@@ -237,6 +241,7 @@ class ORMCompileState(CompileState):
)
querycontext = QueryContext(
compile_state,
+ statement,
session,
load_options,
execution_options,
@@ -278,8 +283,6 @@ class ORMFromStatementCompileState(ORMCompileState):
_has_orm_entities = False
multi_row_eager_loaders = False
compound_eager_adapter = None
- loaders_require_buffering = False
- loaders_require_uniquing = False
@classmethod
def create_for_statement(cls, statement_container, compiler, **kw):
@@ -386,8 +389,6 @@ class ORMSelectCompileState(ORMCompileState, SelectState):
_has_orm_entities = False
multi_row_eager_loaders = False
compound_eager_adapter = None
- loaders_require_buffering = False
- loaders_require_uniquing = False
correlate = None
_where_criteria = ()
@@ -416,7 +417,14 @@ class ORMSelectCompileState(ORMCompileState, SelectState):
self = cls.__new__(cls)
- self.select_statement = select_statement
+ if select_statement._execution_options:
+ # execution options should not impact the compilation of a
+ # query, and at the moment subqueryloader is putting some things
+ # in here that we explicitly don't want stuck in a cache.
+ self.select_statement = select_statement._clone()
+ self.select_statement._execution_options = util.immutabledict()
+ else:
+ self.select_statement = select_statement
# indicates this select() came from Query.statement
self.for_statement = (
@@ -654,6 +662,8 @@ class ORMSelectCompileState(ORMCompileState, SelectState):
)
self._setup_with_polymorphics()
+ # entities will also set up polymorphic adapters for mappers
+ # that have with_polymorphic configured
_QueryEntity.to_compile_state(self, query._raw_columns)
return self
@@ -1810,10 +1820,12 @@ class ORMSelectCompileState(ORMCompileState, SelectState):
self._where_criteria += (single_crit,)
-def _column_descriptions(query_or_select_stmt):
- ctx = ORMSelectCompileState._create_entities_collection(
- query_or_select_stmt
- )
+def _column_descriptions(query_or_select_stmt, compile_state=None):
+ if compile_state is None:
+ compile_state = ORMSelectCompileState._create_entities_collection(
+ query_or_select_stmt
+ )
+ ctx = compile_state
return [
{
"name": ent._label_name,
@@ -2097,6 +2109,7 @@ class _MapperEntity(_QueryEntity):
only_load_props = refresh_state = None
_instance = loading._instance_processor(
+ self,
self.mapper,
context,
result,
diff --git a/lib/sqlalchemy/orm/descriptor_props.py b/lib/sqlalchemy/orm/descriptor_props.py
index 027f2521b..39cf86e34 100644
--- a/lib/sqlalchemy/orm/descriptor_props.py
+++ b/lib/sqlalchemy/orm/descriptor_props.py
@@ -411,7 +411,6 @@ class CompositeProperty(DescriptorProperty):
def expression(self):
clauses = self.clauses._annotate(
{
- "bundle": True,
"parententity": self._parententity,
"parentmapper": self._parententity,
"orm_key": self.prop.key,
diff --git a/lib/sqlalchemy/orm/interfaces.py b/lib/sqlalchemy/orm/interfaces.py
index 6c0f5d3ef..9782d92b7 100644
--- a/lib/sqlalchemy/orm/interfaces.py
+++ b/lib/sqlalchemy/orm/interfaces.py
@@ -158,7 +158,7 @@ class MapperProperty(
"""
def create_row_processor(
- self, context, path, mapper, result, adapter, populators
+ self, context, query_entity, path, mapper, result, adapter, populators
):
"""Produce row processing functions and append to the given
set of populators lists.
@@ -539,7 +539,7 @@ class StrategizedProperty(MapperProperty):
"_wildcard_token",
"_default_path_loader_key",
)
-
+ inherit_cache = True
strategy_wildcard_key = None
def _memoized_attr__wildcard_token(self):
@@ -600,7 +600,7 @@ class StrategizedProperty(MapperProperty):
)
def create_row_processor(
- self, context, path, mapper, result, adapter, populators
+ self, context, query_entity, path, mapper, result, adapter, populators
):
loader = self._get_context_loader(context, path)
if loader and loader.strategy:
@@ -608,7 +608,14 @@ class StrategizedProperty(MapperProperty):
else:
strat = self.strategy
strat.create_row_processor(
- context, path, loader, mapper, result, adapter, populators
+ context,
+ query_entity,
+ path,
+ loader,
+ mapper,
+ result,
+ adapter,
+ populators,
)
def do_init(self):
@@ -668,7 +675,7 @@ class StrategizedProperty(MapperProperty):
)
-class ORMOption(object):
+class ORMOption(HasCacheKey):
"""Base class for option objects that are passed to ORM queries.
These options may be consumed by :meth:`.Query.options`,
@@ -696,7 +703,7 @@ class ORMOption(object):
_is_compile_state = False
-class LoaderOption(HasCacheKey, ORMOption):
+class LoaderOption(ORMOption):
"""Describe a loader modification to an ORM statement at compilation time.
.. versionadded:: 1.4
@@ -736,9 +743,6 @@ class UserDefinedOption(ORMOption):
def __init__(self, payload=None):
self.payload = payload
- def _gen_cache_key(self, *arg, **kw):
- return ()
-
@util.deprecated_cls(
"1.4",
@@ -855,7 +859,15 @@ class LoaderStrategy(object):
"""
def create_row_processor(
- self, context, path, loadopt, mapper, result, adapter, populators
+ self,
+ context,
+ query_entity,
+ path,
+ loadopt,
+ mapper,
+ result,
+ adapter,
+ populators,
):
"""Establish row processing functions for a given QueryContext.
diff --git a/lib/sqlalchemy/orm/loading.py b/lib/sqlalchemy/orm/loading.py
index 424ed5dfe..a33e1b77d 100644
--- a/lib/sqlalchemy/orm/loading.py
+++ b/lib/sqlalchemy/orm/loading.py
@@ -72,8 +72,8 @@ def instances(cursor, context):
)
if context.yield_per and (
- context.compile_state.loaders_require_buffering
- or context.compile_state.loaders_require_uniquing
+ context.loaders_require_buffering
+ or context.loaders_require_uniquing
):
raise sa_exc.InvalidRequestError(
"Can't use yield_per with eager loaders that require uniquing "
@@ -545,6 +545,7 @@ def _warn_for_runid_changed(state):
def _instance_processor(
+ query_entity,
mapper,
context,
result,
@@ -648,6 +649,7 @@ def _instance_processor(
# to see if one fits
prop.create_row_processor(
context,
+ query_entity,
path,
mapper,
result,
@@ -667,7 +669,7 @@ def _instance_processor(
populators = {key: list(value) for key, value in cached_populators.items()}
for prop in getters["todo"]:
prop.create_row_processor(
- context, path, mapper, result, adapter, populators
+ context, query_entity, path, mapper, result, adapter, populators
)
propagated_loader_options = context.propagated_loader_options
@@ -925,6 +927,7 @@ def _instance_processor(
_instance = _decorate_polymorphic_switch(
_instance,
context,
+ query_entity,
mapper,
result,
path,
@@ -1081,6 +1084,7 @@ def _validate_version_id(mapper, state, dict_, row, getter):
def _decorate_polymorphic_switch(
instance_fn,
context,
+ query_entity,
mapper,
result,
path,
@@ -1112,6 +1116,7 @@ def _decorate_polymorphic_switch(
return False
return _instance_processor(
+ query_entity,
sub_mapper,
context,
result,
diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py
index c4cb89c03..bec6da74d 100644
--- a/lib/sqlalchemy/orm/mapper.py
+++ b/lib/sqlalchemy/orm/mapper.py
@@ -720,7 +720,7 @@ class Mapper(
return self
_cache_key_traversal = [
- ("class_", visitors.ExtendedInternalTraversal.dp_plain_obj)
+ ("mapper", visitors.ExtendedInternalTraversal.dp_plain_obj),
]
@property
diff --git a/lib/sqlalchemy/orm/path_registry.py b/lib/sqlalchemy/orm/path_registry.py
index 2e5941713..ac7a64c30 100644
--- a/lib/sqlalchemy/orm/path_registry.py
+++ b/lib/sqlalchemy/orm/path_registry.py
@@ -216,6 +216,8 @@ class RootRegistry(PathRegistry):
"""
+ inherit_cache = True
+
path = natural_path = ()
has_entity = False
is_aliased_class = False
@@ -248,6 +250,8 @@ class PathToken(HasCacheKey, str):
class TokenRegistry(PathRegistry):
__slots__ = ("token", "parent", "path", "natural_path")
+ inherit_cache = True
+
def __init__(self, parent, token):
token = PathToken.intern(token)
@@ -280,6 +284,7 @@ class TokenRegistry(PathRegistry):
class PropRegistry(PathRegistry):
is_unnatural = False
+ inherit_cache = True
def __init__(self, parent, prop):
# restate this path in terms of the
@@ -439,6 +444,7 @@ class AbstractEntityRegistry(PathRegistry):
class SlotsEntityRegistry(AbstractEntityRegistry):
# for aliased class, return lightweight, no-cycles created
# version
+ inherit_cache = True
__slots__ = (
"key",
@@ -454,6 +460,8 @@ class CachingEntityRegistry(AbstractEntityRegistry, dict):
# for long lived mapper, return dict based caching
# version that creates reference cycles
+ inherit_cache = True
+
def __getitem__(self, entity):
if isinstance(entity, (int, slice)):
return self.path[entity]
diff --git a/lib/sqlalchemy/orm/persistence.py b/lib/sqlalchemy/orm/persistence.py
index 19d43d354..8393eaf74 100644
--- a/lib/sqlalchemy/orm/persistence.py
+++ b/lib/sqlalchemy/orm/persistence.py
@@ -38,6 +38,7 @@ from ..sql.base import Options
from ..sql.dml import DeleteDMLState
from ..sql.dml import UpdateDMLState
from ..sql.elements import BooleanClauseList
+from ..sql.util import _entity_namespace_key
def _bulk_insert(
@@ -1820,8 +1821,12 @@ class BulkUDCompileState(CompileState):
if isinstance(k, util.string_types):
desc = sql.util._entity_namespace_key(mapper, k)
values.extend(desc._bulk_update_tuples(v))
- elif isinstance(k, attributes.QueryableAttribute):
- values.extend(k._bulk_update_tuples(v))
+ elif "entity_namespace" in k._annotations:
+ k_anno = k._annotations
+ attr = _entity_namespace_key(
+ k_anno["entity_namespace"], k_anno["orm_key"]
+ )
+ values.extend(attr._bulk_update_tuples(v))
else:
values.append((k, v))
else:
diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py
index 02f0752a5..5fb3beca3 100644
--- a/lib/sqlalchemy/orm/properties.py
+++ b/lib/sqlalchemy/orm/properties.py
@@ -45,6 +45,7 @@ class ColumnProperty(StrategizedProperty):
"""
strategy_wildcard_key = "column"
+ inherit_cache = True
__slots__ = (
"_orig_columns",
diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py
index 284ea9d72..cdad55320 100644
--- a/lib/sqlalchemy/orm/query.py
+++ b/lib/sqlalchemy/orm/query.py
@@ -61,6 +61,7 @@ from ..sql.selectable import LABEL_STYLE_NONE
from ..sql.selectable import LABEL_STYLE_TABLENAME_PLUS_COL
from ..sql.selectable import SelectStatementGrouping
from ..sql.util import _entity_namespace_key
+from ..sql.visitors import InternalTraversal
from ..util import collections_abc
__all__ = ["Query", "QueryContext", "aliased"]
@@ -423,6 +424,7 @@ class Query(
_label_style=self._label_style,
compile_options=compile_options,
)
+ stmt.__dict__.pop("session", None)
stmt._propagate_attrs = self._propagate_attrs
return stmt
@@ -1725,7 +1727,6 @@ class Query(
"""
from_entity = self._filter_by_zero()
-
if from_entity is None:
raise sa_exc.InvalidRequestError(
"Can't use filter_by when the first entity '%s' of a query "
@@ -2900,7 +2901,10 @@ class Query(
compile_state = self._compile_state(for_statement=False)
context = QueryContext(
- compile_state, self.session, self.load_options
+ compile_state,
+ compile_state.statement,
+ self.session,
+ self.load_options,
)
result = loading.instances(result_proxy, context)
@@ -3376,7 +3380,12 @@ class Query(
def _compile_context(self, for_statement=False):
compile_state = self._compile_state(for_statement=for_statement)
- context = QueryContext(compile_state, self.session, self.load_options)
+ context = QueryContext(
+ compile_state,
+ compile_state.statement,
+ self.session,
+ self.load_options,
+ )
return context
@@ -3397,6 +3406,11 @@ class FromStatement(SelectStatementGrouping, Executable):
_for_update_arg = None
+ _traverse_internals = [
+ ("_raw_columns", InternalTraversal.dp_clauseelement_list),
+ ("element", InternalTraversal.dp_clauseelement),
+ ] + Executable._executable_traverse_internals
+
def __init__(self, entities, element):
self._raw_columns = [
coercions.expect(
diff --git a/lib/sqlalchemy/orm/relationships.py b/lib/sqlalchemy/orm/relationships.py
index 683f2b978..bedc54153 100644
--- a/lib/sqlalchemy/orm/relationships.py
+++ b/lib/sqlalchemy/orm/relationships.py
@@ -107,6 +107,7 @@ class RelationshipProperty(StrategizedProperty):
"""
strategy_wildcard_key = "relationship"
+ inherit_cache = True
_persistence_only = dict(
passive_deletes=False,
diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py
index f67c23aab..5f039aff7 100644
--- a/lib/sqlalchemy/orm/strategies.py
+++ b/lib/sqlalchemy/orm/strategies.py
@@ -25,6 +25,7 @@ from .base import _DEFER_FOR_STATE
from .base import _RAISE_FOR_STATE
from .base import _SET_DEFERRED_EXPIRED
from .context import _column_descriptions
+from .context import ORMCompileState
from .interfaces import LoaderStrategy
from .interfaces import StrategizedProperty
from .session import _state_session
@@ -156,7 +157,15 @@ class UninstrumentedColumnLoader(LoaderStrategy):
column_collection.append(c)
def create_row_processor(
- self, context, path, loadopt, mapper, result, adapter, populators
+ self,
+ context,
+ query_entity,
+ path,
+ loadopt,
+ mapper,
+ result,
+ adapter,
+ populators,
):
pass
@@ -224,7 +233,15 @@ class ColumnLoader(LoaderStrategy):
)
def create_row_processor(
- self, context, path, loadopt, mapper, result, adapter, populators
+ self,
+ context,
+ query_entity,
+ path,
+ loadopt,
+ mapper,
+ result,
+ adapter,
+ populators,
):
# look through list of columns represented here
# to see which, if any, is present in the row.
@@ -281,7 +298,15 @@ class ExpressionColumnLoader(ColumnLoader):
memoized_populators[self.parent_property] = fetch
def create_row_processor(
- self, context, path, loadopt, mapper, result, adapter, populators
+ self,
+ context,
+ query_entity,
+ path,
+ loadopt,
+ mapper,
+ result,
+ adapter,
+ populators,
):
# look through list of columns represented here
# to see which, if any, is present in the row.
@@ -332,7 +357,15 @@ class DeferredColumnLoader(LoaderStrategy):
self.group = self.parent_property.group
def create_row_processor(
- self, context, path, loadopt, mapper, result, adapter, populators
+ self,
+ context,
+ query_entity,
+ path,
+ loadopt,
+ mapper,
+ result,
+ adapter,
+ populators,
):
# for a DeferredColumnLoader, this method is only used during a
@@ -542,7 +575,15 @@ class NoLoader(AbstractRelationshipLoader):
)
def create_row_processor(
- self, context, path, loadopt, mapper, result, adapter, populators
+ self,
+ context,
+ query_entity,
+ path,
+ loadopt,
+ mapper,
+ result,
+ adapter,
+ populators,
):
def invoke_no_load(state, dict_, row):
if self.uselist:
@@ -985,7 +1026,15 @@ class LazyLoader(AbstractRelationshipLoader, util.MemoizedSlots):
return None
def create_row_processor(
- self, context, path, loadopt, mapper, result, adapter, populators
+ self,
+ context,
+ query_entity,
+ path,
+ loadopt,
+ mapper,
+ result,
+ adapter,
+ populators,
):
key = self.key
@@ -1039,12 +1088,27 @@ class PostLoader(AbstractRelationshipLoader):
"""A relationship loader that emits a second SELECT statement."""
def _immediateload_create_row_processor(
- self, context, path, loadopt, mapper, result, adapter, populators
+ self,
+ context,
+ query_entity,
+ path,
+ loadopt,
+ mapper,
+ result,
+ adapter,
+ populators,
):
return self.parent_property._get_strategy(
(("lazy", "immediate"),)
).create_row_processor(
- context, path, loadopt, mapper, result, adapter, populators
+ context,
+ query_entity,
+ path,
+ loadopt,
+ mapper,
+ result,
+ adapter,
+ populators,
)
@@ -1057,21 +1121,16 @@ class ImmediateLoader(PostLoader):
(("lazy", "select"),)
).init_class_attribute(mapper)
- def setup_query(
+ def create_row_processor(
self,
- compile_state,
- entity,
+ context,
+ query_entity,
path,
loadopt,
+ mapper,
+ result,
adapter,
- column_collection=None,
- parentmapper=None,
- **kwargs
- ):
- pass
-
- def create_row_processor(
- self, context, path, loadopt, mapper, result, adapter, populators
+ populators,
):
def load_immediate(state, dict_, row):
state.get_impl(self.key).get(state, dict_)
@@ -1093,120 +1152,6 @@ class SubqueryLoader(PostLoader):
(("lazy", "select"),)
).init_class_attribute(mapper)
- def setup_query(
- self,
- compile_state,
- entity,
- path,
- loadopt,
- adapter,
- column_collection=None,
- parentmapper=None,
- **kwargs
- ):
- if (
- not compile_state.compile_options._enable_eagerloads
- or compile_state.compile_options._for_refresh_state
- ):
- return
-
- compile_state.loaders_require_buffering = True
-
- path = path[self.parent_property]
-
- # build up a path indicating the path from the leftmost
- # entity to the thing we're subquery loading.
- with_poly_entity = path.get(
- compile_state.attributes, "path_with_polymorphic", None
- )
- if with_poly_entity is not None:
- effective_entity = with_poly_entity
- else:
- effective_entity = self.entity
-
- subq_path = compile_state.attributes.get(
- ("subquery_path", None), orm_util.PathRegistry.root
- )
-
- subq_path = subq_path + path
-
- # if not via query option, check for
- # a cycle
- 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
-
- (
- leftmost_mapper,
- leftmost_attr,
- leftmost_relationship,
- ) = self._get_leftmost(subq_path)
-
- orig_query = compile_state.attributes.get(
- ("orig_query", SubqueryLoader), compile_state.select_statement
- )
-
- # generate a new Query from the original, then
- # produce a subquery from it.
- left_alias = self._generate_from_original_query(
- compile_state,
- orig_query,
- leftmost_mapper,
- leftmost_attr,
- leftmost_relationship,
- entity.entity_zero,
- )
-
- # generate another Query that will join the
- # left alias to the target relationships.
- # basically doing a longhand
- # "from_self()". (from_self() itself not quite industrial
- # strength enough for all contingencies...but very close)
-
- q = query.Query(effective_entity)
-
- def set_state_options(compile_state):
- compile_state.attributes.update(
- {
- ("orig_query", SubqueryLoader): orig_query,
- ("subquery_path", None): subq_path,
- }
- )
-
- q = q._add_context_option(set_state_options, None)._disable_caching()
-
- q = q._set_enable_single_crit(False)
- to_join, local_attr, parent_alias = self._prep_for_joins(
- left_alias, subq_path
- )
-
- q = q.add_columns(*local_attr)
- q = self._apply_joins(
- q, to_join, left_alias, parent_alias, effective_entity
- )
-
- q = self._setup_options(q, subq_path, orig_query, effective_entity)
- q = self._setup_outermost_orderby(q)
-
- # add new query to attributes to be picked up
- # by create_row_processor
- # NOTE: be sure to consult baked.py for some hardcoded logic
- # about this structure as well
- assert q.session is None
- path.set(
- compile_state.attributes, "subqueryload_data", {"query": q},
- )
-
def _get_leftmost(self, subq_path):
subq_path = subq_path.path
subq_mapper = orm_util._class_to_mapper(subq_path[0])
@@ -1267,27 +1212,34 @@ class SubqueryLoader(PostLoader):
q,
*{
ent["entity"]
- for ent in _column_descriptions(orig_query)
+ for ent in _column_descriptions(
+ orig_query, compile_state=orig_compile_state
+ )
if ent["entity"] is not None
}
)
- # for column information, look to the compile state that is
- # already being passed through
- compile_state = orig_compile_state
-
# select from the identity columns of the outer (specifically, these
- # are the 'local_cols' of the property). This will remove
- # other columns from the query that might suggest the right entity
- # which is why we do _set_select_from above.
- target_cols = compile_state._adapt_col_list(
+ # are the 'local_cols' of the property). This will remove other
+ # columns from the query that might suggest the right entity which is
+ # why we do set select_from above. The attributes we have are
+ # coerced and adapted using the original query's adapter, which is
+ # needed only for the case of adapting a subclass column to
+ # that of a polymorphic selectable, e.g. we have
+ # Engineer.primary_language and the entity is Person. All other
+ # adaptations, e.g. from_self, select_entity_from(), will occur
+ # within the new query when it compiles, as the compile_state we are
+ # using here is only a partial one. If the subqueryload is from a
+ # with_polymorphic() or other aliased() object, left_attr will already
+ # be the correct attributes so no adaptation is needed.
+ target_cols = orig_compile_state._adapt_col_list(
[
- sql.coercions.expect(sql.roles.ByOfRole, o)
+ sql.coercions.expect(sql.roles.ColumnsClauseRole, o)
for o in leftmost_attr
],
- compile_state._get_current_adapter(),
+ orig_compile_state._get_current_adapter(),
)
- q._set_entities(target_cols)
+ q._raw_columns = target_cols
distinct_target_key = leftmost_relationship.distinct_target_key
@@ -1461,13 +1413,13 @@ class SubqueryLoader(PostLoader):
"_data",
)
- def __init__(self, context, subq_info):
+ def __init__(self, context, subq):
# avoid creating a cycle by storing context
# even though that's preferable
self.session = context.session
self.execution_options = context.execution_options
self.load_options = context.load_options
- self.subq = subq_info["query"]
+ self.subq = subq
self._data = None
def get(self, key, default):
@@ -1499,12 +1451,148 @@ class SubqueryLoader(PostLoader):
if self._data is None:
self._load()
+ def _setup_query_from_rowproc(
+ self, context, path, entity, loadopt, adapter,
+ ):
+ compile_state = context.compile_state
+ if (
+ not compile_state.compile_options._enable_eagerloads
+ or compile_state.compile_options._for_refresh_state
+ ):
+ return
+
+ context.loaders_require_buffering = True
+
+ path = path[self.parent_property]
+
+ # build up a path indicating the path from the leftmost
+ # entity to the thing we're subquery loading.
+ with_poly_entity = path.get(
+ compile_state.attributes, "path_with_polymorphic", None
+ )
+ if with_poly_entity is not None:
+ effective_entity = with_poly_entity
+ else:
+ effective_entity = self.entity
+
+ subq_path = context.query._execution_options.get(
+ ("subquery_path", None), orm_util.PathRegistry.root
+ )
+
+ subq_path = subq_path + path
+
+ # if not via query option, check for
+ # a cycle
+ 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
+
+ (
+ leftmost_mapper,
+ leftmost_attr,
+ leftmost_relationship,
+ ) = self._get_leftmost(subq_path)
+
+ # 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
+ # a new one. other approaches include possibly using the
+ # compiled query but swapping the params, seems only marginally
+ # less time spent but more complicated
+ orig_query = context.query._execution_options.get(
+ ("orig_query", SubqueryLoader), context.query
+ )
+
+ # make a new compile_state for the query that's probably cached, but
+ # we're sort of undoing a bit of that caching :(
+ compile_state_cls = ORMCompileState._get_plugin_class_for_plugin(
+ orig_query, "orm"
+ )
+
+ # this would create the full blown compile state, which we don't
+ # need
+ # orig_compile_state = compile_state_cls.create_for_statement(
+ # orig_query, None)
+
+ # this is the more "quick" version, however it's not clear how
+ # much of this we need. in particular I can't get a test to
+ # fail if the "set_base_alias" is missing and not sure why that is.
+ orig_compile_state = compile_state_cls._create_entities_collection(
+ orig_query
+ )
+
+ # generate a new Query from the original, then
+ # produce a subquery from it.
+ left_alias = self._generate_from_original_query(
+ orig_compile_state,
+ orig_query,
+ leftmost_mapper,
+ leftmost_attr,
+ leftmost_relationship,
+ entity,
+ )
+
+ # generate another Query that will join the
+ # left alias to the target relationships.
+ # basically doing a longhand
+ # "from_self()". (from_self() itself not quite industrial
+ # strength enough for all contingencies...but very close)
+
+ q = query.Query(effective_entity)
+
+ q._execution_options = q._execution_options.union(
+ {
+ ("orig_query", SubqueryLoader): orig_query,
+ ("subquery_path", None): subq_path,
+ }
+ )
+
+ q = q._set_enable_single_crit(False)
+ to_join, local_attr, parent_alias = self._prep_for_joins(
+ left_alias, subq_path
+ )
+
+ q = q.add_columns(*local_attr)
+ q = self._apply_joins(
+ q, to_join, left_alias, parent_alias, effective_entity
+ )
+
+ q = self._setup_options(q, subq_path, orig_query, effective_entity)
+ q = self._setup_outermost_orderby(q)
+
+ return q
+
def create_row_processor(
- self, context, path, loadopt, mapper, result, adapter, populators
+ self,
+ context,
+ query_entity,
+ path,
+ loadopt,
+ mapper,
+ result,
+ adapter,
+ populators,
):
if context.refresh_state:
return self._immediateload_create_row_processor(
- context, path, loadopt, mapper, result, adapter, populators
+ context,
+ query_entity,
+ path,
+ loadopt,
+ mapper,
+ result,
+ adapter,
+ populators,
)
if not self.parent.class_manager[self.key].impl.supports_population:
@@ -1513,16 +1601,27 @@ class SubqueryLoader(PostLoader):
"population - eager loading cannot be applied." % self
)
- path = path[self.parent_property]
+ # a little dance here as the "path" is still something that only
+ # semi-tracks the exact series of things we are loading, still not
+ # telling us about with_polymorphic() and stuff like that when it's at
+ # the root.. the initial MapperEntity is more accurate for this case.
+ if len(path) == 1:
+ if not orm_util._entity_isa(query_entity.entity_zero, self.parent):
+ return
+ elif not orm_util._entity_isa(path[-1], self.parent):
+ return
- subq_info = path.get(context.attributes, "subqueryload_data")
+ subq = self._setup_query_from_rowproc(
+ context, path, path[-1], loadopt, adapter,
+ )
- if subq_info is None:
+ if subq is None:
return
- subq = subq_info["query"]
-
assert subq.session is None
+
+ path = path[self.parent_property]
+
local_cols = self.parent_property.local_columns
# cache the loaded collections in the context
@@ -1530,7 +1629,7 @@ class SubqueryLoader(PostLoader):
# call upon create_row_processor again
collections = path.get(context.attributes, "collections")
if collections is None:
- collections = self._SubqCollections(context, subq_info)
+ collections = self._SubqCollections(context, subq)
path.set(context.attributes, "collections", collections)
if adapter:
@@ -1634,7 +1733,6 @@ class JoinedLoader(AbstractRelationshipLoader):
if not compile_state.compile_options._enable_eagerloads:
return
elif self.uselist:
- compile_state.loaders_require_uniquing = True
compile_state.multi_row_eager_loaders = True
path = path[self.parent_property]
@@ -2142,7 +2240,15 @@ class JoinedLoader(AbstractRelationshipLoader):
return False
def create_row_processor(
- self, context, path, loadopt, mapper, result, adapter, populators
+ self,
+ context,
+ query_entity,
+ path,
+ loadopt,
+ mapper,
+ result,
+ adapter,
+ populators,
):
if not self.parent.class_manager[self.key].impl.supports_population:
raise sa_exc.InvalidRequestError(
@@ -2150,6 +2256,9 @@ class JoinedLoader(AbstractRelationshipLoader):
"population - eager loading cannot be applied." % self
)
+ if self.uselist:
+ context.loaders_require_uniquing = True
+
our_path = path[self.parent_property]
eager_adapter = self._create_eager_adapter(
@@ -2160,6 +2269,7 @@ class JoinedLoader(AbstractRelationshipLoader):
key = self.key
_instance = loading._instance_processor(
+ query_entity,
self.mapper,
context,
result,
@@ -2177,7 +2287,14 @@ class JoinedLoader(AbstractRelationshipLoader):
self.parent_property._get_strategy(
(("lazy", "select"),)
).create_row_processor(
- context, path, loadopt, mapper, result, adapter, populators
+ context,
+ query_entity,
+ path,
+ loadopt,
+ mapper,
+ result,
+ adapter,
+ populators,
)
def _create_collection_loader(self, context, key, _instance, populators):
@@ -2382,11 +2499,26 @@ class SelectInLoader(PostLoader, util.MemoizedSlots):
return util.preloaded.ext_baked.bakery(size=50)
def create_row_processor(
- self, context, path, loadopt, mapper, result, adapter, populators
+ self,
+ context,
+ query_entity,
+ path,
+ loadopt,
+ mapper,
+ result,
+ adapter,
+ populators,
):
if context.refresh_state:
return self._immediateload_create_row_processor(
- context, path, loadopt, mapper, result, adapter, populators
+ context,
+ query_entity,
+ path,
+ loadopt,
+ mapper,
+ result,
+ adapter,
+ populators,
)
if not self.parent.class_manager[self.key].impl.supports_population:
@@ -2395,13 +2527,20 @@ class SelectInLoader(PostLoader, util.MemoizedSlots):
"population - eager loading cannot be applied." % self
)
+ # a little dance here as the "path" is still something that only
+ # semi-tracks the exact series of things we are loading, still not
+ # telling us about with_polymorphic() and stuff like that when it's at
+ # the root.. the initial MapperEntity is more accurate for this case.
+ if len(path) == 1:
+ if not orm_util._entity_isa(query_entity.entity_zero, self.parent):
+ return
+ elif not orm_util._entity_isa(path[-1], self.parent):
+ return
+
selectin_path = (
context.compile_state.current_path or orm_util.PathRegistry.root
) + path
- if not orm_util._entity_isa(path[-1], self.parent):
- return
-
if loading.PostLoad.path_exists(
context, selectin_path, self.parent_property
):
@@ -2427,7 +2566,6 @@ class SelectInLoader(PostLoader, util.MemoizedSlots):
return
elif selectin_path_w_prop.contains_mapper(self.mapper):
return
-
loading.PostLoad.callable_for_path(
context,
selectin_path,
@@ -2543,7 +2681,39 @@ class SelectInLoader(PostLoader, util.MemoizedSlots):
)
)
- orig_query = context.query
+ # a test which exercises what these comments talk about is
+ # test_selectin_relations.py -> test_twolevel_selectin_w_polymorphic
+ #
+ # effective_entity above is given to us in terms of the cached
+ # statement, namely this one:
+ orig_query = context.compile_state.select_statement
+
+ # the actual statement that was requested is this one:
+ # context_query = context.query
+ #
+ # that's not the cached one, however. So while it is of the identical
+ # structure, if it has entities like AliasedInsp, which we get from
+ # aliased() or with_polymorphic(), the AliasedInsp will likely be a
+ # different object identity each time, and will not match up
+ # hashing-wise to the corresponding AliasedInsp that's in the
+ # cached query, meaning it won't match on paths and loader lookups
+ # and loaders like this one will be skipped if it is used in options.
+ #
+ # Now we want to transfer loader options from the parent query to the
+ # "selectinload" query we're about to run. Which query do we transfer
+ # the options from? We use the cached query, because the options in
+ # that query will be in terms of the effective entity we were just
+ # handed.
+ #
+ # But now the selectinload/ baked query we are running is *also*
+ # cached. What if it's cached and running from some previous iteration
+ # of that AliasedInsp? Well in that case it will also use the previous
+ # iteration of the loader options. If the baked query expires and
+ # gets generated again, it will be handed the current effective_entity
+ # and the current _with_options, again in terms of whatever
+ # compile_state.select_statement happens to be right now, so the
+ # query will still be internally consistent and loader callables
+ # will be correctly invoked.
q._add_lazyload_options(
orig_query._with_options, path[self.parent_property]
diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py
index 85f4f85d1..f7a97bfe5 100644
--- a/lib/sqlalchemy/orm/util.py
+++ b/lib/sqlalchemy/orm/util.py
@@ -1187,9 +1187,9 @@ class Bundle(ORMColumnsClauseRole, SupportsCloneAnnotations, InspectionAttr):
return cloned
def __clause_element__(self):
- annotations = self._annotations.union(
- {"bundle": self, "entity_namespace": self}
- )
+ # ensure existing entity_namespace remains
+ annotations = {"bundle": self, "entity_namespace": self}
+ annotations.update(self._annotations)
return expression.ClauseList(
_literal_as_text_role=roles.ColumnsClauseRole,
group=False,
@@ -1258,6 +1258,8 @@ class _ORMJoin(expression.Join):
__visit_name__ = expression.Join.__visit_name__
+ inherit_cache = True
+
def __init__(
self,
left,