diff options
Diffstat (limited to 'lib/sqlalchemy/orm')
-rw-r--r-- | lib/sqlalchemy/orm/context.py | 16 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/interfaces.py | 3 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/loading.py | 116 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/mapper.py | 16 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/persistence.py | 8 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/query.py | 70 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/session.py | 19 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/strategies.py | 255 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/strategy_options.py | 2 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/util.py | 2 |
10 files changed, 293 insertions, 214 deletions
diff --git a/lib/sqlalchemy/orm/context.py b/lib/sqlalchemy/orm/context.py index 55a6b4cd2..96725e55b 100644 --- a/lib/sqlalchemy/orm/context.py +++ b/lib/sqlalchemy/orm/context.py @@ -46,6 +46,7 @@ class QueryContext(object): __slots__ = ( "compile_state", "query", + "params", "load_options", "bind_arguments", "execution_options", @@ -83,6 +84,7 @@ class QueryContext(object): self, compile_state, statement, + params, session, load_options, execution_options=None, @@ -96,6 +98,7 @@ class QueryContext(object): self.session = session self.loaders_require_buffering = False self.loaders_require_uniquing = False + self.params = params self.propagated_loader_options = { o for o in statement._with_options if o.propagate_to_loaders @@ -239,7 +242,13 @@ class ORMCompileState(CompileState): @classmethod def orm_setup_cursor_result( - cls, session, statement, execution_options, bind_arguments, result + cls, + session, + statement, + params, + execution_options, + bind_arguments, + result, ): execution_context = result.context compile_state = execution_context.compiled.compile_state @@ -256,6 +265,7 @@ class ORMCompileState(CompileState): querycontext = QueryContext( compile_state, statement, + params, session, load_options, execution_options, @@ -711,7 +721,9 @@ class ORMSelectCompileState(ORMCompileState, SelectState): and "entity_namespace" in element._annotations ): for elem in _select_iterables( - element._annotations["entity_namespace"].columns + element._annotations[ + "entity_namespace" + ]._all_column_expressions ): yield elem else: diff --git a/lib/sqlalchemy/orm/interfaces.py b/lib/sqlalchemy/orm/interfaces.py index e569c0603..4cf820ae3 100644 --- a/lib/sqlalchemy/orm/interfaces.py +++ b/lib/sqlalchemy/orm/interfaces.py @@ -38,6 +38,7 @@ from .. import util from ..sql import operators from ..sql import roles from ..sql import visitors +from ..sql.base import ExecutableOption from ..sql.traversals import HasCacheKey if util.TYPE_CHECKING: @@ -675,7 +676,7 @@ class StrategizedProperty(MapperProperty): ) -class ORMOption(HasCacheKey): +class ORMOption(ExecutableOption): """Base class for option objects that are passed to ORM queries. These options may be consumed by :meth:`.Query.options`, diff --git a/lib/sqlalchemy/orm/loading.py b/lib/sqlalchemy/orm/loading.py index 8d1ae2e69..601393156 100644 --- a/lib/sqlalchemy/orm/loading.py +++ b/lib/sqlalchemy/orm/loading.py @@ -187,6 +187,7 @@ def merge_frozen_result(session, statement, frozen_result, load=True): @util.preload_module("sqlalchemy.orm.context") def merge_result(query, iterator, load=True): """Merge a result into this :class:`.Query` object's Session.""" + querycontext = util.preloaded.orm_context session = query.session @@ -298,7 +299,8 @@ def load_on_ident( with_for_update=None, only_load_props=None, no_autoflush=False, - bind_arguments=util.immutabledict(), + bind_arguments=util.EMPTY_DICT, + execution_options=util.EMPTY_DICT, ): """Load the given identity key from the database.""" if key is not None: @@ -318,6 +320,7 @@ def load_on_ident( identity_token=identity_token, no_autoflush=no_autoflush, bind_arguments=bind_arguments, + execution_options=execution_options, ) @@ -331,7 +334,8 @@ def load_on_pk_identity( only_load_props=None, identity_token=None, no_autoflush=False, - bind_arguments=util.immutabledict(), + bind_arguments=util.EMPTY_DICT, + execution_options=util.EMPTY_DICT, ): """Load the given primary key identity from the database.""" @@ -339,21 +343,18 @@ def load_on_pk_identity( query = statement q = query._clone() + is_lambda = q._is_lambda_element + # TODO: fix these imports .... from .context import QueryContext, ORMCompileState if load_options is None: load_options = QueryContext.default_load_options - compile_options = ORMCompileState.default_compile_options.safe_merge( - q._compile_options - ) + compile_options = ORMCompileState.default_compile_options if primary_key_identity is not None: - # mapper = query._only_full_mapper_zero("load_on_pk_identity") - - # TODO: error checking? - mapper = query._raw_columns[0]._annotations["parententity"] + mapper = query._propagate_attrs["plugin_subject"] (_get_clause, _get_params) = mapper._get_clause @@ -379,10 +380,16 @@ def load_on_pk_identity( "release." ) - # TODO: can mapper._get_clause be pre-adapted? - q._where_criteria = ( - sql_util._deep_annotate(_get_clause, {"_orm_adapt": True}), - ) + if is_lambda: + q = q.add_criteria( + lambda q: q.where( + sql_util._deep_annotate(_get_clause, {"_orm_adapt": True}) + ), + ) + else: + q._where_criteria = ( + sql_util._deep_annotate(_get_clause, {"_orm_adapt": True}), + ) params = dict( [ @@ -392,44 +399,71 @@ def load_on_pk_identity( ) ] ) + else: + params = None - load_options += {"_params": params} + if is_lambda: + if with_for_update is not None or refresh_state or only_load_props: + raise NotImplementedError( + "refresh operation not supported with lambda statement" + ) - if with_for_update is not None: - version_check = True - q._for_update_arg = with_for_update - elif query._for_update_arg is not None: - version_check = True - q._for_update_arg = query._for_update_arg - else: version_check = False - if refresh_state and refresh_state.load_options: - compile_options += {"_current_path": refresh_state.load_path.parent} - q = q.options(*refresh_state.load_options) - - # TODO: most of the compile_options that are not legacy only involve this - # function, so try to see if handling of them can mostly be local to here + _, load_options = _set_get_options( + compile_options, + load_options, + populate_existing=bool(refresh_state), + version_check=version_check, + only_load_props=only_load_props, + refresh_state=refresh_state, + identity_token=identity_token, + ) - q._compile_options, load_options = _set_get_options( - compile_options, - load_options, - populate_existing=bool(refresh_state), - version_check=version_check, - only_load_props=only_load_props, - refresh_state=refresh_state, - identity_token=identity_token, - ) - q._order_by = None + if no_autoflush: + load_options += {"_autoflush": False} + else: + if with_for_update is not None: + version_check = True + q._for_update_arg = with_for_update + elif query._for_update_arg is not None: + version_check = True + q._for_update_arg = query._for_update_arg + else: + version_check = False + + if refresh_state and refresh_state.load_options: + compile_options += { + "_current_path": refresh_state.load_path.parent + } + q = q.options(*refresh_state.load_options) + + # TODO: most of the compile_options that are not legacy only involve + # this function, so try to see if handling of them can mostly be local + # to here + + q._compile_options, load_options = _set_get_options( + compile_options, + load_options, + populate_existing=bool(refresh_state), + version_check=version_check, + only_load_props=only_load_props, + refresh_state=refresh_state, + identity_token=identity_token, + ) + q._order_by = None - if no_autoflush: - load_options += {"_autoflush": False} + if no_autoflush: + load_options += {"_autoflush": False} + execution_options = util.EMPTY_DICT.merge_with( + execution_options, {"_sa_orm_load_options": load_options} + ) result = ( session.execute( q, - params=load_options._params, - execution_options={"_sa_orm_load_options": load_options}, + params=params, + execution_options=execution_options, bind_arguments=bind_arguments, future=True, ) diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index 2b04f1cc7..6d22c6205 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -719,9 +719,8 @@ class Mapper( """ return self - _cache_key_traversal = [ - ("mapper", visitors.ExtendedInternalTraversal.dp_plain_obj), - ] + def _gen_cache_key(self, anon_map, bindparams): + return (self,) @property def entity(self): @@ -2315,6 +2314,17 @@ class Mapper( ) ) + @property + def _all_column_expressions(self): + poly_properties = self._polymorphic_properties + adapter = self._polymorphic_adapter + + return [ + adapter.columns[prop.columns[0]] if adapter else prop.columns[0] + for prop in poly_properties + if isinstance(prop, properties.ColumnProperty) + ] + def _columns_plus_keys(self, polymorphic_mappers=()): if polymorphic_mappers: poly_properties = self._iterate_polymorphic_properties( diff --git a/lib/sqlalchemy/orm/persistence.py b/lib/sqlalchemy/orm/persistence.py index a78af92b9..7c254c61b 100644 --- a/lib/sqlalchemy/orm/persistence.py +++ b/lib/sqlalchemy/orm/persistence.py @@ -1832,7 +1832,13 @@ class BulkUDCompileState(CompileState): @classmethod def orm_setup_cursor_result( - cls, session, statement, execution_options, bind_arguments, result + cls, + session, + statement, + params, + execution_options, + bind_arguments, + result, ): # this stage of the execution is called after the diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py index b8226dfc0..d60c03bdc 100644 --- a/lib/sqlalchemy/orm/query.py +++ b/lib/sqlalchemy/orm/query.py @@ -125,6 +125,8 @@ class Query( load_options = QueryContext.default_load_options + _params = util.EMPTY_DICT + # local Query builder state, not needed for # compilation or execution _aliased_generation = None @@ -366,10 +368,8 @@ class Query( else: stmt = self._compile_state(for_statement=True).statement - if self.load_options._params: - # this is the search and replace thing. this is kind of nuts - # to be doing here. - stmt = stmt.params(self.load_options._params) + if self._params: + stmt = stmt.params(self._params) return stmt @@ -431,11 +431,7 @@ class Query( return stmt def subquery( - self, - name=None, - with_labels=False, - reduce_columns=False, - _legacy_core_statement=False, + self, name=None, with_labels=False, reduce_columns=False, ): """Return the full SELECT statement represented by this :class:`_query.Query`, embedded within an @@ -463,10 +459,7 @@ class Query( if with_labels: q = q.with_labels() - if _legacy_core_statement: - q = q._compile_state(for_statement=True).statement - else: - q = q.statement + q = q.statement if reduce_columns: q = q.reduce_columns() @@ -994,6 +987,13 @@ class Query( after rollback or commit handles object state automatically. This method is not intended for general use. + .. versionadded:: 1.4 + + The :meth:`.populate_existing` method is equivalent to passing the + ``populate_existing=True`` option to the + :meth:`_orm.Query.execution_options` method. + + """ self.load_options += {"_populate_existing": True} @@ -1298,16 +1298,11 @@ class Query( self.with_labels() .enable_eagerloads(False) .correlate(None) - .subquery(_legacy_core_statement=True) + .subquery() ._anonymous_fromclause() ) - parententity = self._raw_columns[0]._annotations.get("parententity") - if parententity: - ac = aliased(parententity.mapper, alias=fromclause) - q = self._from_selectable(ac) - else: - q = self._from_selectable(fromclause) + q = self._from_selectable(fromclause) if entities: q._set_entities(entities) @@ -1503,12 +1498,29 @@ class Query( def execution_options(self, **kwargs): """ Set non-SQL options which take effect during execution. - The options are the same as those accepted by - :meth:`_engine.Connection.execution_options`. + Options allowed here include all of those accepted by + :meth:`_engine.Connection.execution_options`, as well as a series + of ORM specific options: + + ``populate_existing=True`` - equivalent to using + :meth:`_orm.Query.populate_existing` + + ``autoflush=True|False`` - equivalent to using + :meth:`_orm.Query.autoflush` + + ``yield_per=<value>`` - equivalent to using + :meth:`_orm.Query.yield_per` Note that the ``stream_results`` execution option is enabled automatically if the :meth:`~sqlalchemy.orm.query.Query.yield_per()` - method is used. + method or execution option is used. + + The execution options may also be specified on a per execution basis + when using :term:`2.0 style` queries via the + :paramref:`_orm.Session.execution_options` parameter. + + .. versionadded:: 1.4 - added ORM options to + :meth:`_orm.Query.execution_options` .. seealso:: @@ -1579,8 +1591,7 @@ class Query( "params() takes zero or one positional argument, " "which is a dictionary." ) - params = self.load_options._params.union(kwargs) - self.load_options += {"_params": params} + self._params = self._params.union(kwargs) def where(self, *criterion): """A synonym for :meth:`.Query.filter`. @@ -2694,7 +2705,8 @@ class Query( def _iter(self): # new style execution. - params = self.load_options._params + params = self._params + statement = self._statement_20() result = self.session.execute( statement, @@ -2789,6 +2801,7 @@ class Query( context = QueryContext( compile_state, compile_state.statement, + self._params, self.session, self.load_options, ) @@ -2984,7 +2997,7 @@ class Query( delete_._where_criteria = self._where_criteria result = self.session.execute( delete_, - self.load_options._params, + self._params, execution_options={"synchronize_session": synchronize_session}, future=True, ) @@ -3060,7 +3073,7 @@ class Query( upd._where_criteria = self._where_criteria result = self.session.execute( upd, - self.load_options._params, + self._params, execution_options={"synchronize_session": synchronize_session}, future=True, ) @@ -3104,6 +3117,7 @@ class Query( context = QueryContext( compile_state, compile_state.statement, + self._params, self.session, self.load_options, ) diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py index 25aedd52d..339c57bdc 100644 --- a/lib/sqlalchemy/orm/session.py +++ b/lib/sqlalchemy/orm/session.py @@ -36,7 +36,6 @@ from ..inspection import inspect from ..sql import coercions from ..sql import dml from ..sql import roles -from ..sql import selectable from ..sql import visitors from ..sql.base import CompileState @@ -235,17 +234,22 @@ class ORMExecuteState(util.MemoizedSlots): @property def is_select(self): """return True if this is a SELECT operation.""" - return isinstance(self.statement, selectable.Select) + return self.statement.is_select + + @property + def is_insert(self): + """return True if this is an INSERT operation.""" + return self.statement.is_dml and self.statement.is_insert @property def is_update(self): """return True if this is an UPDATE operation.""" - return isinstance(self.statement, dml.Update) + return self.statement.is_dml and self.statement.is_update @property def is_delete(self): """return True if this is a DELETE operation.""" - return isinstance(self.statement, dml.Delete) + return self.statement.is_dml and self.statement.is_delete @property def _is_crud(self): @@ -1622,7 +1626,12 @@ class Session(_SessionClassMethods): if compile_state_cls: result = compile_state_cls.orm_setup_cursor_result( - self, statement, execution_options, bind_arguments, result + self, + statement, + params, + execution_options, + bind_arguments, + result, ) return result diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index db82f0b74..44f303fee 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -26,6 +26,7 @@ from .base import _RAISE_FOR_STATE from .base import _SET_DEFERRED_EXPIRED from .context import _column_descriptions from .context import ORMCompileState +from .context import QueryContext from .interfaces import LoaderStrategy from .interfaces import StrategizedProperty from .session import _state_session @@ -34,7 +35,6 @@ from .util import _none_set from .util import aliased from .. import event from .. import exc as sa_exc -from .. import future from .. import inspect from .. import log from .. import sql @@ -487,7 +487,7 @@ class DeferredColumnLoader(LoaderStrategy): if ( loading.load_on_ident( session, - future.select(localparent).apply_labels(), + sql.select(localparent).apply_labels(), state.key, only_load_props=group, refresh_state=state, @@ -620,7 +620,7 @@ class LazyLoader(AbstractRelationshipLoader, util.MemoizedSlots): "_simple_lazy_clause", "_raise_always", "_raise_on_sql", - "_bakery", + "_query_cache", ) def __init__(self, parent, strategy_key): @@ -881,83 +881,68 @@ class LazyLoader(AbstractRelationshipLoader, util.MemoizedSlots): for pk in self.mapper.primary_key ] - @util.preload_module("sqlalchemy.ext.baked") - def _memoized_attr__bakery(self): - return util.preloaded.ext_baked.bakery(size=50) + def _memoized_attr__query_cache(self): + return util.LRUCache(30) @util.preload_module("sqlalchemy.orm.strategy_options") def _emit_lazyload(self, session, state, primary_key_identity, passive): - # emit lazy load now using BakedQuery, to cut way down on the overhead - # of generating queries. - # there are two big things we are trying to guard against here: - # - # 1. two different lazy loads that need to have a different result, - # being cached on the same key. The results between two lazy loads - # can be different due to the options passed to the query, which - # take effect for descendant objects. Therefore we have to make - # sure paths and load options generate good cache keys, and if they - # don't, we don't cache. - # 2. a lazy load that gets cached on a key that includes some - # "throwaway" object, like a per-query AliasedClass, meaning - # the cache key will never be seen again and the cache itself - # will fill up. (the cache is an LRU cache, so while we won't - # run out of memory, it will perform terribly when it's full. A - # warning is emitted if this occurs.) We must prevent the - # generation of a cache key that is including a throwaway object - # in the key. - strategy_options = util.preloaded.orm_strategy_options - # note that "lazy='select'" and "lazy=True" make two separate - # lazy loaders. Currently the LRU cache is local to the LazyLoader, - # however add ourselves to the initial cache key just to future - # proof in case it moves - q = self._bakery(lambda session: session.query(self.entity), self) - - q.add_criteria( - lambda q: q._with_invoke_all_eagers(False), self.parent_property, + stmt = sql.lambda_stmt( + lambda: sql.select(self.entity) + .apply_labels() + ._set_compile_options(ORMCompileState.default_compile_options), + global_track_bound_values=False, + lambda_cache=self._query_cache, + track_on=(self,), ) if not self.parent_property.bake_queries: - q.spoil(full=True) + stmt = stmt.spoil() + + load_options = QueryContext.default_load_options + + load_options += { + "_invoke_all_eagers": False, + "_lazy_loaded_from": state, + } if self.parent_property.secondary is not None: - q.add_criteria( - lambda q: q.select_from( - self.mapper, self.parent_property.secondary - ) + stmt += lambda stmt: stmt.select_from( + self.mapper, self.parent_property.secondary ) pending = not state.key # don't autoflush on pending if pending or passive & attributes.NO_AUTOFLUSH: - q.add_criteria(lambda q: q.autoflush(False)) + stmt += lambda stmt: stmt.execution_options(autoflush=False) if state.load_options: - # here, if any of the options cannot return a cache key, - # the BakedQuery "spoils" and caching will not occur. a path - # that features Cls.attribute.of_type(some_alias) will cancel - # caching, for example, since "some_alias" is user-defined and - # is usually a throwaway object. + effective_path = state.load_path[self.parent_property] - q._add_lazyload_options(state.load_options, effective_path) + opts = list(state.load_options) + + stmt += lambda stmt: stmt.options(*opts) + stmt += lambda stmt: stmt._update_compile_options( + {"_current_path": effective_path} + ) if self.use_get: if self._raise_on_sql: self._invoke_raise_load(state, passive, "raise_on_sql") - return ( - q(session) - .with_post_criteria(lambda q: q._set_lazyload_from(state)) - ._load_on_pk_identity( - session, session.query(self.mapper), primary_key_identity - ) + return loading.load_on_pk_identity( + session, + stmt, + primary_key_identity, + load_options=load_options, + execution_options={"compiled_cache": self._query_cache}, ) if self._order_by: - q.add_criteria(lambda q: q.order_by(*self._order_by)) + stmt += lambda stmt: stmt.order_by(*self._order_by) def _lazyload_reverse(compile_context): for rev in self.parent_property._reverse_property: @@ -974,13 +959,18 @@ class LazyLoader(AbstractRelationshipLoader, util.MemoizedSlots): ] ).lazyload(rev.key).process_compile_state(compile_context) - q.add_criteria( - lambda q: q._add_context_option( - _lazyload_reverse, self.parent_property - ) + stmt += lambda stmt: stmt._add_context_option( + _lazyload_reverse, self.parent_property ) lazy_clause, params = self._generate_lazy_clause(state, passive) + + execution_options = { + "_sa_orm_load_options": load_options, + } + if not self.parent_property.bake_queries: + execution_options["compiled_cache"] = None + if self.key in state.dict: return attributes.ATTR_WAS_SET @@ -994,21 +984,16 @@ class LazyLoader(AbstractRelationshipLoader, util.MemoizedSlots): if self._raise_on_sql: self._invoke_raise_load(state, passive, "raise_on_sql") - q.add_criteria(lambda q: q.filter(lazy_clause)) - - # set parameters in the query such that we don't overwrite - # parameters that are already set within it - def set_default_params(q): - params.update(q.load_options._params) - q.load_options += {"_params": params} - return q + stmt = stmt.add_criteria( + lambda stmt: stmt.where(lazy_clause), enable_tracking=False + ) - result = ( - q(session) - .with_post_criteria(lambda q: q._set_lazyload_from(state)) - .with_post_criteria(set_default_params) - .all() + result = session.execute( + stmt, params, future=True, execution_options=execution_options ) + + result = result.unique().scalars().all() + if self.uselist: return result else: @@ -1409,6 +1394,7 @@ class SubqueryLoader(PostLoader): "session", "execution_options", "load_options", + "params", "subq", "_data", ) @@ -1419,6 +1405,7 @@ class SubqueryLoader(PostLoader): self.session = context.session self.execution_options = context.execution_options self.load_options = context.load_options + self.params = context.params or {} self.subq = subq self._data = None @@ -1443,7 +1430,7 @@ class SubqueryLoader(PostLoader): # 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)) + rows = list(q.params(self.params)) for k, v in itertools.groupby(rows, lambda x: x[1:]): self._data[k].extend(vv[0] for vv in v) @@ -1519,19 +1506,16 @@ class SubqueryLoader(PostLoader): 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) - if orig_query._is_lambda_element: - util.warn( - 'subqueryloader for "%s" must invoke lambda callable at %r in ' - "order to produce a new query, decreasing the efficiency " - "of caching for this statement. Consider using " - "selectinload() for more effective full-lambda caching" - % (self, orig_query) - ) + if context.load_options._lazy_loaded_from is None: + util.warn( + 'subqueryloader for "%s" must invoke lambda callable ' + "at %r in " + "order to produce a new query, decreasing the efficiency " + "of caching for this statement. Consider using " + "selectinload() for more effective full-lambda caching" + % (self, orig_query) + ) orig_query = orig_query._resolved # this is the more "quick" version, however it's not clear how @@ -2112,7 +2096,7 @@ class JoinedLoader(AbstractRelationshipLoader): else: # all other cases are innerjoin=='nested' approach eagerjoin = self._splice_nested_inner_join( - path, towrap, clauses, onclause + path, towrap, clauses, onclause, ) compile_state.eager_joins[query_entity_key] = eagerjoin @@ -2153,7 +2137,7 @@ class JoinedLoader(AbstractRelationshipLoader): assert isinstance(join_obj, orm_util._ORMJoin) elif isinstance(join_obj, sql.selectable.FromGrouping): return self._splice_nested_inner_join( - path, join_obj.element, clauses, onclause, splicing + path, join_obj.element, clauses, onclause, splicing, ) elif not isinstance(join_obj, orm_util._ORMJoin): if path[-2] is splicing: @@ -2170,12 +2154,12 @@ class JoinedLoader(AbstractRelationshipLoader): return None target_join = self._splice_nested_inner_join( - path, join_obj.right, clauses, onclause, join_obj._right_memo + path, join_obj.right, clauses, onclause, join_obj._right_memo, ) if target_join is None: right_splice = False target_join = self._splice_nested_inner_join( - path, join_obj.left, clauses, onclause, join_obj._left_memo + path, join_obj.left, clauses, onclause, join_obj._left_memo, ) if target_join is None: # should only return None when recursively called, @@ -2401,7 +2385,7 @@ class SelectInLoader(PostLoader, util.MemoizedSlots): "_parent_alias", "_query_info", "_fallback_query_info", - "_bakery", + "_query_cache", ) query_info = collections.namedtuple( @@ -2504,9 +2488,8 @@ class SelectInLoader(PostLoader, util.MemoizedSlots): (("lazy", "select"),) ).init_class_attribute(mapper) - @util.preload_module("sqlalchemy.ext.baked") - def _memoized_attr__bakery(self): - return util.preloaded.ext_baked.bakery(size=50) + def _memoized_attr__query_cache(self): + return util.LRUCache(30) def create_row_processor( self, @@ -2564,9 +2547,8 @@ class SelectInLoader(PostLoader, util.MemoizedSlots): with_poly_entity = path_w_prop.get( context.attributes, "path_with_polymorphic", None ) - if with_poly_entity is not None: - effective_entity = with_poly_entity + effective_entity = inspect(with_poly_entity) else: effective_entity = self.entity @@ -2645,32 +2627,37 @@ class SelectInLoader(PostLoader, util.MemoizedSlots): # we need to adapt our "pk_cols" and "in_expr" to that # entity. in non-"omit join" mode, these are against the # parent entity and do not need adaption. - insp = inspect(effective_entity) - if insp.is_aliased_class: - pk_cols = [insp._adapt_element(col) for col in pk_cols] - in_expr = insp._adapt_element(in_expr) - pk_cols = [insp._adapt_element(col) for col in pk_cols] - - q = self._bakery( - lambda session: session.query( + if effective_entity.is_aliased_class: + pk_cols = [ + effective_entity._adapt_element(col) for col in pk_cols + ] + in_expr = effective_entity._adapt_element(in_expr) + + q = sql.lambda_stmt( + lambda: sql.select( orm_util.Bundle("pk", *pk_cols), effective_entity - ), - self, + ).apply_labels(), + lambda_cache=self._query_cache, + global_track_bound_values=False, + track_on=(self, effective_entity,) + tuple(pk_cols), ) + if not self.parent_property.bake_queries: + q = q.spoil() + if not query_info.load_with_join: # the Bundle we have in the "omit_join" case is against raw, non # annotated columns, so to ensure the Query knows its primary # entity, we add it explicitly. If we made the Bundle against # annotated columns, we hit a performance issue in this specific # case, which is detailed in issue #4347. - q.add_criteria(lambda q: q.select_from(effective_entity)) + q = q.add_criteria(lambda q: q.select_from(effective_entity)) else: # in the non-omit_join case, the Bundle is against the annotated/ # mapped column of the parent entity, but the #4347 issue does not # occur in this case. pa = self._parent_alias - q.add_criteria( + q = q.add_criteria( lambda q: q.select_from(pa).join( getattr(pa, self.parent_property.key).of_type( effective_entity @@ -2678,18 +2665,9 @@ class SelectInLoader(PostLoader, util.MemoizedSlots): ) ) - if query_info.load_only_child: - q.add_criteria( - lambda q: q.filter( - in_expr.in_(sql.bindparam("primary_keys", expanding=True)) - ) - ) - else: - q.add_criteria( - lambda q: q.filter( - in_expr.in_(sql.bindparam("primary_keys", expanding=True)) - ) - ) + q = q.add_criteria( + lambda q: q.filter(in_expr.in_(sql.bindparam("primary_keys"))) + ) # a test which exercises what these comments talk about is # test_selectin_relations.py -> test_twolevel_selectin_w_polymorphic @@ -2715,31 +2693,39 @@ class SelectInLoader(PostLoader, util.MemoizedSlots): # 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* + # But now the selectinload 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 + # iteration of the loader options. If the 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] + effective_path = path[self.parent_property] + + options = orig_query._with_options + q = q.add_criteria( + lambda q: q.options(*options)._update_compile_options( + {"_current_path": effective_path} + ) ) if context.populate_existing: - q.add_criteria(lambda q: q.populate_existing()) + q = q.add_criteria( + lambda q: q.execution_options(populate_existing=True) + ) if self.parent_property.order_by: if not query_info.load_with_join: eager_order_by = self.parent_property.order_by - if insp.is_aliased_class: + if effective_entity.is_aliased_class: eager_order_by = [ - insp._adapt_element(elem) for elem in eager_order_by + effective_entity._adapt_element(elem) + for elem in eager_order_by ] - q.add_criteria(lambda q: q.order_by(*eager_order_by)) + q = q.add_criteria(lambda q: q.order_by(*eager_order_by)) else: def _setup_outermost_orderby(compile_context): @@ -2747,7 +2733,7 @@ class SelectInLoader(PostLoader, util.MemoizedSlots): util.to_list(self.parent_property.order_by) ) - q.add_criteria( + q = q.add_criteria( lambda q: q._add_context_option( _setup_outermost_orderby, self.parent_property ) @@ -2770,11 +2756,16 @@ class SelectInLoader(PostLoader, util.MemoizedSlots): our_keys = our_keys[self._chunksize :] data = { k: v - for k, v in q(context.session).params( - primary_keys=[ - key[0] if query_info.zero_idx else key for key in chunk - ] - ) + for k, v in context.session.execute( + q, + params={ + "primary_keys": [ + key[0] if query_info.zero_idx else key + for key in chunk + ] + }, + future=True, + ).unique() } for key in chunk: @@ -2817,7 +2808,9 @@ class SelectInLoader(PostLoader, util.MemoizedSlots): data = collections.defaultdict(list) for k, v in itertools.groupby( - q(context.session).params(primary_keys=primary_keys), + context.session.execute( + q, params={"primary_keys": primary_keys}, future=True + ).unique(), lambda x: x[0], ): data[k].extend(vv[1] for vv in v) diff --git a/lib/sqlalchemy/orm/strategy_options.py b/lib/sqlalchemy/orm/strategy_options.py index ffaa93404..b405153b9 100644 --- a/lib/sqlalchemy/orm/strategy_options.py +++ b/lib/sqlalchemy/orm/strategy_options.py @@ -352,7 +352,6 @@ class Load(Generative, LoaderOption): self, attr, strategy, propagate_to_loaders=True ): strategy = self._coerce_strat(strategy) - self.propagate_to_loaders = propagate_to_loaders cloned = self._clone_for_bind_strategy(attr, strategy, "relationship") self.path = cloned.path @@ -577,6 +576,7 @@ class _UnboundLoad(Load): if attr: path = path + (attr,) self.path = path + return path def __getstate__(self): diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py index 68ffa2393..71ee29597 100644 --- a/lib/sqlalchemy/orm/util.py +++ b/lib/sqlalchemy/orm/util.py @@ -806,7 +806,7 @@ class AliasedInsp( return {} @util.memoized_property - def columns(self): + def _all_column_expressions(self): if self._is_with_polymorphic: cols_plus_keys = self.mapper._columns_plus_keys( [ent.mapper for ent in self._with_polymorphic_entities] |