summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy/orm
diff options
context:
space:
mode:
Diffstat (limited to 'lib/sqlalchemy/orm')
-rw-r--r--lib/sqlalchemy/orm/context.py100
-rw-r--r--lib/sqlalchemy/orm/loading.py189
-rw-r--r--lib/sqlalchemy/orm/query.py93
-rw-r--r--lib/sqlalchemy/orm/session.py106
-rw-r--r--lib/sqlalchemy/orm/strategies.py38
-rw-r--r--lib/sqlalchemy/orm/strategy_options.py7
6 files changed, 303 insertions, 230 deletions
diff --git a/lib/sqlalchemy/orm/context.py b/lib/sqlalchemy/orm/context.py
index 3acab7df7..09f3e7a12 100644
--- a/lib/sqlalchemy/orm/context.py
+++ b/lib/sqlalchemy/orm/context.py
@@ -39,11 +39,12 @@ from ..sql.visitors import InternalTraversal
_path_registry = PathRegistry.root
+_EMPTY_DICT = util.immutabledict()
+
class QueryContext(object):
__slots__ = (
"compile_state",
- "orm_query",
"query",
"load_options",
"bind_arguments",
@@ -74,8 +75,7 @@ class QueryContext(object):
_yield_per = None
_refresh_state = None
_lazy_loaded_from = None
- _orm_query = None
- _params = util.immutabledict()
+ _params = _EMPTY_DICT
def __init__(
self,
@@ -87,10 +87,9 @@ class QueryContext(object):
):
self.load_options = load_options
- self.execution_options = execution_options or {}
- self.bind_arguments = bind_arguments or {}
+ self.execution_options = execution_options or _EMPTY_DICT
+ self.bind_arguments = bind_arguments or _EMPTY_DICT
self.compile_state = compile_state
- self.orm_query = compile_state.orm_query
self.query = query = compile_state.query
self.session = session
@@ -118,20 +117,16 @@ class QueryContext(object):
% ", ".join(compile_state._no_yield_pers)
)
- @property
- def is_single_entity(self):
- # used for the check if we return a list of entities or tuples.
- # this is gone in 2.0 when we no longer make this decision.
- return (
- not self.load_options._only_return_tuples
- and len(self.compile_state._entities) == 1
- and self.compile_state._entities[0].supports_single_entity
- )
+ def dispose(self):
+ self.attributes.clear()
+ self.load_options._refresh_state = None
+ self.load_options._lazy_loaded_from = None
class ORMCompileState(CompileState):
class default_compile_options(CacheableOptions):
_cache_key_traversal = [
+ ("_use_legacy_query_style", InternalTraversal.dp_boolean),
("_orm_results", InternalTraversal.dp_boolean),
("_bake_ok", InternalTraversal.dp_boolean),
(
@@ -140,7 +135,6 @@ class ORMCompileState(CompileState):
),
("_current_path", InternalTraversal.dp_has_cache_key),
("_enable_single_crit", InternalTraversal.dp_boolean),
- ("_statement", InternalTraversal.dp_clauseelement),
("_enable_eagerloads", InternalTraversal.dp_boolean),
("_orm_only_from_obj_alias", InternalTraversal.dp_boolean),
("_only_load_props", InternalTraversal.dp_plain_obj),
@@ -148,6 +142,7 @@ class ORMCompileState(CompileState):
("_for_refresh_state", InternalTraversal.dp_boolean),
]
+ _use_legacy_query_style = False
_orm_results = True
_bake_ok = True
_with_polymorphic_adapt_map = ()
@@ -159,37 +154,36 @@ class ORMCompileState(CompileState):
_set_base_alias = False
_for_refresh_state = False
- # non-cache-key elements mostly for legacy use
- _statement = None
- _orm_query = None
-
@classmethod
def merge(cls, other):
return cls + other._state_dict()
- orm_query = None
current_path = _path_registry
def __init__(self, *arg, **kw):
raise NotImplementedError()
+ def dispose(self):
+ self.attributes.clear()
+
@classmethod
def create_for_statement(cls, statement_container, compiler, **kw):
raise NotImplementedError()
@classmethod
- def _create_for_legacy_query(cls, query, for_statement=False):
+ def _create_for_legacy_query(cls, query, toplevel, for_statement=False):
stmt = query._statement_20(orm_results=not for_statement)
- if query.compile_options._statement is not None:
- compile_state_cls = ORMFromStatementCompileState
- else:
- compile_state_cls = ORMSelectCompileState
+ # this chooses between ORMFromStatementCompileState and
+ # ORMSelectCompileState. We could also base this on
+ # query._statement is not None as we have the ORM Query here
+ # however this is the more general path.
+ compile_state_cls = CompileState._get_plugin_class_for_plugin(
+ stmt, "orm"
+ )
- # true in all cases except for two tests in test/orm/test_events.py
- # assert stmt.compile_options._orm_query is query
return compile_state_cls._create_for_statement_or_query(
- stmt, for_statement=for_statement
+ stmt, toplevel, for_statement=for_statement
)
@classmethod
@@ -199,6 +193,10 @@ class ORMCompileState(CompileState):
raise NotImplementedError()
@classmethod
+ def get_column_descriptions(self, statement):
+ return _column_descriptions(statement)
+
+ @classmethod
def orm_pre_session_exec(
cls, session, statement, execution_options, bind_arguments
):
@@ -219,10 +217,16 @@ class ORMCompileState(CompileState):
# as the statement is built. "subject" mapper is the generally
# standard object used as an identifier for multi-database schemes.
- if "plugin_subject" in statement._propagate_attrs:
- bind_arguments["mapper"] = statement._propagate_attrs[
- "plugin_subject"
- ].mapper
+ # we are here based on the fact that _propagate_attrs contains
+ # "compile_state_plugin": "orm". The "plugin_subject"
+ # needs to be present as well.
+
+ try:
+ plugin_subject = statement._propagate_attrs["plugin_subject"]
+ except KeyError:
+ assert False, "statement had 'orm' plugin but no plugin_subject"
+ else:
+ bind_arguments["mapper"] = plugin_subject.mapper
if load_options._autoflush:
session._autoflush()
@@ -296,11 +300,14 @@ class ORMFromStatementCompileState(ORMCompileState):
@classmethod
def create_for_statement(cls, statement_container, compiler, **kw):
compiler._rewrites_selected_columns = True
- return cls._create_for_statement_or_query(statement_container)
+ toplevel = not compiler.stack
+ return cls._create_for_statement_or_query(
+ statement_container, toplevel
+ )
@classmethod
def _create_for_statement_or_query(
- cls, statement_container, for_statement=False,
+ cls, statement_container, toplevel, for_statement=False,
):
# from .query import FromStatement
@@ -309,8 +316,9 @@ class ORMFromStatementCompileState(ORMCompileState):
self = cls.__new__(cls)
self._primary_entity = None
- self.orm_query = statement_container.compile_options._orm_query
-
+ self.use_orm_style = (
+ statement_container.compile_options._use_legacy_query_style
+ )
self.statement_container = self.query = statement_container
self.requested_statement = statement_container.element
@@ -325,12 +333,13 @@ class ORMFromStatementCompileState(ORMCompileState):
self.current_path = statement_container.compile_options._current_path
- if statement_container._with_options:
+ if toplevel and statement_container._with_options:
self.attributes = {"_unbound_load_dedupes": set()}
for opt in statement_container._with_options:
if opt._is_compile_state:
opt.process_compile_state(self)
+
else:
self.attributes = {}
@@ -411,24 +420,24 @@ class ORMSelectCompileState(ORMCompileState, SelectState):
_where_criteria = ()
_having_criteria = ()
- orm_query = None
-
@classmethod
def create_for_statement(cls, statement, compiler, **kw):
if not statement._is_future:
return SelectState(statement, compiler, **kw)
+ toplevel = not compiler.stack
+
compiler._rewrites_selected_columns = True
orm_state = cls._create_for_statement_or_query(
- statement, for_statement=True
+ statement, for_statement=True, toplevel=toplevel
)
SelectState.__init__(orm_state, orm_state.statement, compiler, **kw)
return orm_state
@classmethod
def _create_for_statement_or_query(
- cls, query, for_statement=False, _entities_only=False,
+ cls, query, toplevel, for_statement=False, _entities_only=False
):
assert isinstance(query, future.Select)
@@ -440,9 +449,8 @@ class ORMSelectCompileState(ORMCompileState, SelectState):
self._primary_entity = None
- self.orm_query = query.compile_options._orm_query
-
self.query = query
+ self.use_orm_style = query.compile_options._use_legacy_query_style
self.select_statement = select_statement = query
@@ -484,7 +492,7 @@ class ORMSelectCompileState(ORMCompileState, SelectState):
# rather than LABEL_STYLE_NONE, and if we can use disambiguate style
# for new style ORM selects too.
if self.select_statement._label_style is LABEL_STYLE_NONE:
- if self.orm_query and not for_statement:
+ if self.use_orm_style and not for_statement:
self.label_style = LABEL_STYLE_TABLENAME_PLUS_COL
else:
self.label_style = LABEL_STYLE_DISAMBIGUATE_ONLY
@@ -495,7 +503,7 @@ class ORMSelectCompileState(ORMCompileState, SelectState):
self.eager_order_by = ()
- if select_statement._with_options:
+ if toplevel and select_statement._with_options:
self.attributes = {"_unbound_load_dedupes": set()}
for opt in self.select_statement._with_options:
diff --git a/lib/sqlalchemy/orm/loading.py b/lib/sqlalchemy/orm/loading.py
index 616e757a3..44ab7dd63 100644
--- a/lib/sqlalchemy/orm/loading.py
+++ b/lib/sqlalchemy/orm/loading.py
@@ -14,8 +14,6 @@ as well as some of the attribute loading strategies.
"""
from __future__ import absolute_import
-import collections
-
from . import attributes
from . import exc as orm_exc
from . import path_registry
@@ -57,7 +55,11 @@ def instances(cursor, context):
compile_state = context.compile_state
filtered = compile_state._has_mapper_entities
- single_entity = context.is_single_entity
+ single_entity = (
+ not context.load_options._only_return_tuples
+ and len(compile_state._entities) == 1
+ and compile_state._entities[0].supports_single_entity
+ )
try:
(process, labels, extra) = list(
@@ -105,7 +107,7 @@ def instances(cursor, context):
if not fetch:
break
else:
- fetch = cursor.fetchall()
+ fetch = cursor._raw_all_rows()
if single_entity:
proc = process[0]
@@ -123,6 +125,10 @@ def instances(cursor, context):
if not yield_per:
break
+ context.dispose()
+ if not cursor.context.compiled.cache_key:
+ compile_state.attributes.clear()
+
result = ChunkedIteratorResult(
row_metadata, chunks, source_supports_scalars=single_entity, raw=cursor
)
@@ -347,12 +353,6 @@ def load_on_pk_identity(
q.compile_options
)
- # checking that query doesnt have criteria on it
- # just delete it here w/ optional assertion? since we are setting a
- # where clause also
- if refresh_state is None:
- _no_criterion_assertion(q, "get", order_by=False, distinct=False)
-
if primary_key_identity is not None:
# mapper = query._only_full_mapper_zero("load_on_pk_identity")
@@ -446,24 +446,6 @@ def load_on_pk_identity(
return None
-def _no_criterion_assertion(stmt, meth, order_by=True, distinct=True):
- if (
- stmt._where_criteria
- or stmt.compile_options._statement is not None
- or stmt._from_obj
- or stmt._legacy_setup_joins
- or stmt._limit_clause is not None
- or stmt._offset_clause is not None
- or stmt._group_by_clauses
- or (order_by and stmt._order_by_clauses)
- or (distinct and stmt._distinct)
- ):
- raise sa_exc.InvalidRequestError(
- "Query.%s() being called on a "
- "Query with existing criterion. " % meth
- )
-
-
def _set_get_options(
compile_opt,
load_opt,
@@ -587,49 +569,110 @@ def _instance_processor(
# performance-critical section in the whole ORM.
identity_class = mapper._identity_class
+ compile_state = context.compile_state
- populators = collections.defaultdict(list)
+ # look for "row getter" functions that have been assigned along
+ # with the compile state that were cached from a previous load.
+ # these are operator.itemgetter() objects that each will extract a
+ # particular column from each row.
+
+ getter_key = ("getters", mapper)
+ getters = path.get(compile_state.attributes, getter_key, None)
+
+ if getters is None:
+ # no getters, so go through a list of attributes we are loading for,
+ # and the ones that are column based will have already put information
+ # for us in another collection "memoized_setups", which represents the
+ # output of the LoaderStrategy.setup_query() method. We can just as
+ # easily call LoaderStrategy.create_row_processor for each, but by
+ # getting it all at once from setup_query we save another method call
+ # per attribute.
+ props = mapper._prop_set
+ if only_load_props is not None:
+ props = props.intersection(
+ mapper._props[k] for k in only_load_props
+ )
- props = mapper._prop_set
- if only_load_props is not None:
- props = props.intersection(mapper._props[k] for k in only_load_props)
+ quick_populators = path.get(
+ context.attributes, "memoized_setups", _none_set
+ )
- quick_populators = path.get(
- context.attributes, "memoized_setups", _none_set
- )
+ todo = []
+ cached_populators = {
+ "new": [],
+ "quick": [],
+ "deferred": [],
+ "expire": [],
+ "delayed": [],
+ "existing": [],
+ "eager": [],
+ }
+
+ if refresh_state is None:
+ # we can also get the "primary key" tuple getter function
+ pk_cols = mapper.primary_key
- for prop in props:
- if prop in quick_populators:
- # this is an inlined path just for column-based attributes.
- col = quick_populators[prop]
- if col is _DEFER_FOR_STATE:
- populators["new"].append(
- (prop.key, prop._deferred_column_loader)
- )
- elif col is _SET_DEFERRED_EXPIRED:
- # note that in this path, we are no longer
- # searching in the result to see if the column might
- # be present in some unexpected way.
- populators["expire"].append((prop.key, False))
- elif col is _RAISE_FOR_STATE:
- populators["new"].append((prop.key, prop._raise_column_loader))
- else:
- getter = None
- if not getter:
- getter = result._getter(col, False)
- if getter:
- populators["quick"].append((prop.key, getter))
- else:
- # fall back to the ColumnProperty itself, which
- # will iterate through all of its columns
- # to see if one fits
- prop.create_row_processor(
- context, path, mapper, result, adapter, populators
- )
+ if adapter:
+ pk_cols = [adapter.columns[c] for c in pk_cols]
+ primary_key_getter = result._tuple_getter(pk_cols)
else:
- prop.create_row_processor(
- context, path, mapper, result, adapter, populators
- )
+ primary_key_getter = None
+
+ getters = {
+ "cached_populators": cached_populators,
+ "todo": todo,
+ "primary_key_getter": primary_key_getter,
+ }
+ for prop in props:
+ if prop in quick_populators:
+ # this is an inlined path just for column-based attributes.
+ col = quick_populators[prop]
+ if col is _DEFER_FOR_STATE:
+ cached_populators["new"].append(
+ (prop.key, prop._deferred_column_loader)
+ )
+ elif col is _SET_DEFERRED_EXPIRED:
+ # note that in this path, we are no longer
+ # searching in the result to see if the column might
+ # be present in some unexpected way.
+ cached_populators["expire"].append((prop.key, False))
+ elif col is _RAISE_FOR_STATE:
+ cached_populators["new"].append(
+ (prop.key, prop._raise_column_loader)
+ )
+ else:
+ getter = None
+ if not getter:
+ getter = result._getter(col, False)
+ if getter:
+ cached_populators["quick"].append((prop.key, getter))
+ else:
+ # fall back to the ColumnProperty itself, which
+ # will iterate through all of its columns
+ # to see if one fits
+ prop.create_row_processor(
+ context,
+ path,
+ mapper,
+ result,
+ adapter,
+ cached_populators,
+ )
+ else:
+ # loader strategries like subqueryload, selectinload,
+ # joinedload, basically relationships, these need to interact
+ # with the context each time to work correctly.
+ todo.append(prop)
+
+ path.set(compile_state.attributes, getter_key, getters)
+
+ cached_populators = getters["cached_populators"]
+
+ 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
+ )
propagated_loader_options = context.propagated_loader_options
load_path = (
@@ -707,11 +750,7 @@ def _instance_processor(
else:
refresh_identity_key = None
- pk_cols = mapper.primary_key
-
- if adapter:
- pk_cols = [adapter.columns[c] for c in pk_cols]
- tuple_getter = result._tuple_getter(pk_cols)
+ primary_key_getter = getters["primary_key_getter"]
if mapper.allow_partial_pks:
is_not_primary_key = _none_set.issuperset
@@ -732,7 +771,11 @@ def _instance_processor(
else:
# look at the row, see if that identity is in the
# session, or we have to create a new one
- identitykey = (identity_class, tuple_getter(row), identity_token)
+ identitykey = (
+ identity_class,
+ primary_key_getter(row),
+ identity_token,
+ )
instance = session_identity_map.get(identitykey)
@@ -875,7 +918,7 @@ def _instance_processor(
def ensure_no_pk(row):
identitykey = (
identity_class,
- tuple_getter(row),
+ primary_key_getter(row),
identity_token,
)
if not is_not_primary_key(identitykey[1]):
diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py
index 25d6f4736..97a81e30f 100644
--- a/lib/sqlalchemy/orm/query.py
+++ b/lib/sqlalchemy/orm/query.py
@@ -128,6 +128,7 @@ class Query(
_aliased_generation = None
_enable_assertions = True
_last_joined_entity = None
+ _statement = None
# mirrors that of ClauseElement, used to propagate the "orm"
# plugin as well as the "subject" of the plugin, e.g. the mapper
@@ -232,7 +233,7 @@ class Query(
return
if (
self._where_criteria
- or self.compile_options._statement is not None
+ or self._statement is not None
or self._from_obj
or self._legacy_setup_joins
or self._limit_clause is not None
@@ -250,7 +251,7 @@ class Query(
self._no_criterion_assertion(meth, order_by, distinct)
self._from_obj = self._legacy_setup_joins = ()
- if self.compile_options._statement is not None:
+ if self._statement is not None:
self.compile_options += {"_statement": None}
self._where_criteria = ()
self._distinct = False
@@ -270,7 +271,7 @@ class Query(
def _no_statement_condition(self, meth):
if not self._enable_assertions:
return
- if self.compile_options._statement is not None:
+ if self._statement is not None:
raise sa_exc.InvalidRequestError(
(
"Query.%s() being called on a Query with an existing full "
@@ -356,7 +357,6 @@ class Query(
if (
not self.compile_options._set_base_alias
and not self.compile_options._with_polymorphic_adapt_map
- # and self.compile_options._statement is None
):
# if we don't have legacy top level aliasing features in use
# then convert to a future select() directly
@@ -383,48 +383,25 @@ class Query(
if not fn._bake_ok:
self.compile_options += {"_bake_ok": False}
- if self.compile_options._statement is not None:
- stmt = FromStatement(
- self._raw_columns, self.compile_options._statement
- )
- # TODO: once SubqueryLoader uses select(), we can remove
- # "_orm_query" from this structure
+ compile_options = self.compile_options
+ compile_options += {"_use_legacy_query_style": True}
+
+ if self._statement is not None:
+ stmt = FromStatement(self._raw_columns, self._statement)
stmt.__dict__.update(
_with_options=self._with_options,
_with_context_options=self._with_context_options,
- compile_options=self.compile_options
- + {"_orm_query": self.with_session(None)},
+ compile_options=compile_options,
_execution_options=self._execution_options,
)
stmt._propagate_attrs = self._propagate_attrs
else:
+ # Query / select() internal attributes are 99% cross-compatible
stmt = FutureSelect.__new__(FutureSelect)
-
+ stmt.__dict__.update(self.__dict__)
stmt.__dict__.update(
- _raw_columns=self._raw_columns,
- _where_criteria=self._where_criteria,
- _from_obj=self._from_obj,
- _legacy_setup_joins=self._legacy_setup_joins,
- _order_by_clauses=self._order_by_clauses,
- _group_by_clauses=self._group_by_clauses,
- _having_criteria=self._having_criteria,
- _distinct=self._distinct,
- _distinct_on=self._distinct_on,
- _with_options=self._with_options,
- _with_context_options=self._with_context_options,
- _hints=self._hints,
- _statement_hints=self._statement_hints,
- _correlate=self._correlate,
- _auto_correlate=self._auto_correlate,
- _limit_clause=self._limit_clause,
- _offset_clause=self._offset_clause,
- _for_update_arg=self._for_update_arg,
- _prefixes=self._prefixes,
- _suffixes=self._suffixes,
_label_style=self._label_style,
- compile_options=self.compile_options
- + {"_orm_query": self.with_session(None)},
- _execution_options=self._execution_options,
+ compile_options=compile_options,
)
if not orm_results:
@@ -897,9 +874,11 @@ class Query(
:return: The object instance, or ``None``.
"""
+ self._no_criterion_assertion("get", order_by=False, distinct=False)
return self._get_impl(ident, loading.load_on_pk_identity)
def _get_impl(self, primary_key_identity, db_load_fn, identity_token=None):
+
# convert composite types to individual args
if hasattr(primary_key_identity, "__composite_values__"):
primary_key_identity = primary_key_identity.__composite_values__()
@@ -977,33 +956,14 @@ class Query(
"""An :class:`.InstanceState` that is using this :class:`_query.Query`
for a lazy load operation.
- The primary rationale for this attribute is to support the horizontal
- sharding extension, where it is available within specific query
- execution time hooks created by this extension. To that end, the
- attribute is only intended to be meaningful at **query execution
- time**, and importantly not any time prior to that, including query
- compilation time.
-
- .. note::
-
- Within the realm of regular :class:`_query.Query` usage, this
- attribute is set by the lazy loader strategy before the query is
- invoked. However there is no established hook that is available to
- reliably intercept this value programmatically. It is set by the
- lazy loading strategy after any mapper option objects would have
- been applied, and now that the lazy loading strategy in the ORM
- makes use of "baked" queries to cache SQL compilation, the
- :meth:`.QueryEvents.before_compile` hook is also not reliable.
+ .. deprecated:: 1.4 This attribute should be viewed via the
+ :attr:`.ORMExecuteState.lazy_loaded_from` attribute, within
+ the context of the :meth:`.SessionEvents.do_orm_execute`
+ event.
- Currently, setting the :paramref:`_orm.relationship.bake_queries`
- to ``False`` on the target :func:`_orm.relationship`, and then
- making use of the :meth:`.QueryEvents.before_compile` event hook,
- is the only available programmatic path to intercepting this
- attribute. In future releases, there will be new hooks available
- that allow interception of the :class:`_query.Query` before it is
- executed, rather than before it is compiled.
+ .. seealso::
- .. versionadded:: 1.2.9
+ :attr:`.ORMExecuteState.lazy_loaded_from`
"""
return self.load_options._lazy_loaded_from
@@ -2713,6 +2673,7 @@ class Query(
statement = coercions.expect(
roles.SelectStatementRole, statement, apply_propagate_attrs=self
)
+ self._statement = statement
self.compile_options += {"_statement": statement}
def first(self):
@@ -2736,7 +2697,7 @@ class Query(
"""
# replicates limit(1) behavior
- if self.compile_options._statement is not None:
+ if self._statement is not None:
return self._iter().first()
else:
return self.limit(1)._iter().first()
@@ -2918,7 +2879,9 @@ class Query(
"for linking ORM results to arbitrary select constructs.",
version="1.4",
)
- compile_state = ORMCompileState._create_for_legacy_query(self)
+ compile_state = ORMCompileState._create_for_legacy_query(
+ self, toplevel=True
+ )
context = QueryContext(
compile_state, self.session, self.load_options
)
@@ -3332,7 +3295,7 @@ class Query(
def _compile_state(self, for_statement=False, **kw):
return ORMCompileState._create_for_legacy_query(
- self, for_statement=for_statement, **kw
+ self, toplevel=True, for_statement=for_statement, **kw
)
def _compile_context(self, for_statement=False):
@@ -3366,7 +3329,7 @@ class FromStatement(SelectStatementGrouping, Executable):
super(FromStatement, self).__init__(element)
def _compiler_dispatch(self, compiler, **kw):
- compile_state = self._compile_state_factory(self, self, **kw)
+ compile_state = self._compile_state_factory(self, compiler, **kw)
toplevel = not compiler.stack
diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py
index 8d2f13df3..25e224348 100644
--- a/lib/sqlalchemy/orm/session.py
+++ b/lib/sqlalchemy/orm/session.py
@@ -35,6 +35,7 @@ from ..inspection import inspect
from ..sql import coercions
from ..sql import roles
from ..sql import visitors
+from ..sql.base import CompileState
__all__ = ["Session", "SessionTransaction", "sessionmaker"]
@@ -98,7 +99,7 @@ DEACTIVE = util.symbol("DEACTIVE")
CLOSED = util.symbol("CLOSED")
-class ORMExecuteState(object):
+class ORMExecuteState(util.MemoizedSlots):
"""Stateful object used for the :meth:`.SessionEvents.do_orm_execute`
.. versionadded:: 1.4
@@ -109,7 +110,8 @@ class ORMExecuteState(object):
"session",
"statement",
"parameters",
- "execution_options",
+ "_execution_options",
+ "_merged_execution_options",
"bind_arguments",
)
@@ -119,7 +121,7 @@ class ORMExecuteState(object):
self.session = session
self.statement = statement
self.parameters = parameters
- self.execution_options = execution_options
+ self._execution_options = execution_options
self.bind_arguments = bind_arguments
def invoke_statement(
@@ -182,33 +184,51 @@ class ORMExecuteState(object):
_params = self.parameters
if execution_options:
- _execution_options = dict(self.execution_options)
+ _execution_options = dict(self._execution_options)
_execution_options.update(execution_options)
else:
- _execution_options = self.execution_options
+ _execution_options = self._execution_options
return self.session.execute(
statement, _params, _execution_options, _bind_arguments
)
@property
- def orm_query(self):
- """Return the :class:`_orm.Query` object associated with this
- execution.
+ def execution_options(self):
+ """Placeholder for execution options.
+
+ Raises an informative message, as there are local options
+ vs. merged options that can be viewed, via the
+ :attr:`.ORMExecuteState.local_execution_options` and
+ :attr:`.ORMExecuteState.merged_execution_options` methods.
- For SQLAlchemy-2.0 style usage, the :class:`_orm.Query` object
- is not used at all, and this attribute will return None.
"""
- load_opts = self.load_options
- if load_opts._orm_query:
- return load_opts._orm_query
+ raise AttributeError(
+ "Please use .local_execution_options or "
+ ".merged_execution_options"
+ )
- opts = self._orm_compile_options()
- if opts is not None:
- return opts._orm_query
- else:
- return None
+ @property
+ def local_execution_options(self):
+ """Dictionary view of the execution options passed to the
+ :meth:`.Session.execute` method. This does not include options
+ that may be associated with the statement being invoked.
+
+ """
+ return util.immutabledict(self._execution_options)
+
+ @property
+ def merged_execution_options(self):
+ """Dictionary view of all execution options merged together;
+ this includes those of the statement as well as those passed to
+ :meth:`.Session.execute`, with the local options taking precedence.
+
+ """
+ return self._merged_execution_options
+
+ def _memoized_attr__merged_execution_options(self):
+ return self.statement._execution_options.union(self._execution_options)
def _orm_compile_options(self):
opts = self.statement.compile_options
@@ -218,6 +238,21 @@ class ORMExecuteState(object):
return None
@property
+ def lazy_loaded_from(self):
+ """An :class:`.InstanceState` that is using this statement execution
+ for a lazy load operation.
+
+ The primary rationale for this attribute is to support the horizontal
+ sharding extension, where it is available within specific query
+ execution time hooks created by this extension. To that end, the
+ attribute is only intended to be meaningful at **query execution
+ time**, and importantly not any time prior to that, including query
+ compilation time.
+
+ """
+ return self.load_options._lazy_loaded_from
+
+ @property
def loader_strategy_path(self):
"""Return the :class:`.PathRegistry` for the current load path.
@@ -235,7 +270,7 @@ class ORMExecuteState(object):
def load_options(self):
"""Return the load_options that will be used for this execution."""
- return self.execution_options.get(
+ return self._execution_options.get(
"_sa_orm_load_options", context.QueryContext.default_load_options
)
@@ -1407,7 +1442,6 @@ class Session(_SessionClassMethods):
in order to execute the statement.
"""
-
statement = coercions.expect(roles.CoerceTextStatementRole, statement)
if not bind_arguments:
@@ -1415,12 +1449,19 @@ class Session(_SessionClassMethods):
elif kw:
bind_arguments.update(kw)
- compile_state_cls = statement._get_plugin_compile_state_cls("orm")
- if compile_state_cls:
+ if (
+ statement._propagate_attrs.get("compile_state_plugin", None)
+ == "orm"
+ ):
+ compile_state_cls = CompileState._get_plugin_class_for_plugin(
+ statement, "orm"
+ )
+
compile_state_cls.orm_pre_session_exec(
self, statement, execution_options, bind_arguments
)
else:
+ compile_state_cls = None
bind_arguments.setdefault("clause", statement)
if statement._is_future:
execution_options = util.immutabledict().merge_with(
@@ -1694,9 +1735,19 @@ class Session(_SessionClassMethods):
:meth:`.Session.bind_table`
"""
+
+ # this function is documented as a subclassing hook, so we have
+ # to call this method even if the return is simple
if bind:
return bind
+ elif not self.__binds and self.bind:
+ # simplest and most common case, we have a bind and no
+ # per-mapper/table binds, we're done
+ return self.bind
+ # we don't have self.bind and either have self.__binds
+ # or we don't have self.__binds (which is legacy). Look at the
+ # mapper and the clause
if mapper is clause is None:
if self.bind:
return self.bind
@@ -1707,6 +1758,7 @@ class Session(_SessionClassMethods):
"a binding."
)
+ # look more closely at the mapper.
if mapper is not None:
try:
mapper = inspect(mapper)
@@ -1718,6 +1770,7 @@ class Session(_SessionClassMethods):
else:
raise
+ # match up the mapper or clause in the __binds
if self.__binds:
# matching mappers and selectables to entries in the
# binds dictionary; supported use case.
@@ -1733,7 +1786,8 @@ class Session(_SessionClassMethods):
if obj in self.__binds:
return self.__binds[obj]
- # session has a single bind; supported use case.
+ # none of the __binds matched, but we have a fallback bind.
+ # return that
if self.bind:
return self.bind
@@ -1745,16 +1799,10 @@ class Session(_SessionClassMethods):
if clause is not None:
if clause.bind:
return clause.bind
- # for obj in visitors.iterate(clause):
- # if obj.bind:
- # return obj.bind
if mapper:
if mapper.persist_selectable.bind:
return mapper.persist_selectable.bind
- # for obj in visitors.iterate(mapper.persist_selectable):
- # if obj.bind:
- # return obj.bind
context = []
if mapper is not None:
diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py
index a7d501b53..626018997 100644
--- a/lib/sqlalchemy/orm/strategies.py
+++ b/lib/sqlalchemy/orm/strategies.py
@@ -12,12 +12,12 @@ from __future__ import absolute_import
import collections
import itertools
-from sqlalchemy.orm import query
from . import attributes
from . import exc as orm_exc
from . import interfaces
from . import loading
from . import properties
+from . import query
from . import relationships
from . import unitofwork
from . import util as orm_util
@@ -1143,7 +1143,7 @@ class SubqueryLoader(PostLoader):
) = self._get_leftmost(subq_path)
orig_query = compile_state.attributes.get(
- ("orig_query", SubqueryLoader), compile_state.orm_query
+ ("orig_query", SubqueryLoader), compile_state.query
)
# generate a new Query from the original, then
@@ -1168,9 +1168,7 @@ class SubqueryLoader(PostLoader):
def set_state_options(compile_state):
compile_state.attributes.update(
{
- ("orig_query", SubqueryLoader): orig_query.with_session(
- None
- ),
+ ("orig_query", SubqueryLoader): orig_query,
("subquery_path", None): subq_path,
}
)
@@ -1236,6 +1234,19 @@ class SubqueryLoader(PostLoader):
# to look only for significant columns
q = orig_query._clone().correlate(None)
+ # LEGACY: make a Query back from the select() !!
+ # This suits at least two legacy cases:
+ # 1. applications which expect before_compile() to be called
+ # below when we run .subquery() on this query (Keystone)
+ # 2. applications which are doing subqueryload with complex
+ # from_self() queries, as query.subquery() / .statement
+ # has to do the full compile context for multiply-nested
+ # from_self() (Neutron) - see test_subqload_from_self
+ # for demo.
+ q2 = query.Query.__new__(query.Query)
+ q2.__dict__.update(q.__dict__)
+ q = q2
+
# set the query's "FROM" list explicitly to what the
# FROM list would be in any case, as we will be limiting
# the columns in the SELECT list which may no longer include
@@ -1251,15 +1262,6 @@ class SubqueryLoader(PostLoader):
}
)
- # NOTE: keystone has a test which is counting before_compile
- # events. That test is in one case dependent on an extra
- # call that was occurring here within the subqueryloader setup
- # process, probably when the subquery() method was called.
- # Ultimately that call will not be occurring here.
- # the event has already been called on the original query when
- # we are here in any case, so keystone will need to adjust that
- # test.
-
# for column information, look to the compile state that is
# already being passed through
compile_state = orig_compile_state
@@ -1304,7 +1306,8 @@ class SubqueryLoader(PostLoader):
# the original query now becomes a subquery
# which we'll join onto.
-
+ # LEGACY: as "q" is a Query, the before_compile() event is invoked
+ # here.
embed_q = q.apply_labels().subquery()
left_alias = orm_util.AliasedClass(
leftmost_mapper, embed_q, use_mapper_path=True
@@ -1416,8 +1419,6 @@ class SubqueryLoader(PostLoader):
# these will fire relative to subq_path.
q = q._with_current_path(subq_path)
q = q.options(*orig_query._with_options)
- if orig_query.load_options._populate_existing:
- q.load_options += {"_populate_existing": True}
return q
@@ -1475,8 +1476,11 @@ class SubqueryLoader(PostLoader):
)
q = q.with_session(self.session)
+ if self.load_options._populate_existing:
+ q = q.populate_existing()
# to work with baked query, the parameters may have been
# updated since this query was created, so take these into account
+
rows = list(q.params(self.load_options._params))
for k, v in itertools.groupby(rows, lambda x: x[1:]):
self._data[k].extend(vv[0] for vv in v)
diff --git a/lib/sqlalchemy/orm/strategy_options.py b/lib/sqlalchemy/orm/strategy_options.py
index e0ba3050c..2049a7fe0 100644
--- a/lib/sqlalchemy/orm/strategy_options.py
+++ b/lib/sqlalchemy/orm/strategy_options.py
@@ -861,7 +861,14 @@ class _UnboundLoad(Load):
# we just located, then go through the rest of our path
# tokens and populate into the Load().
loader = Load(path_element)
+
if context is not None:
+ # TODO: this creates a cycle with context.attributes.
+ # the current approach to mitigating this is the context /
+ # compile_state attributes are cleared out when a result
+ # is fetched. However, it would be nice if these attributes
+ # could be passed to all methods so that all the state
+ # gets set up without ever creating any assignments.
loader.context = context
else:
context = loader.context