diff options
Diffstat (limited to 'lib/sqlalchemy/orm')
-rw-r--r-- | lib/sqlalchemy/orm/context.py | 100 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/loading.py | 189 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/query.py | 93 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/session.py | 106 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/strategies.py | 38 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/strategy_options.py | 7 |
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 |