summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy/orm
diff options
context:
space:
mode:
Diffstat (limited to 'lib/sqlalchemy/orm')
-rw-r--r--lib/sqlalchemy/orm/context.py16
-rw-r--r--lib/sqlalchemy/orm/interfaces.py3
-rw-r--r--lib/sqlalchemy/orm/loading.py116
-rw-r--r--lib/sqlalchemy/orm/mapper.py16
-rw-r--r--lib/sqlalchemy/orm/persistence.py8
-rw-r--r--lib/sqlalchemy/orm/query.py70
-rw-r--r--lib/sqlalchemy/orm/session.py19
-rw-r--r--lib/sqlalchemy/orm/strategies.py255
-rw-r--r--lib/sqlalchemy/orm/strategy_options.py2
-rw-r--r--lib/sqlalchemy/orm/util.py2
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]