diff options
| author | Mike Bayer <mike_mp@zzzcomputing.com> | 2021-12-08 20:27:16 -0500 |
|---|---|---|
| committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2021-12-27 12:30:38 -0500 |
| commit | 91501e06a17d873902114275d7149ba24973db6a (patch) | |
| tree | 464d2c7167c30d92be13c851b52c0f4471645456 /lib | |
| parent | 2bb6cfc7c9b8f09eaa4efeffc337a1162993979c (diff) | |
| download | sqlalchemy-91501e06a17d873902114275d7149ba24973db6a.tar.gz | |
factor out UnboundLoad and rearchitect strategy_options.py
The architecture of Load is mostly rewritten here.
The change includes removal of the "pluggable" aspect
of the loader options, which would patch new methods onto
Load. This has been replaced by normal methods that
respond normally to typing annotations. As part of this
change, the bake_loaders() and unbake_loaders() options,
which have no effect since 1.4 and were unlikely to be
in any common use, have been removed.
Additionally, to support annotations for methods that
make use of @decorator, @generative etc., modified
format_argspec_plus to no longer return "args", instead
returns "grouped_args" which is always grouped and
allows return annotations to format correctly.
Fixes: #6986
Change-Id: I6117c642345cdde65a64389bba6057ddd5374427
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/sqlalchemy/ext/baked.py | 67 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/__init__.py | 32 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/context.py | 21 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/interfaces.py | 40 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/mapper.py | 10 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/path_registry.py | 60 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/properties.py | 3 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/relationships.py | 3 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/strategies.py | 14 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/strategy_options.py | 3169 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/util.py | 64 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/base.py | 6 | ||||
| -rw-r--r-- | lib/sqlalchemy/testing/plugin/pytestplugin.py | 2 | ||||
| -rw-r--r-- | lib/sqlalchemy/util/compat.py | 2 | ||||
| -rw-r--r-- | lib/sqlalchemy/util/langhelpers.py | 56 |
15 files changed, 1887 insertions, 1662 deletions
diff --git a/lib/sqlalchemy/ext/baked.py b/lib/sqlalchemy/ext/baked.py index e91277311..a2db9dbec 100644 --- a/lib/sqlalchemy/ext/baked.py +++ b/lib/sqlalchemy/ext/baked.py @@ -19,7 +19,6 @@ import logging from .. import exc as sa_exc from .. import util from ..orm import exc as orm_exc -from ..orm import strategy_options from ..orm.query import Query from ..orm.session import Session from ..sql import func @@ -579,70 +578,4 @@ class Result: return None -@util.deprecated( - "1.2", "Baked lazy loading is now the default implementation." -) -def bake_lazy_loaders(): - """Enable the use of baked queries for all lazyloaders systemwide. - - The "baked" implementation of lazy loading is now the sole implementation - for the base lazy loader; this method has no effect except for a warning. - - """ - pass - - -@util.deprecated( - "1.2", "Baked lazy loading is now the default implementation." -) -def unbake_lazy_loaders(): - """Disable the use of baked queries for all lazyloaders systemwide. - - This method now raises NotImplementedError() as the "baked" implementation - is the only lazy load implementation. The - :paramref:`_orm.relationship.bake_queries` flag may be used to disable - the caching of queries on a per-relationship basis. - - """ - raise NotImplementedError( - "Baked lazy loading is now the default implementation" - ) - - -@strategy_options.loader_option() -def baked_lazyload(loadopt, attr): - """Indicate that the given attribute should be loaded using "lazy" - loading with a "baked" query used in the load. - - """ - return loadopt.set_relationship_strategy(attr, {"lazy": "baked_select"}) - - -@baked_lazyload._add_unbound_fn -@util.deprecated( - "1.2", - "Baked lazy loading is now the default " - "implementation for lazy loading.", -) -def baked_lazyload(*keys): - return strategy_options._UnboundLoad._from_keys( - strategy_options._UnboundLoad.baked_lazyload, keys, False, {} - ) - - -@baked_lazyload._add_unbound_all_fn -@util.deprecated( - "1.2", - "Baked lazy loading is now the default " - "implementation for lazy loading.", -) -def baked_lazyload_all(*keys): - return strategy_options._UnboundLoad._from_keys( - strategy_options._UnboundLoad.baked_lazyload, keys, True, {} - ) - - -baked_lazyload = baked_lazyload._unbound_fn -baked_lazyload_all = baked_lazyload_all._unbound_all_fn - bakery = BakedQuery.bakery diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py index c49ff4ec6..69a3e64da 100644 --- a/lib/sqlalchemy/orm/__init__.py +++ b/lib/sqlalchemy/orm/__init__.py @@ -70,7 +70,22 @@ from .session import sessionmaker from .session import SessionTransaction from .state import AttributeState from .state import InstanceState +from .strategy_options import contains_eager +from .strategy_options import defaultload +from .strategy_options import defer +from .strategy_options import immediateload +from .strategy_options import joinedload +from .strategy_options import lazyload from .strategy_options import Load +from .strategy_options import load_only +from .strategy_options import noload +from .strategy_options import raiseload +from .strategy_options import selectin_polymorphic +from .strategy_options import selectinload +from .strategy_options import subqueryload +from .strategy_options import undefer +from .strategy_options import undefer_group +from .strategy_options import with_expression from .unitofwork import UOWTransaction from .util import aliased from .util import Bundle @@ -269,23 +284,6 @@ def clear_mappers(): mapperlib._dispose_registries(mapperlib._all_registries(), False) -joinedload = strategy_options.joinedload._unbound_fn -contains_eager = strategy_options.contains_eager._unbound_fn -defer = strategy_options.defer._unbound_fn -undefer = strategy_options.undefer._unbound_fn -undefer_group = strategy_options.undefer_group._unbound_fn -with_expression = strategy_options.with_expression._unbound_fn -load_only = strategy_options.load_only._unbound_fn -lazyload = strategy_options.lazyload._unbound_fn -subqueryload = strategy_options.subqueryload._unbound_fn -selectinload = strategy_options.selectinload._unbound_fn -immediateload = strategy_options.immediateload._unbound_fn -noload = strategy_options.noload._unbound_fn -raiseload = strategy_options.raiseload._unbound_fn -defaultload = strategy_options.defaultload._unbound_fn -selectin_polymorphic = strategy_options.selectin_polymorphic._unbound_fn - - @_sa_util.deprecated_20("eagerload", "Please use :func:`_orm.joinedload`.") def eagerload(*args, **kwargs): """A synonym for :func:`joinedload()`.""" diff --git a/lib/sqlalchemy/orm/context.py b/lib/sqlalchemy/orm/context.py index e834b22e8..f438b0b3a 100644 --- a/lib/sqlalchemy/orm/context.py +++ b/lib/sqlalchemy/orm/context.py @@ -458,7 +458,7 @@ class ORMFromStatementCompileState(ORMCompileState): self.current_path = statement_container._compile_options._current_path if toplevel and statement_container._with_options: - self.attributes = {"_unbound_load_dedupes": set()} + self.attributes = {} self.global_attributes = compiler._global_attributes for opt in statement_container._with_options: @@ -648,7 +648,7 @@ class ORMSelectCompileState(ORMCompileState, SelectState): select_statement._with_options or select_statement._memoized_select_entities ): - self.attributes = {"_unbound_load_dedupes": set()} + self.attributes = {} for ( memoized_entities @@ -669,9 +669,14 @@ class ORMSelectCompileState(ORMCompileState, SelectState): for opt in self.select_statement._with_options: if opt._is_compile_state: opt.process_compile_state(self) + else: self.attributes = {} + # uncomment to print out the context.attributes structure + # after it's been set up above + # self._dump_option_struct() + if select_statement._with_context_options: for fn, key in select_statement._with_context_options: fn(self) @@ -703,6 +708,18 @@ class ORMSelectCompileState(ORMCompileState, SelectState): return self + def _dump_option_struct(self): + print("\n---------------------------------------------------\n") + print(f"current path: {self.current_path}") + for key in self.attributes: + if isinstance(key, tuple) and key[0] == "loader": + print(f"\nLoader: {PathRegistry.coerce(key[1])}") + print(f" {self.attributes[key]}") + print(f" {self.attributes[key].__dict__}") + elif isinstance(key, tuple) and key[0] == "path_with_polymorphic": + print(f"\nWith Polymorphic: {PathRegistry.coerce(key[1])}") + print(f" {self.attributes[key]}") + def _setup_for_generate(self): query = self.select_statement diff --git a/lib/sqlalchemy/orm/interfaces.py b/lib/sqlalchemy/orm/interfaces.py index 2c6818a93..92ecbdd2d 100644 --- a/lib/sqlalchemy/orm/interfaces.py +++ b/lib/sqlalchemy/orm/interfaces.py @@ -590,17 +590,13 @@ class StrategizedProperty(MapperProperty): def _memoized_attr__wildcard_token(self): return ( - "%s:%s" - % (self.strategy_wildcard_key, path_registry._WILDCARD_TOKEN), + f"{self.strategy_wildcard_key}:{path_registry._WILDCARD_TOKEN}", ) def _memoized_attr__default_path_loader_key(self): return ( "loader", - ( - "%s:%s" - % (self.strategy_wildcard_key, path_registry._DEFAULT_TOKEN), - ), + (f"{self.strategy_wildcard_key}:{path_registry._DEFAULT_TOKEN}",), ) def _get_context_loader(self, context, path): @@ -619,6 +615,13 @@ class StrategizedProperty(MapperProperty): load = context.attributes[path_key] break + # note that if strategy_options.Load is placing non-actionable + # objects in the context like defaultload(), we would + # need to continue the loop here if we got such an + # option as below. + # if load.strategy or load.local_opts: + # break + return load def _get_strategy(self, key): @@ -780,7 +783,13 @@ class CompileStateOption(HasCacheKey, ORMOption): _is_compile_state = True def process_compile_state(self, compile_state): - """Apply a modification to a given :class:`.CompileState`.""" + """Apply a modification to a given :class:`.CompileState`. + + This method is part of the implementation of a particular + :class:`.CompileStateOption` and is only invoked internally + when an ORM query is compiled. + + """ def process_compile_state_replaced_entities( self, compile_state, mapper_entities @@ -789,6 +798,10 @@ class CompileStateOption(HasCacheKey, ORMOption): given entities that were replaced by with_only_columns() or with_entities(). + This method is part of the implementation of a particular + :class:`.CompileStateOption` and is only invoked internally + when an ORM query is compiled. + .. versionadded:: 1.4.19 """ @@ -804,18 +817,8 @@ class LoaderOption(CompileStateOption): def process_compile_state_replaced_entities( self, compile_state, mapper_entities ): - """Apply a modification to a given :class:`.CompileState`, - given entities that were replaced by with_only_columns() or - with_entities(). - - .. versionadded:: 1.4.19 - - """ self.process_compile_state(compile_state) - def process_compile_state(self, compile_state): - """Apply a modification to a given :class:`.CompileState`.""" - class CriteriaOption(CompileStateOption): """Describe a WHERE criteria modification to an ORM statement at @@ -827,9 +830,6 @@ class CriteriaOption(CompileStateOption): _is_criteria_option = True - def process_compile_state(self, compile_state): - """Apply a modification to a given :class:`.CompileState`.""" - def get_global_criteria(self, attributes): """update additional entity criteria options in the given attributes dictionary. diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index 40a76181c..a94c6bfa7 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -966,7 +966,7 @@ class Mapper( return self.persist_selectable @util.memoized_property - def _path_registry(self): + def _path_registry(self) -> PathRegistry: return PathRegistry.per_mapper(self) def _configure_inheritance(self): @@ -1985,7 +1985,7 @@ class Mapper( return "<Mapper at 0x%x; %s>" % (id(self), self.class_.__name__) def __str__(self): - return "mapped class %s%s->%s" % ( + return "Mapper[%s%s(%s)]" % ( self.class_.__name__, self.non_primary and " (non-primary)" or "", self.local_table.description @@ -3113,21 +3113,23 @@ class Mapper( if prop.parent is self or prop in keep_props: # "enable" options, to turn on the properties that we want to # load by default (subject to options from the query) - enable_opt.set_generic_strategy( + enable_opt = enable_opt._set_generic_strategy( # convert string name to an attribute before passing # to loader strategy (getattr(entity.entity_namespace, prop.key),), dict(prop.strategy_key), + _reconcile_to_other=True, ) else: # "disable" options, to turn off the properties from the # superclass that we *don't* want to load, applied after # the options from the query to override them - disable_opt.set_generic_strategy( + disable_opt = disable_opt._set_generic_strategy( # convert string name to an attribute before passing # to loader strategy (getattr(entity.entity_namespace, prop.key),), {"do_nothing": True}, + _reconcile_to_other=False, ) primary_key = [ diff --git a/lib/sqlalchemy/orm/path_registry.py b/lib/sqlalchemy/orm/path_registry.py index 0aa9de817..f2768a6b6 100644 --- a/lib/sqlalchemy/orm/path_registry.py +++ b/lib/sqlalchemy/orm/path_registry.py @@ -11,6 +11,9 @@ from functools import reduce from itertools import chain import logging +from typing import Any +from typing import Tuple +from typing import Union from . import base as orm_base from .. import exc @@ -60,7 +63,13 @@ class PathRegistry(HasCacheKey): is_token = False is_root = False + has_entity = False + + path: Tuple + natural_path: Tuple + parent: Union["PathRegistry", None] + root: "PathRegistry" _cache_key_traversal = [ ("path", visitors.ExtendedInternalTraversal.dp_has_cache_key_list) ] @@ -110,6 +119,9 @@ class PathRegistry(HasCacheKey): def __hash__(self): return id(self) + def __getitem__(self, key: Any) -> "PathRegistry": + raise NotImplementedError() + @property def length(self): return len(self.path) @@ -184,32 +196,13 @@ class PathRegistry(HasCacheKey): p = p[0:-1] return p - @classmethod - def serialize_context_dict(cls, dict_, tokens): - return [ - ((key, cls._serialize_path(path)), value) - for (key, path), value in [ - (k, v) - for k, v in dict_.items() - if isinstance(k, tuple) and k[0] in tokens - ] - ] - - @classmethod - def deserialize_context_dict(cls, serialized): - return util.OrderedDict( - ((key, tuple(cls._deserialize_path(path))), value) - for (key, path), value in serialized - ) - def serialize(self): path = self.path return self._serialize_path(path) @classmethod - def deserialize(cls, path): - if path is None: - return None + def deserialize(cls, path: Tuple) -> "PathRegistry": + assert path is not None p = cls._deserialize_path(path) return cls.coerce(p) @@ -225,18 +218,21 @@ class PathRegistry(HasCacheKey): return reduce(lambda prev, next: prev[next], raw, cls.root) def token(self, token): - if token.endswith(":" + _WILDCARD_TOKEN): + if token.endswith(f":{_WILDCARD_TOKEN}"): return TokenRegistry(self, token) - elif token.endswith(":" + _DEFAULT_TOKEN): + elif token.endswith(f":{_DEFAULT_TOKEN}"): return TokenRegistry(self.root, token) else: - raise exc.ArgumentError("invalid token: %s" % token) + raise exc.ArgumentError(f"invalid token: {token}") def __add__(self, other): return reduce(lambda prev, next: prev[next], other.path, self) + def __str__(self): + return f"ORM Path[{' -> '.join(str(elem) for elem in self.path)}]" + def __repr__(self): - return "%s(%r)" % (self.__class__.__name__, self.path) + return f"{self.__class__.__name__}({self.path!r})" class RootRegistry(PathRegistry): @@ -254,9 +250,9 @@ class RootRegistry(PathRegistry): def __getitem__(self, entity): if entity in PathToken._intern: - return PathToken._intern[entity] + return TokenRegistry(self, PathToken._intern[entity]) else: - return entity._path_registry + return inspection.inspect(entity)._path_registry PathRegistry.root = RootRegistry() @@ -315,7 +311,10 @@ class TokenRegistry(PathRegistry): yield self def __getitem__(self, entity): - raise NotImplementedError() + try: + return self.path[entity] + except TypeError as te: + raise IndexError(f"{entity}") from te class PropRegistry(PathRegistry): @@ -394,9 +393,6 @@ class PropRegistry(PathRegistry): self._default_path_loader_key = self.prop._default_path_loader_key self._loader_key = ("loader", self.natural_path) - def __str__(self): - return " -> ".join(str(elem) for elem in self.path) - @util.memoized_property def has_entity(self): return self.prop._links_to_entity @@ -511,6 +507,8 @@ class CachingEntityRegistry(AbstractEntityRegistry, dict): def __getitem__(self, entity): if isinstance(entity, (int, slice)): return self.path[entity] + elif isinstance(entity, PathToken): + return TokenRegistry(self, entity) else: return dict.__getitem__(self, entity) diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py index de46f84a7..04b7f89c2 100644 --- a/lib/sqlalchemy/orm/properties.py +++ b/lib/sqlalchemy/orm/properties.py @@ -13,6 +13,7 @@ mapped attributes. """ from . import attributes +from . import strategy_options from .descriptor_props import CompositeProperty from .descriptor_props import ConcreteInheritedProperty from .descriptor_props import SynonymProperty @@ -43,7 +44,7 @@ class ColumnProperty(StrategizedProperty): """ - strategy_wildcard_key = "column" + strategy_wildcard_key = strategy_options._COLUMN_TOKEN inherit_cache = True _links_to_entity = False diff --git a/lib/sqlalchemy/orm/relationships.py b/lib/sqlalchemy/orm/relationships.py index b4aef874f..4293cf656 100644 --- a/lib/sqlalchemy/orm/relationships.py +++ b/lib/sqlalchemy/orm/relationships.py @@ -18,6 +18,7 @@ import re import weakref from . import attributes +from . import strategy_options from .base import _is_mapped_class from .base import state_str from .interfaces import MANYTOMANY @@ -100,7 +101,7 @@ class RelationshipProperty(StrategizedProperty): """ - strategy_wildcard_key = "relationship" + strategy_wildcard_key = strategy_options._RELATIONSHIP_TOKEN inherit_cache = True _links_to_entity = True diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index d5f5f8527..844b0f007 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -1000,7 +1000,7 @@ class LazyLoader(AbstractRelationshipLoader, util.MemoizedSlots): and rev._use_get and not isinstance(rev.strategy, LazyLoader) ): - strategy_options.Load.for_existing_path( + strategy_options.Load._construct_for_existing_path( compile_context.compile_options._current_path[ rev.parent ] @@ -2168,7 +2168,7 @@ class JoinedLoader(AbstractRelationshipLoader): anonymize_labels=True, ) - assert clauses.aliased_class is not None + assert clauses.is_aliased_class innerjoin = ( loadopt.local_opts.get("innerjoin", self.parent_property.innerjoin) @@ -2265,12 +2265,12 @@ class JoinedLoader(AbstractRelationshipLoader): ) if adapter: - if getattr(adapter, "aliased_class", None): + if getattr(adapter, "is_aliased_class", False): # joining from an adapted entity. The adapted entity # might be a "with_polymorphic", so resolve that to our # specific mapper's entity before looking for our attribute # name on it. - efm = inspect(adapter.aliased_class)._entity_for_mapper( + efm = adapter.aliased_insp._entity_for_mapper( localparent if localparent.isa(self.parent) else self.parent @@ -2291,7 +2291,7 @@ class JoinedLoader(AbstractRelationshipLoader): else: onclause = self.parent_property - assert clauses.aliased_class is not None + assert clauses.is_aliased_class attach_on_outside = ( not chained_from_outerjoin @@ -2315,7 +2315,7 @@ class JoinedLoader(AbstractRelationshipLoader): # this is the "classic" eager join case. eagerjoin = orm_util._ORMJoin( towrap, - clauses.aliased_class, + clauses.aliased_insp, onclause, isouter=not innerjoin or query_entity.entity_zero.represents_outer_join @@ -2381,7 +2381,7 @@ class JoinedLoader(AbstractRelationshipLoader): if path[-2] is splicing: return orm_util._ORMJoin( join_obj, - clauses.aliased_class, + clauses.aliased_insp, onclause, isouter=False, _left_memo=splicing, diff --git a/lib/sqlalchemy/orm/strategy_options.py b/lib/sqlalchemy/orm/strategy_options.py index a5c539773..02068adce 100644 --- a/lib/sqlalchemy/orm/strategy_options.py +++ b/lib/sqlalchemy/orm/strategy_options.py @@ -8,19 +8,22 @@ """ +import typing +from typing import Any +from typing import cast +from typing import Mapping +from typing import Tuple +from typing import Union + from . import util as orm_util -from .attributes import QueryableAttribute -from .base import _class_to_mapper -from .base import _is_aliased_class -from .base import _is_mapped_class from .base import InspectionAttr from .interfaces import LoaderOption -from .interfaces import PropComparator from .path_registry import _DEFAULT_TOKEN from .path_registry import _WILDCARD_TOKEN from .path_registry import PathRegistry from .path_registry import TokenRegistry from .util import _orm_full_deannotate +from .util import AliasedInsp from .. import exc as sa_exc from .. import inspect from .. import util @@ -32,583 +35,754 @@ from ..sql import visitors from ..sql.base import _generative from ..sql.base import Generative +_RELATIONSHIP_TOKEN = "relationship" +_COLUMN_TOKEN = "column" -class Load(Generative, LoaderOption): - """Represents loader options which modify the state of a - :class:`_query.Query` in order to affect how various mapped attributes are - loaded. +if typing.TYPE_CHECKING: + from .mapper import Mapper - The :class:`_orm.Load` object is in most cases used implicitly behind the - scenes when one makes use of a query option like :func:`_orm.joinedload`, - :func:`.defer`, or similar. However, the :class:`_orm.Load` object - can also be used directly, and in some cases can be useful. - To use :class:`_orm.Load` directly, instantiate it with the target mapped - class as the argument. This style of usage is - useful when dealing with a :class:`_query.Query` - that has multiple entities:: +class _AbstractLoad(Generative, LoaderOption): + _is_strategy_option = True + propagate_to_loaders = False - myopt = Load(MyClass).joinedload("widgets") + def contains_eager(self, attr, alias=None, _is_chain=False): + r"""Indicate that the given attribute should be eagerly loaded from + columns stated manually in the query. - The above ``myopt`` can now be used with :meth:`_query.Query.options`, - where it - will only take effect for the ``MyClass`` entity:: + This function is part of the :class:`_orm.Load` interface and supports + both method-chained and standalone operation. - session.query(MyClass, MyOtherClass).options(myopt) + The option is used in conjunction with an explicit join that loads + the desired rows, i.e.:: - One case where :class:`_orm.Load` - is useful as public API is when specifying - "wildcard" options that only take effect for a certain class:: + sess.query(Order).\ + join(Order.user).\ + options(contains_eager(Order.user)) - session.query(Order).options(Load(Order).lazyload('*')) + The above query would join from the ``Order`` entity to its related + ``User`` entity, and the returned ``Order`` objects would have the + ``Order.user`` attribute pre-populated. - Above, all relationships on ``Order`` will be lazy-loaded, but other - attributes on those descendant objects will load using their normal - loader strategy. + It may also be used for customizing the entries in an eagerly loaded + collection; queries will normally want to use the + :ref:`orm_queryguide_populate_existing` execution option assuming the + primary collection of parent objects may already have been loaded:: - .. seealso:: + sess.query(User).\ + join(User.addresses).\ + filter(Address.email_address.like('%@aol.com')).\ + options(contains_eager(User.addresses)).\ + populate_existing() - :ref:`deferred_options` + See the section :ref:`contains_eager` for complete usage details. - :ref:`deferred_loading_w_multiple` + .. seealso:: - :ref:`relationship_loader_options` + :ref:`loading_toplevel` - """ + :ref:`contains_eager` - _is_strategy_option = True + """ + if alias is not None: + if not isinstance(alias, str): + info = inspect(alias) + alias = info.selectable - _cache_key_traversal = [ - ("path", visitors.ExtendedInternalTraversal.dp_has_cache_key), - ("strategy", visitors.ExtendedInternalTraversal.dp_plain_obj), - ("_of_type", visitors.ExtendedInternalTraversal.dp_multi), - ("_extra_criteria", visitors.InternalTraversal.dp_clauseelement_list), - ( - "_context_cache_key", - visitors.ExtendedInternalTraversal.dp_has_cache_key_tuples, - ), - ( - "local_opts", - visitors.ExtendedInternalTraversal.dp_string_multi_dict, - ), - ] + else: + util.warn_deprecated( + "Passing a string name for the 'alias' argument to " + "'contains_eager()` is deprecated, and will not work in a " + "future release. Please use a sqlalchemy.alias() or " + "sqlalchemy.orm.aliased() construct.", + version="1.4", + ) - def __init__(self, entity): - insp = inspect(entity) - insp._post_inspect + elif getattr(attr, "_of_type", None): + ot = inspect(attr._of_type) + alias = ot.selectable - self.path = insp._path_registry - # note that this .context is shared among all descendant - # Load objects - self.context = util.OrderedDict() - self.local_opts = {} - self.is_class_strategy = False + cloned = self._set_relationship_strategy( + attr, + {"lazy": "joined"}, + propagate_to_loaders=False, + opts={"eager_from_alias": alias}, + _reconcile_to_other=True if _is_chain else None, + ) + return cloned - @classmethod - def for_existing_path(cls, path): - load = cls.__new__(cls) - load.path = path - load.context = {} - load.local_opts = {} - load._of_type = None - load._extra_criteria = () - return load + def load_only(self, *attrs): + """Indicate that for a particular entity, only the given list + of column-based attribute names should be loaded; all others will be + deferred. - def _generate_extra_criteria(self, context): - """Apply the current bound parameters in a QueryContext to the - immediate "extra_criteria" stored with this Load object. + This function is part of the :class:`_orm.Load` interface and supports + both method-chained and standalone operation. - Load objects are typically pulled from the cached version of - the statement from a QueryContext. The statement currently being - executed will have new values (and keys) for bound parameters in the - extra criteria which need to be applied by loader strategies when - they handle this criteria for a result set. + Example - given a class ``User``, load only the ``name`` and + ``fullname`` attributes:: + + session.query(User).options(load_only(User.name, User.fullname)) + + Example - given a relationship ``User.addresses -> Address``, specify + subquery loading for the ``User.addresses`` collection, but on each + ``Address`` object load only the ``email_address`` attribute:: + + session.query(User).options( + subqueryload(User.addresses).load_only(Address.email_address) + ) + + For a statement that has multiple entities, + the lead entity can be + specifically referred to using the :class:`_orm.Load` constructor:: + + stmt = select(User, Address).join(User.addresses).options( + Load(User).load_only(User.name, User.fullname), + Load(Address).load_only(Address.email_address) + ) + + .. note:: This method will still load a :class:`_schema.Column` even + if the column property is defined with ``deferred=True`` + for the :func:`.column_property` function. """ + cloned = self._set_column_strategy( + attrs, + {"deferred": False, "instrument": True}, + ) + cloned = cloned._set_column_strategy( + "*", {"deferred": True, "instrument": True}, {"undefer_pks": True} + ) + return cloned - assert ( - self._extra_criteria - ), "this should only be called if _extra_criteria is present" + def joinedload(self, attr, innerjoin=None): + """Indicate that the given attribute should be loaded using joined + eager loading. - orig_query = context.compile_state.select_statement - current_query = context.query + This function is part of the :class:`_orm.Load` interface and supports + both method-chained and standalone operation. - # NOTE: while it seems like we should not do the "apply" operation - # here if orig_query is current_query, skipping it in the "optimized" - # case causes the query to be different from a cache key perspective, - # because we are creating a copy of the criteria which is no longer - # the same identity of the _extra_criteria in the loader option - # itself. cache key logic produces a different key for - # (A, copy_of_A) vs. (A, A), because in the latter case it shortens - # the second part of the key to just indicate on identity. + examples:: - # if orig_query is current_query: - # not cached yet. just do the and_() - # return and_(*self._extra_criteria) + # joined-load the "orders" collection on "User" + query(User).options(joinedload(User.orders)) - k1 = orig_query._generate_cache_key() - k2 = current_query._generate_cache_key() + # joined-load Order.items and then Item.keywords + query(Order).options( + joinedload(Order.items).joinedload(Item.keywords)) - return k2._apply_params_to_element(k1, and_(*self._extra_criteria)) + # lazily load Order.items, but when Items are loaded, + # joined-load the keywords collection + query(Order).options( + lazyload(Order.items).joinedload(Item.keywords)) - def _adjust_for_extra_criteria(self, context): - """Apply the current bound parameters in a QueryContext to all - occurrences "extra_criteria" stored within al this Load object; - copying in place. + :param innerjoin: if ``True``, indicates that the joined eager load + should use an inner join instead of the default of left outer join:: + + query(Order).options(joinedload(Order.user, innerjoin=True)) + + In order to chain multiple eager joins together where some may be + OUTER and others INNER, right-nested joins are used to link them:: + + query(A).options( + joinedload(A.bs, innerjoin=False). + joinedload(B.cs, innerjoin=True) + ) + + The above query, linking A.bs via "outer" join and B.cs via "inner" + join would render the joins as "a LEFT OUTER JOIN (b JOIN c)". When + using older versions of SQLite (< 3.7.16), this form of JOIN is + translated to use full subqueries as this syntax is otherwise not + directly supported. + + The ``innerjoin`` flag can also be stated with the term ``"unnested"``. + This indicates that an INNER JOIN should be used, *unless* the join + is linked to a LEFT OUTER JOIN to the left, in which case it + will render as LEFT OUTER JOIN. For example, supposing ``A.bs`` + is an outerjoin:: + + query(A).options( + joinedload(A.bs). + joinedload(B.cs, innerjoin="unnested") + ) + + The above join will render as "a LEFT OUTER JOIN b LEFT OUTER JOIN c", + rather than as "a LEFT OUTER JOIN (b JOIN c)". + + .. note:: The "unnested" flag does **not** affect the JOIN rendered + from a many-to-many association table, e.g. a table configured as + :paramref:`_orm.relationship.secondary`, to the target table; for + correctness of results, these joins are always INNER and are + therefore right-nested if linked to an OUTER join. + + .. versionchanged:: 1.0.0 ``innerjoin=True`` now implies + ``innerjoin="nested"``, whereas in 0.9 it implied + ``innerjoin="unnested"``. In order to achieve the pre-1.0 + "unnested" inner join behavior, use the value + ``innerjoin="unnested"``. See :ref:`migration_3008`. + + .. note:: + + The joins produced by :func:`_orm.joinedload` are **anonymously + aliased**. The criteria by which the join proceeds cannot be + modified, nor can the ORM-enabled :class:`_sql.Select` or legacy + :class:`_query.Query` refer to these joins in any way, including + ordering. See :ref:`zen_of_eager_loading` for further detail. + + To produce a specific SQL JOIN which is explicitly available, use + :meth:`_sql.Select.join` and :meth:`_query.Query.join`. To combine + explicit JOINs with eager loading of collections, use + :func:`_orm.contains_eager`; see :ref:`contains_eager`. + + .. seealso:: + + :ref:`loading_toplevel` + + :ref:`joined_eager_loading` """ - orig_query = context.compile_state.select_statement + loader = self._set_relationship_strategy( + attr, + {"lazy": "joined"}, + opts={"innerjoin": innerjoin} + if innerjoin is not None + else util.EMPTY_DICT, + ) + return loader - applied = {} + def subqueryload(self, attr): + """Indicate that the given attribute should be loaded using + subquery eager loading. - ck = [None, None] + This function is part of the :class:`_orm.Load` interface and supports + both method-chained and standalone operation. - def process(opt): - if not opt._extra_criteria: - return + examples:: - if ck[0] is None: - ck[:] = ( - orig_query._generate_cache_key(), - context.query._generate_cache_key(), - ) - k1, k2 = ck + # subquery-load the "orders" collection on "User" + query(User).options(subqueryload(User.orders)) - opt._extra_criteria = tuple( - k2._apply_params_to_element(k1, crit) - for crit in opt._extra_criteria - ) + # subquery-load Order.items and then Item.keywords + query(Order).options( + subqueryload(Order.items).subqueryload(Item.keywords)) - return self._deep_clone(applied, process) + # lazily load Order.items, but when Items are loaded, + # subquery-load the keywords collection + query(Order).options( + lazyload(Order.items).subqueryload(Item.keywords)) - def _deep_clone(self, applied, process): - if self in applied: - return applied[self] - cloned = self._generate() + .. seealso:: - applied[self] = cloned + :ref:`loading_toplevel` - cloned.strategy = self.strategy + :ref:`subquery_eager_loading` - assert cloned.propagate_to_loaders == self.propagate_to_loaders - assert cloned.is_class_strategy == self.is_class_strategy - assert cloned.is_opts_only == self.is_opts_only + """ + return self._set_relationship_strategy(attr, {"lazy": "subquery"}) - if self.context: - cloned.context = util.OrderedDict( - [ - ( - key, - value._deep_clone(applied, process) - if isinstance(value, Load) - else value, - ) - for key, value in self.context.items() - ] - ) + def selectinload(self, attr): + """Indicate that the given attribute should be loaded using + SELECT IN eager loading. - cloned.local_opts.update(self.local_opts) + This function is part of the :class:`_orm.Load` interface and supports + both method-chained and standalone operation. - process(cloned) + examples:: - return cloned + # selectin-load the "orders" collection on "User" + query(User).options(selectinload(User.orders)) - @property - def _context_cache_key(self): - serialized = [] - if self.context is None: - return [] - for (key, loader_path), obj in self.context.items(): - if key != "loader": - continue - serialized.append(loader_path + (obj,)) - return serialized + # selectin-load Order.items and then Item.keywords + query(Order).options( + selectinload(Order.items).selectinload(Item.keywords)) - def _generate(self): - cloned = super(Load, self)._generate() - cloned.local_opts = {} - return cloned + # lazily load Order.items, but when Items are loaded, + # selectin-load the keywords collection + query(Order).options( + lazyload(Order.items).selectinload(Item.keywords)) - is_opts_only = False - is_class_strategy = False - strategy = None - propagate_to_loaders = False - _of_type = None - _extra_criteria = () + .. versionadded:: 1.2 - def process_compile_state_replaced_entities( - self, compile_state, mapper_entities - ): - if not compile_state.compile_options._enable_eagerloads: - return + .. seealso:: - # process is being run here so that the options given are validated - # against what the lead entities were, as well as to accommodate - # for the entities having been replaced with equivalents - self._process( - compile_state, - mapper_entities, - not bool(compile_state.current_path), - ) + :ref:`loading_toplevel` - def process_compile_state(self, compile_state): - if not compile_state.compile_options._enable_eagerloads: - return + :ref:`selectin_eager_loading` - self._process( - compile_state, - compile_state._lead_mapper_entities, - not bool(compile_state.current_path) - and not compile_state.compile_options._for_refresh_state, + """ + return self._set_relationship_strategy(attr, {"lazy": "selectin"}) + + def lazyload(self, attr): + """Indicate that the given attribute should be loaded using "lazy" + loading. + + This function is part of the :class:`_orm.Load` interface and supports + both method-chained and standalone operation. + + .. seealso:: + + :ref:`loading_toplevel` + + :ref:`lazy_loading` + + """ + return self._set_relationship_strategy(attr, {"lazy": "select"}) + + def immediateload(self, attr): + """Indicate that the given attribute should be loaded using + an immediate load with a per-attribute SELECT statement. + + The load is achieved using the "lazyloader" strategy and does not + fire off any additional eager loaders. + + The :func:`.immediateload` option is superseded in general + by the :func:`.selectinload` option, which performs the same task + more efficiently by emitting a SELECT for all loaded objects. + + This function is part of the :class:`_orm.Load` interface and supports + both method-chained and standalone operation. + + .. seealso:: + + :ref:`loading_toplevel` + + :ref:`selectin_eager_loading` + + """ + loader = self._set_relationship_strategy(attr, {"lazy": "immediate"}) + return loader + + def noload(self, attr): + """Indicate that the given relationship attribute should remain + unloaded. + + The relationship attribute will return ``None`` when accessed without + producing any loading effect. + + This function is part of the :class:`_orm.Load` interface and supports + both method-chained and standalone operation. + + :func:`_orm.noload` applies to :func:`_orm.relationship` attributes + only. + + .. note:: Setting this loading strategy as the default strategy + for a relationship using the :paramref:`.orm.relationship.lazy` + parameter may cause issues with flushes, such if a delete operation + needs to load related objects and instead ``None`` was returned. + + .. seealso:: + + :ref:`loading_toplevel` + + """ + + return self._set_relationship_strategy(attr, {"lazy": "noload"}) + + def raiseload(self, attr, sql_only=False): + """Indicate that the given attribute should raise an error if accessed. + + A relationship attribute configured with :func:`_orm.raiseload` will + raise an :exc:`~sqlalchemy.exc.InvalidRequestError` upon access. The + typical way this is useful is when an application is attempting to + ensure that all relationship attributes that are accessed in a + particular context would have been already loaded via eager loading. + Instead of having to read through SQL logs to ensure lazy loads aren't + occurring, this strategy will cause them to raise immediately. + + :func:`_orm.raiseload` applies to :func:`_orm.relationship` attributes + only. In order to apply raise-on-SQL behavior to a column-based + attribute, use the :paramref:`.orm.defer.raiseload` parameter on the + :func:`.defer` loader option. + + :param sql_only: if True, raise only if the lazy load would emit SQL, + but not if it is only checking the identity map, or determining that + the related value should just be None due to missing keys. When False, + the strategy will raise for all varieties of relationship loading. + + This function is part of the :class:`_orm.Load` interface and supports + both method-chained and standalone operation. + + + .. versionadded:: 1.1 + + .. seealso:: + + :ref:`loading_toplevel` + + :ref:`prevent_lazy_with_raiseload` + + :ref:`deferred_raiseload` + + """ + + return self._set_relationship_strategy( + attr, {"lazy": "raise_on_sql" if sql_only else "raise"} ) - def _process(self, compile_state, mapper_entities, raiseerr): - is_refresh = compile_state.compile_options._for_refresh_state - current_path = compile_state.current_path - if current_path: - for (token, start_path), loader in self.context.items(): - if is_refresh and not loader.propagate_to_loaders: - continue - chopped_start_path = self._chop_path(start_path, current_path) - if chopped_start_path is not None: - compile_state.attributes[ - (token, chopped_start_path) - ] = loader - else: - compile_state.attributes.update(self.context) + def defaultload(self, attr): + """Indicate an attribute should load using its default loader style. - def _generate_path( - self, - path, - attr, - for_strategy, - wildcard_key, - raiseerr=True, - polymorphic_entity_context=None, - ): - existing_of_type = self._of_type - self._of_type = None - if raiseerr and not path.has_entity: - if isinstance(path, TokenRegistry): - raise sa_exc.ArgumentError( - "Wildcard token cannot be followed by another entity" - ) - else: - raise sa_exc.ArgumentError( - "Mapped attribute '%s' does not " - "refer to a mapped entity" % (path.prop,) - ) + This method is used to link to other loader options further into + a chain of attributes without altering the loader style of the links + along the chain. For example, to set joined eager loading for an + element of an element:: - if isinstance(attr, str): + session.query(MyClass).options( + defaultload(MyClass.someattribute). + joinedload(MyOtherClass.someotherattribute) + ) - default_token = attr.endswith(_DEFAULT_TOKEN) - if attr.endswith(_WILDCARD_TOKEN) or default_token: - if default_token: - self.propagate_to_loaders = False - if wildcard_key: - attr = "%s:%s" % (wildcard_key, attr) - - # TODO: AliasedInsp inside the path for of_type is not - # working for a with_polymorphic entity because the - # relationship loaders don't render the with_poly into the - # path. See #4469 which will try to improve this - if existing_of_type and not existing_of_type.is_aliased_class: - path = path.parent[existing_of_type] - path = path.token(attr) - self.path = path - return path + :func:`.defaultload` is also useful for setting column-level options on + a related class, namely that of :func:`.defer` and :func:`.undefer`:: - raise sa_exc.ArgumentError( - "Strings are not accepted for attribute names in loader " - "options; please use class-bound attributes directly." + session.query(MyClass).options( + defaultload(MyClass.someattribute). + defer("some_column"). + undefer("some_other_column") ) - insp = inspect(attr) + .. seealso:: - if insp.is_mapper or insp.is_aliased_class: - # TODO: this does not appear to be a valid codepath. "attr" - # would never be a mapper. This block is present in 1.2 - # as well however does not seem to be accessed in any tests. - if not orm_util._entity_corresponds_to_use_path_impl( - attr.parent, path[-1] - ): - if raiseerr: - raise sa_exc.ArgumentError( - "Attribute '%s' does not " - "link from element '%s'" % (attr, path.entity) - ) - else: - return None - elif insp.is_property: - prop = found_property = attr - path = path[prop] - elif insp.is_attribute: - prop = found_property = attr.property + :meth:`_orm.Load.options` - allows for complex hierarchical + loader option structures with less verbosity than with individual + :func:`.defaultload` directives. - if not orm_util._entity_corresponds_to_use_path_impl( - attr.parent, path[-1] - ): - if raiseerr: - raise sa_exc.ArgumentError( - 'Attribute "%s" does not ' - 'link from element "%s".%s' - % ( - attr, - path.entity, - ( - " Did you mean to use " - "%s.of_type(%s)?" - % (path[-2], attr.class_.__name__) - if len(path) > 1 - and path.entity.is_mapper - and attr.parent.is_aliased_class - else "" - ), - ) - ) - else: - return None + :ref:`relationship_loader_options` + + :ref:`deferred_loading_w_multiple` - if attr._extra_criteria and not self._extra_criteria: - # in most cases, the process that brings us here will have - # already established _extra_criteria. however if not, - # and it's present on the attribute, then use that. - self._extra_criteria = attr._extra_criteria + """ + return self._set_relationship_strategy(attr, None) - if getattr(attr, "_of_type", None): - ac = attr._of_type - ext_info = of_type_info = inspect(ac) + def defer(self, key, raiseload=False): + r"""Indicate that the given column-oriented attribute should be + deferred, e.g. not loaded until accessed. - if polymorphic_entity_context is None: - polymorphic_entity_context = self.context + This function is part of the :class:`_orm.Load` interface and supports + both method-chained and standalone operation. - existing = path.entity_path[prop].get( - polymorphic_entity_context, "path_with_polymorphic" - ) + e.g.:: - if not ext_info.is_aliased_class: - ac = orm_util.with_polymorphic( - ext_info.mapper.base_mapper, - ext_info.mapper, - aliased=True, - _use_mapper_path=True, - _existing_alias=inspect(existing) - if existing is not None - else None, - ) + from sqlalchemy.orm import defer + + session.query(MyClass).options( + defer(MyClass.attribute_one), + defer(MyClass.attribute_two) + ) + + To specify a deferred load of an attribute on a related class, + the path can be specified one token at a time, specifying the loading + style for each link along the chain. To leave the loading style + for a link unchanged, use :func:`_orm.defaultload`:: + + session.query(MyClass).options( + defaultload(MyClass.someattr).defer(RelatedClass.some_column) + ) - ext_info = inspect(ac) + Multiple deferral options related to a relationship can be bundled + at once using :meth:`_orm.Load.options`:: - path.entity_path[prop].set( - polymorphic_entity_context, "path_with_polymorphic", ac + + session.query(MyClass).options( + defaultload(MyClass.someattr).options( + defer(RelatedClass.some_column), + defer(RelatedClass.some_other_column), + defer(RelatedClass.another_column) ) + ) - path = path[prop][ext_info] + :param key: Attribute to be deferred. - self._of_type = of_type_info + :param raiseload: raise :class:`.InvalidRequestError` if the column + value is to be loaded from emitting SQL. Used to prevent unwanted + SQL from being emitted. - else: - path = path[prop] + .. versionadded:: 1.4 - if for_strategy is not None: - found_property._get_strategy(for_strategy) - if path.has_entity: - path = path.entity_path - self.path = path - return path + .. seealso:: - def __str__(self): - return "Load(strategy=%r)" % (self.strategy,) + :ref:`deferred_raiseload` - def _coerce_strat(self, strategy): - if strategy is not None: - strategy = tuple(sorted(strategy.items())) - return strategy + .. seealso:: + + :ref:`deferred` + + :func:`_orm.undefer` + + """ + strategy = {"deferred": True, "instrument": True} + if raiseload: + strategy["raiseload"] = True + return self._set_column_strategy((key,), strategy) + + def undefer(self, key): + r"""Indicate that the given column-oriented attribute should be + undeferred, e.g. specified within the SELECT statement of the entity + as a whole. + + The column being undeferred is typically set up on the mapping as a + :func:`.deferred` attribute. - def _apply_to_parent(self, parent, applied, bound): - raise NotImplementedError( - "Only 'unbound' loader options may be used with the " - "Load.options() method" + This function is part of the :class:`_orm.Load` interface and supports + both method-chained and standalone operation. + + Examples:: + + # undefer two columns + session.query(MyClass).options(undefer("col1"), undefer("col2")) + + # undefer all columns specific to a single class using Load + * + session.query(MyClass, MyOtherClass).options( + Load(MyClass).undefer("*")) + + # undefer a column on a related object + session.query(MyClass).options( + defaultload(MyClass.items).undefer('text')) + + :param key: Attribute to be undeferred. + + .. seealso:: + + :ref:`deferred` + + :func:`_orm.defer` + + :func:`_orm.undefer_group` + + """ + return self._set_column_strategy( + (key,), {"deferred": False, "instrument": True} ) - @_generative - def options(self, *opts): - r"""Apply a series of options as sub-options to this - :class:`_orm.Load` - object. + def undefer_group(self, name): + """Indicate that columns within the given deferred group name should be + undeferred. - E.g.:: + The columns being undeferred are set up on the mapping as + :func:`.deferred` attributes and include a "group" name. - query = session.query(Author) - query = query.options( - joinedload(Author.book).options( - load_only(Book.summary, Book.excerpt), - joinedload(Book.citations).options( - joinedload(Citation.author) - ) - ) - ) + E.g:: - :param \*opts: A series of loader option objects (ultimately - :class:`_orm.Load` objects) which should be applied to the path - specified by this :class:`_orm.Load` object. + session.query(MyClass).options(undefer_group("large_attrs")) - .. versionadded:: 1.3.6 + To undefer a group of attributes on a related entity, the path can be + spelled out using relationship loader options, such as + :func:`_orm.defaultload`:: + + session.query(MyClass).options( + defaultload("someattr").undefer_group("large_attrs")) .. seealso:: - :func:`.defaultload` + :ref:`deferred` - :ref:`relationship_loader_options` + :func:`_orm.defer` - :ref:`deferred_loading_w_multiple` + :func:`_orm.undefer` """ - apply_cache = {} - bound = not isinstance(self, _UnboundLoad) - if bound: - raise NotImplementedError( - "The options() method is currently only supported " - "for 'unbound' loader options" + return self._set_column_strategy( + _WILDCARD_TOKEN, None, {f"undefer_group_{name}": True} + ) + + def with_expression(self, key, expression): + r"""Apply an ad-hoc SQL expression to a "deferred expression" + attribute. + + This option is used in conjunction with the + :func:`_orm.query_expression` mapper-level construct that indicates an + attribute which should be the target of an ad-hoc SQL expression. + + E.g.:: + + sess.query(SomeClass).options( + with_expression(SomeClass.x_y_expr, SomeClass.x + SomeClass.y) ) - for opt in opts: - opt._apply_to_parent(self, apply_cache, bound) + + .. versionadded:: 1.2 + + :param key: Attribute to be undeferred. + + :param expr: SQL expression to be applied to the attribute. + + .. note:: the target attribute is populated only if the target object + is **not currently loaded** in the current :class:`_orm.Session` + unless the :ref:`orm_queryguide_populate_existing` execution option + is used. Please refer to :ref:`mapper_querytime_expression` for + complete usage details. + + .. seealso:: + + :ref:`mapper_querytime_expression` + + """ + + expression = coercions.expect( + roles.LabeledColumnExprRole, _orm_full_deannotate(expression) + ) + + return self._set_column_strategy( + (key,), {"query_expression": True}, opts={"expression": expression} + ) + + def selectin_polymorphic(self, classes): + """Indicate an eager load should take place for all attributes + specific to a subclass. + + This uses an additional SELECT with IN against all matched primary + key values, and is the per-query analogue to the ``"selectin"`` + setting on the :paramref:`.mapper.polymorphic_load` parameter. + + .. versionadded:: 1.2 + + .. seealso:: + + :ref:`polymorphic_selectin` + + """ + self = self._set_class_strategy( + {"selectinload_polymorphic": True}, + opts={ + "entities": tuple( + sorted((inspect(cls) for cls in classes), key=id) + ) + }, + ) + return self + + def _coerce_strat(self, strategy): + if strategy is not None: + strategy = tuple(sorted(strategy.items())) + return strategy @_generative - def set_relationship_strategy( - self, attr, strategy, propagate_to_loaders=True - ): + def _set_relationship_strategy( + self, + attr, + strategy, + propagate_to_loaders=True, + opts=None, + _reconcile_to_other=None, + ) -> "_AbstractLoad": 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 - self._of_type = cloned._of_type - self._extra_criteria = cloned._extra_criteria - cloned.is_class_strategy = self.is_class_strategy = False - self.propagate_to_loaders = cloned.propagate_to_loaders + + self._clone_for_bind_strategy( + (attr,), + strategy, + _RELATIONSHIP_TOKEN, + opts=opts, + propagate_to_loaders=propagate_to_loaders, + reconcile_to_other=_reconcile_to_other, + ) + return self @_generative - def set_column_strategy(self, attrs, strategy, opts=None, opts_only=False): + def _set_column_strategy( + self, attrs, strategy, opts=None + ) -> "_AbstractLoad": strategy = self._coerce_strat(strategy) - self.is_class_strategy = False - for attr in attrs: - cloned = self._clone_for_bind_strategy( - attr, strategy, "column", opts_only=opts_only, opts=opts - ) - cloned.propagate_to_loaders = True + + self._clone_for_bind_strategy( + attrs, + strategy, + _COLUMN_TOKEN, + opts=opts, + attr_group=attrs, + ) + return self @_generative - def set_generic_strategy(self, attrs, strategy): + def _set_generic_strategy( + self, attrs, strategy, _reconcile_to_other=None + ) -> "_AbstractLoad": strategy = self._coerce_strat(strategy) - for attr in attrs: - cloned = self._clone_for_bind_strategy(attr, strategy, None) - cloned.propagate_to_loaders = True + self._clone_for_bind_strategy( + attrs, + strategy, + None, + propagate_to_loaders=True, + reconcile_to_other=_reconcile_to_other, + ) + return self @_generative - def set_class_strategy(self, strategy, opts): + def _set_class_strategy(self, strategy, opts) -> "_AbstractLoad": strategy = self._coerce_strat(strategy) - cloned = self._clone_for_bind_strategy(None, strategy, None) - cloned.is_class_strategy = True - cloned.propagate_to_loaders = True - cloned.local_opts.update(opts) - def _clone_for_bind_strategy( - self, attr, strategy, wildcard_key, opts_only=False, opts=None - ): - """Create an anonymous clone of the Load/_UnboundLoad that is suitable - to be placed in the context / _to_bind collection of this Load - object. The clone will then lose references to context/_to_bind - in order to not create reference cycles. + self._clone_for_bind_strategy(None, strategy, None, opts=opts) + return self + + def _apply_to_parent(self, parent): + """apply this :class:`_orm._AbstractLoad` object as a sub-option o + a :class:`_orm.Load` object. + + Implementation is provided by subclasses. """ - cloned = self._generate() - cloned._generate_path(self.path, attr, strategy, wildcard_key) - cloned.strategy = strategy + raise NotImplementedError() - cloned.local_opts = self.local_opts - if opts: - cloned.local_opts.update(opts) - if opts_only: - cloned.is_opts_only = True + def options(self, *opts) -> "_AbstractLoad": + r"""Apply a series of options as sub-options to this + :class:`_orm._AbstractLoad` object. - if strategy or cloned.is_opts_only: - cloned._set_path_strategy() - return cloned + Implementation is provided by subclasses. - def _set_for_path(self, context, path, replace=True, merge_opts=False): - if merge_opts or not replace: - existing = path.get(context, "loader") - if existing: - if merge_opts: - existing.local_opts.update(self.local_opts) - existing._extra_criteria += self._extra_criteria - else: - path.set(context, "loader", self) - else: - existing = path.get(context, "loader") - path.set(context, "loader", self) - if existing and existing.is_opts_only: - self.local_opts.update(existing.local_opts) - existing._extra_criteria += self._extra_criteria - - def _set_path_strategy(self): - if not self.is_class_strategy and self.path.has_entity: - effective_path = self.path.parent - else: - effective_path = self.path + """ + raise NotImplementedError() - if effective_path.is_token: - for path in effective_path.generate_for_superclasses(): - self._set_for_path( - self.context, - path, - replace=True, - merge_opts=self.is_opts_only, - ) - else: - self._set_for_path( - self.context, - effective_path, - replace=True, - merge_opts=self.is_opts_only, - ) + def _clone_for_bind_strategy( + self, + attrs, + strategy, + wildcard_key, + opts=None, + attr_group=None, + propagate_to_loaders=True, + reconcile_to_other=None, + ): + raise NotImplementedError() - # remove cycles; _set_path_strategy is always invoked on an - # anonymous clone of the Load / UnboundLoad object since #5056 - self.context = None + def process_compile_state_replaced_entities( + self, compile_state, mapper_entities + ): + if not compile_state.compile_options._enable_eagerloads: + return - def __getstate__(self): - d = self.__dict__.copy() + # process is being run here so that the options given are validated + # against what the lead entities were, as well as to accommodate + # for the entities having been replaced with equivalents + self._process( + compile_state, + mapper_entities, + not bool(compile_state.current_path), + ) - # can't pickle this right now; warning is raised by strategies - d["_extra_criteria"] = () + def process_compile_state(self, compile_state): + if not compile_state.compile_options._enable_eagerloads: + return - if d["context"] is not None: - d["context"] = PathRegistry.serialize_context_dict( - d["context"], ("loader",) - ) - d["path"] = self.path.serialize() - return d + self._process( + compile_state, + compile_state._lead_mapper_entities, + not bool(compile_state.current_path) + and not compile_state.compile_options._for_refresh_state, + ) - def __setstate__(self, state): - self.__dict__.update(state) - self.path = PathRegistry.deserialize(self.path) - if self.context is not None: - self.context = PathRegistry.deserialize_context_dict(self.context) + def _process(self, compile_state, mapper_entities, raiseerr): + """implemented by subclasses""" + raise NotImplementedError() - def _chop_path(self, to_chop, path): + @classmethod + def _chop_path(cls, to_chop, path, debug=False): i = -1 for i, (c_token, p_token) in enumerate(zip(to_chop, path.path)): if isinstance(c_token, str): - # TODO: this is approximated from the _UnboundLoad - # version and probably has issues, not fully covered. - - if i == 0 and c_token.endswith(":" + _DEFAULT_TOKEN): + if i == 0 and c_token.endswith(f":{_DEFAULT_TOKEN}"): return to_chop elif ( - c_token != "relationship:%s" % (_WILDCARD_TOKEN,) + c_token != f"{_RELATIONSHIP_TOKEN}:{_WILDCARD_TOKEN}" and c_token != p_token.key ): return None @@ -618,413 +792,447 @@ class Load(Generative, LoaderOption): elif ( isinstance(c_token, InspectionAttr) and c_token.is_mapper - and p_token.is_mapper - and c_token.isa(p_token) + and ( + (p_token.is_mapper and c_token.isa(p_token)) + or ( + # a too-liberal check here to allow a path like + # A->A.bs->B->B.cs->C->C.ds, natural path, to chop + # against current path + # A->A.bs->B(B, B2)->B(B, B2)->cs, in an of_type() + # scenario which should only be occurring in a loader + # that is against a non-aliased lead element with + # single path. otherwise the + # "B" wont match into the B(B, B2). + # + # i>=2 prevents this check from proceeding for + # the first path element. + # + # if we could do away with the "natural_path" + # concept, we would not need guessy checks like this + # + # two conflicting tests for this comparison are: + # test_eager_relations.py-> + # test_lazyload_aliased_abs_bcs_two + # and + # test_of_type.py->test_all_subq_query + # + i >= 2 + and p_token.is_aliased_class + and p_token._is_with_polymorphic + and c_token in p_token.with_polymorphic_mappers + # and (breakpoint() or True) + ) + ) ): continue + else: return None return to_chop[i + 1 :] -class _UnboundLoad(Load): - """Represent a loader option that isn't tied to a root entity. +class Load(_AbstractLoad): + """Represents loader options which modify the state of a + ORM-enabled :class:`_sql.Select` or a legacy :class:`_query.Query` in + order to affect how various mapped attributes are loaded. - The loader option will produce an entity-linked :class:`_orm.Load` - object when it is passed :meth:`_query.Query.options`. + The :class:`_orm.Load` object is in most cases used implicitly behind the + scenes when one makes use of a query option like :func:`_orm.joinedload`, + :func:`.defer`, or similar. However, the :class:`_orm.Load` object + can also be used directly, and in some cases can be useful. - This provides compatibility with the traditional system - of freestanding options, e.g. ``joinedload('x.y.z')``. + To use :class:`_orm.Load` directly, instantiate it with the target mapped + class as the argument. This style of usage is + useful when dealing with a statement + that has multiple entities:: - """ + myopt = Load(MyClass).joinedload("widgets") + + The above ``myopt`` can now be used with :meth:`_sql.Select.options` or + :meth:`_query.Query.options` where it + will only take effect for the ``MyClass`` entity:: - def __init__(self): - self.path = () - self._to_bind = [] - self.local_opts = {} - self._extra_criteria = () + stmt = select(MyClass, MyOtherClass).options(myopt) - def _gen_cache_key(self, anon_map, bindparams, _unbound_option_seen=None): - """Inlined gen_cache_key + One case where :class:`_orm.Load` + is useful as public API is when specifying + "wildcard" options that only take effect for a certain class:: - Original traversal is:: + stmt = select(Order).options(Load(Order).lazyload('*')) + Above, all relationships on ``Order`` will be lazy-loaded, but other + attributes on those descendant objects will load using their normal + loader strategy. - _cache_key_traversal = [ - ("path", visitors.ExtendedInternalTraversal.dp_multi_list), - ("strategy", visitors.ExtendedInternalTraversal.dp_plain_obj), - ( - "_to_bind", - visitors.ExtendedInternalTraversal.dp_has_cache_key_list, - ), - ( - "_extra_criteria", - visitors.InternalTraversal.dp_clauseelement_list), - ( - "local_opts", - visitors.ExtendedInternalTraversal.dp_string_multi_dict, - ), - ] + .. seealso:: - The inlining is so that the "_to_bind" list can be flattened to not - repeat the same UnboundLoad options over and over again. + :ref:`deferred_options` - See #6869 + :ref:`deferred_loading_w_multiple` - """ + :ref:`relationship_loader_options` - idself = id(self) - cls = self.__class__ + """ - if idself in anon_map: - return (anon_map[idself], cls) - else: - id_ = anon_map[idself] + _cache_key_traversal = [ + ("path", visitors.ExtendedInternalTraversal.dp_has_cache_key), + ( + "context", + visitors.InternalTraversal.dp_has_cache_key_list, + ), + ] - vis = traversals._cache_key_traversal_visitor + path: PathRegistry + context: Tuple["_LoadElement", ...] - seen = _unbound_option_seen - if seen is None: - seen = set() + def __init__(self, entity): + insp = cast(Union["Mapper", AliasedInsp], inspect(entity)) + insp._post_inspect - return ( - (id_, cls) - + vis.visit_multi_list( - "path", self.path, self, anon_map, bindparams - ) - + ("strategy", self.strategy) - + ( - ( - "_to_bind", - tuple( - elem._gen_cache_key( - anon_map, bindparams, _unbound_option_seen=seen - ) - for elem in self._to_bind - if elem not in seen and not seen.add(elem) - ), - ) - if self._to_bind - else () - ) - + ( - ( - "_extra_criteria", - tuple( - elem._gen_cache_key(anon_map, bindparams) - for elem in self._extra_criteria - ), - ) - if self._extra_criteria - else () - ) - + ( - vis.visit_string_multi_dict( - "local_opts", self.local_opts, self, anon_map, bindparams - ) - if self.local_opts - else () - ) - ) + self.path = insp._path_registry + self.context = () - _is_chain_link = False + def __str__(self): + return f"Load({self.path[0]})" - def _set_path_strategy(self): - self._to_bind.append(self) + @classmethod + def _construct_for_existing_path(cls, path): + load = cls.__new__(cls) + load.path = path + load.context = () + return load - # remove cycles; _set_path_strategy is always invoked on an - # anonymous clone of the Load / UnboundLoad object since #5056 - self._to_bind = None + def _adjust_for_extra_criteria(self, context): + """Apply the current bound parameters in a QueryContext to all + occurrences "extra_criteria" stored within this ``Load`` object, + returning a new instance of this ``Load`` object. - def _deep_clone(self, applied, process): - if self in applied: - return applied[self] + """ + orig_query = context.compile_state.select_statement - cloned = self._generate() + orig_cache_key = None + replacement_cache_key = None - applied[self] = cloned + def process(opt): + if not opt._extra_criteria: + return opt - cloned.strategy = self.strategy + nonlocal orig_cache_key, replacement_cache_key - assert cloned.propagate_to_loaders == self.propagate_to_loaders - assert cloned.is_class_strategy == self.is_class_strategy - assert cloned.is_opts_only == self.is_opts_only + # avoid generating cache keys for the queries if we don't + # actually have any extra_criteria options, which is the + # common case + if orig_cache_key is None or replacement_cache_key is None: + orig_cache_key = orig_query._generate_cache_key() + replacement_cache_key = context.query._generate_cache_key() - cloned._to_bind = [ - elem._deep_clone(applied, process) for elem in self._to_bind or () - ] + opt._extra_criteria = tuple( + replacement_cache_key._apply_params_to_element( + orig_cache_key, crit + ) + for crit in opt._extra_criteria + ) + return opt - cloned.local_opts.update(self.local_opts) + cloned = self._generate() - process(cloned) + if self.context: + cloned.context = tuple( + process(value._clone()) for value in self.context + ) return cloned - def _apply_to_parent(self, parent, applied, bound, to_bind=None): - if self in applied: - return applied[self] + def _reconcile_query_entities_with_us(self, mapper_entities, raiseerr): + """called at process time to allow adjustment of the root + entity inside of _LoadElement objects. - if to_bind is None: - to_bind = self._to_bind + """ + path = self.path - cloned = self._generate() + ezero = None + for ent in mapper_entities: + ezero = ent.entity_zero + if ezero and orm_util._entity_corresponds_to(ezero, path[0]): + return ezero + + return None - applied[self] = cloned + def _process(self, compile_state, mapper_entities, raiseerr): - cloned.strategy = self.strategy - if self.path: - attr = self.path[-1] - if isinstance(attr, str) and attr.endswith(_DEFAULT_TOKEN): - attr = attr.split(":")[0] + ":" + _WILDCARD_TOKEN - cloned._generate_path( - parent.path + self.path[0:-1], attr, self.strategy, None + reconciled_lead_entity = self._reconcile_query_entities_with_us( + mapper_entities, raiseerr + ) + + for loader in self.context: + loader.process_compile_state( + self, + compile_state, + mapper_entities, + reconciled_lead_entity, + raiseerr, ) - # these assertions can go away once the "sub options" API is - # mature + def _apply_to_parent(self, parent): + """apply this :class:`_orm.Load` object as a sub-option of another + :class:`_orm.Load` object. + + This method is used by the :meth:`_orm.Load.options` method. + + """ + cloned = self._generate() + assert cloned.propagate_to_loaders == self.propagate_to_loaders - assert cloned.is_class_strategy == self.is_class_strategy - assert cloned.is_opts_only == self.is_opts_only - uniq = set() + if not orm_util._entity_corresponds_to_use_path_impl( + parent.path[-1], cloned.path[0] + ): + raise sa_exc.ArgumentError( + f'Attribute "{cloned.path[1]}" does not link ' + f'from element "{parent.path[-1]}".' + ) - cloned._to_bind = parent._to_bind + cloned.path = PathRegistry.coerce(parent.path[0:-1] + cloned.path[:]) - cloned._to_bind[:] = [ - elem - for elem in cloned._to_bind - if elem not in uniq and not uniq.add(elem) - ] + [ - elem._apply_to_parent(parent, applied, bound, to_bind) - for elem in to_bind - if elem not in uniq and not uniq.add(elem) - ] + if self.context: + cloned.context = tuple( + value._prepend_path_from(parent) for value in self.context + ) - cloned.local_opts.update(self.local_opts) + if cloned.context: + parent.context += cloned.context - return cloned + @_generative + def options(self, *opts) -> "_AbstractLoad": + r"""Apply a series of options as sub-options to this + :class:`_orm.Load` + object. - def _generate_path(self, path, attr, for_strategy, wildcard_key): - if ( - wildcard_key - and isinstance(attr, str) - and attr in (_WILDCARD_TOKEN, _DEFAULT_TOKEN) - ): - if attr == _DEFAULT_TOKEN: - self.propagate_to_loaders = False - attr = "%s:%s" % (wildcard_key, attr) - if path and _is_mapped_class(path[-1]) and not self.is_class_strategy: - path = path[0:-1] - if attr: - path = path + (attr,) - self.path = path - self._extra_criteria = getattr(attr, "_extra_criteria", ()) + E.g.:: - return path + query = session.query(Author) + query = query.options( + joinedload(Author.book).options( + load_only(Book.summary, Book.excerpt), + joinedload(Book.citations).options( + joinedload(Citation.author) + ) + ) + ) - def __getstate__(self): - d = self.__dict__.copy() + :param \*opts: A series of loader option objects (ultimately + :class:`_orm.Load` objects) which should be applied to the path + specified by this :class:`_orm.Load` object. - # can't pickle this right now; warning is raised by strategies - d["_extra_criteria"] = () + .. versionadded:: 1.3.6 - d["path"] = self._serialize_path(self.path, filter_aliased_class=True) - return d + .. seealso:: - def __setstate__(self, state): - ret = [] - for key in state["path"]: - if isinstance(key, tuple): - if len(key) == 2: - # support legacy - cls, propkey = key - of_type = None - else: - cls, propkey, of_type = key - prop = getattr(cls, propkey) - if of_type: - prop = prop.of_type(of_type) - ret.append(prop) - else: - ret.append(key) - state["path"] = tuple(ret) - self.__dict__ = state + :func:`.defaultload` - def _process(self, compile_state, mapper_entities, raiseerr): - dedupes = compile_state.attributes["_unbound_load_dedupes"] - is_refresh = compile_state.compile_options._for_refresh_state - for val in self._to_bind: - if val not in dedupes: - dedupes.add(val) - if is_refresh and not val.propagate_to_loaders: - continue - val._bind_loader( - [ent.entity_zero for ent in mapper_entities], - compile_state.current_path, - compile_state.attributes, - raiseerr, - ) + :ref:`relationship_loader_options` - @classmethod - def _from_keys(cls, meth, keys, chained, kw): - opt = _UnboundLoad() - - def _split_key(key): - if isinstance(key, str): - # coerce fooload('*') into "default loader strategy" - if key == _WILDCARD_TOKEN: - return (_DEFAULT_TOKEN,) - # coerce fooload(".*") into "wildcard on default entity" - elif key.startswith("." + _WILDCARD_TOKEN): - util.warn_deprecated( - "The undocumented `.{WILDCARD}` format is deprecated " - "and will be removed in a future version as it is " - "believed to be unused. " - "If you have been using this functionality, please " - "comment on Issue #4390 on the SQLAlchemy project " - "tracker.", - version="1.4", - ) - key = key[1:] - return key.split(".") - else: - return (key,) + :ref:`deferred_loading_w_multiple` - all_tokens = [token for key in keys for token in _split_key(key)] + """ + for opt in opts: + opt._apply_to_parent(self) + return self - for token in all_tokens[0:-1]: - # set _is_chain_link first so that clones of the - # object also inherit this flag - opt._is_chain_link = True - if chained: - opt = meth(opt, token, **kw) + def _clone_for_bind_strategy( + self, + attrs, + strategy, + wildcard_key, + opts=None, + attr_group=None, + propagate_to_loaders=True, + reconcile_to_other=None, + ) -> None: + # for individual strategy that needs to propagate, set the whole + # Load container to also propagate, so that it shows up in + # InstanceState.load_options + if propagate_to_loaders: + self.propagate_to_loaders = True + + if not self.path.has_entity: + if self.path.is_token: + raise sa_exc.ArgumentError( + "Wildcard token cannot be followed by another entity" + ) else: - opt = opt.defaultload(token) + # re-use the lookup which will raise a nicely formatted + # LoaderStrategyException + if strategy: + self.path.prop._strategy_lookup( + self.path.prop, strategy[0] + ) + else: + raise sa_exc.ArgumentError( + f"Mapped attribute '{self.path.prop}' does not " + "refer to a mapped entity" + ) - opt = meth(opt, all_tokens[-1], **kw) - opt._is_chain_link = False - return opt + if attrs is None: + load_element = _ClassStrategyLoad.create( + self.path, + None, + strategy, + wildcard_key, + opts, + propagate_to_loaders, + attr_group=attr_group, + reconcile_to_other=reconcile_to_other, + ) + if load_element: + self.context += (load_element,) - def _chop_path(self, to_chop, path): - i = -1 - for i, (c_token, (p_entity, p_prop)) in enumerate( - zip(to_chop, path.pairs()) - ): - if isinstance(c_token, str): - if i == 0 and c_token.endswith(":" + _DEFAULT_TOKEN): - return to_chop - elif ( - c_token != "relationship:%s" % (_WILDCARD_TOKEN,) - and c_token != p_prop.key - ): - return None - elif isinstance(c_token, PropComparator): - if c_token.property is not p_prop or ( - c_token._parententity is not p_entity - and ( - not c_token._parententity.is_mapper - or not c_token._parententity.isa(p_entity) - ) - ): - return None else: - i += 1 - - return to_chop[i:] - - def _serialize_path(self, path, filter_aliased_class=False): - ret = [] - for token in path: - if isinstance(token, QueryableAttribute): - if ( - filter_aliased_class - and token._of_type - and inspect(token._of_type).is_aliased_class - ): - ret.append((token._parentmapper.class_, token.key, None)) + for attr in attrs: + if isinstance(attr, str): + load_element = _TokenStrategyLoad.create( + self.path, + attr, + strategy, + wildcard_key, + opts, + propagate_to_loaders, + attr_group=attr_group, + reconcile_to_other=reconcile_to_other, + ) else: - ret.append( - ( - token._parentmapper.class_, - token.key, - token._of_type.entity if token._of_type else None, - ) + load_element = _AttributeStrategyLoad.create( + self.path, + attr, + strategy, + wildcard_key, + opts, + propagate_to_loaders, + attr_group=attr_group, + reconcile_to_other=reconcile_to_other, ) - elif isinstance(token, PropComparator): - ret.append((token._parentmapper.class_, token.key, None)) - else: - ret.append(token) - return ret - def _bind_loader(self, entities, current_path, context, raiseerr): - """Convert from an _UnboundLoad() object into a Load() object. + if load_element: + # for relationship options, update self.path on this Load + # object with the latest path. + if wildcard_key is _RELATIONSHIP_TOKEN: + self.path = load_element.path + self.context += (load_element,) - The _UnboundLoad() uses an informal "path" and does not necessarily - refer to a lead entity as it may use string tokens. The Load() - OTOH refers to a complete path. This method reconciles from a - given Query into a Load. + def __getstate__(self): + d = self.__dict__.copy() + d["path"] = self.path.serialize() + return d - Example:: + def __setstate__(self, state): + self.__dict__.update(state) + self.path = PathRegistry.deserialize(self.path) - query = session.query(User).options( - joinedload("orders").joinedload("items")) +class _WildcardLoad(_AbstractLoad): + """represent a standalone '*' load operation""" - The above options will be an _UnboundLoad object along the lines - of (note this is not the exact API of _UnboundLoad):: + _cache_key_traversal = [ + ("strategy", visitors.ExtendedInternalTraversal.dp_plain_obj), + ( + "local_opts", + visitors.ExtendedInternalTraversal.dp_string_multi_dict, + ), + ] - _UnboundLoad( - _to_bind=[ - _UnboundLoad(["orders"], {"lazy": "joined"}), - _UnboundLoad(["orders", "items"], {"lazy": "joined"}), - ] - ) + local_opts = util.EMPTY_DICT + path: Tuple[str, ...] = () + + def _clone_for_bind_strategy( + self, + attrs, + strategy, + wildcard_key, + opts=None, + attr_group=None, + propagate_to_loaders=True, + reconcile_to_other=None, + ): + attr = attrs[0] + assert ( + wildcard_key + and isinstance(attr, str) + and attr in (_WILDCARD_TOKEN, _DEFAULT_TOKEN) + ) + + if attr == _DEFAULT_TOKEN: + # for someload('*'), this currently does propagate=False, + # to prevent it from taking effect for lazy loads. + # it seems like adjusting for current_path for a lazy load etc. + # should be taking care of that, so that the option still takes + # effect for a refresh as well, but currently it does not. + # probably should be adjusted to be more accurate re: current + # path vs. refresh + self.propagate_to_loaders = False + + attr = f"{wildcard_key}:{attr}" - After this method, we get something more like this (again this is - not exact API):: + self.strategy = strategy + self.path = (attr,) + if opts: + self.local_opts = util.immutabledict(opts) + + def options(self, *opts) -> "_AbstractLoad": + raise NotImplementedError("Star option does not support sub-options") - Load( - User, - (User, User.orders.property)) - Load( - User, - (User, User.orders.property, Order, Order.items.property)) + def _apply_to_parent(self, parent): + """apply this :class:`_orm._WildcardLoad` object as a sub-option of + a :class:`_orm.Load` object. + + This method is used by the :meth:`_orm.Load.options` method. Note + that :class:`_orm.WildcardLoad` itself can't have sub-options, but + it may be used as the sub-option of a :class:`_orm.Load` object. """ - start_path = self.path + attr = self.path[0] + if attr.endswith(_DEFAULT_TOKEN): + attr = f"{attr.split(':')[0]}:{_WILDCARD_TOKEN}" - if self.is_class_strategy and current_path: - start_path += (entities[0],) + effective_path = parent.path.token(attr) - # _current_path implies we're in a - # secondary load with an existing path + assert effective_path.is_token + loader = _TokenStrategyLoad.create( + effective_path, + None, + self.strategy, + None, + self.local_opts, + self.propagate_to_loaders, + ) + + parent.context += (loader,) + + def _process(self, compile_state, mapper_entities, raiseerr): + is_refresh = compile_state.compile_options._for_refresh_state + + if is_refresh and not self.propagate_to_loaders: + return + + entities = [ent.entity_zero for ent in mapper_entities] + current_path = compile_state.current_path + + start_path = self.path + + # TODO: chop_path already occurs in loader.process_compile_state() + # so we will seek to simplify this if current_path: start_path = self._chop_path(start_path, current_path) + if not start_path: + return - if not start_path: - return None + # start_path is a single-token tuple + assert start_path and len(start_path) == 1 - # look at the first token and try to locate within the Query - # what entity we are referring towards. token = start_path[0] - if isinstance(token, str): - entity = self._find_entity_basestring(entities, token, raiseerr) - elif isinstance(token, PropComparator): - prop = token.property - entity = self._find_entity_prop_comparator( - entities, prop, token._parententity, raiseerr - ) - elif self.is_class_strategy and _is_mapped_class(token): - entity = inspect(token) - if entity not in entities: - entity = None - else: - raise sa_exc.ArgumentError( - "mapper option expects " "string key or list of attributes" - ) + entity = self._find_entity_basestring(entities, token, raiseerr) if not entity: return @@ -1035,103 +1243,43 @@ class _UnboundLoad(Load): # with a real entity path. Start with the lead entity # we just located, then go through the rest of our path # tokens and populate into the Load(). - loader = Load(path_element) - - if context is None: - context = loader.context - - loader.strategy = self.strategy - loader.is_opts_only = self.is_opts_only - loader.is_class_strategy = self.is_class_strategy - loader._extra_criteria = self._extra_criteria - - path = loader.path - - if not loader.is_class_strategy: - for idx, token in enumerate(start_path): - if not loader._generate_path( - loader.path, - token, - self.strategy if idx == len(start_path) - 1 else None, - None, - raiseerr, - polymorphic_entity_context=context, - ): - return - loader.local_opts.update(self.local_opts) + loader = _TokenStrategyLoad.create( + path_element._path_registry, + token, + self.strategy, + None, + self.local_opts, + self.propagate_to_loaders, + raiseerr=raiseerr, + ) + if not loader: + return - if not loader.is_class_strategy and loader.path.has_entity: - effective_path = loader.path.parent - else: - effective_path = loader.path - - # prioritize "first class" options over those - # that were "links in the chain", e.g. "x" and "y" in - # someload("x.y.z") versus someload("x") / someload("x.y") - - if effective_path.is_token: - for path in effective_path.generate_for_superclasses(): - loader._set_for_path( - context, - path, - replace=not self._is_chain_link, - merge_opts=self.is_opts_only, - ) - else: - loader._set_for_path( - context, - effective_path, - replace=not self._is_chain_link, - merge_opts=self.is_opts_only, - ) + assert loader.path.is_token - return loader + # don't pass a reconciled lead entity here + loader.process_compile_state( + self, compile_state, mapper_entities, None, raiseerr + ) - def _find_entity_prop_comparator(self, entities, prop, mapper, raiseerr): - if _is_aliased_class(mapper): - searchfor = mapper - else: - searchfor = _class_to_mapper(mapper) - for ent in entities: - if orm_util._entity_corresponds_to(ent, searchfor): - return ent - else: - if raiseerr: - if not list(entities): - raise sa_exc.ArgumentError( - "Query has only expression-based entities, " - 'which do not apply to %s "%s"' - % (util.clsname_as_plain_name(type(prop)), prop) - ) - else: - raise sa_exc.ArgumentError( - 'Mapped attribute "%s" does not apply to any of the ' - "root entities in this query, e.g. %s. Please " - "specify the full path " - "from one of the root entities to the target " - "attribute. " - % (prop, ", ".join(str(x) for x in entities)) - ) - else: - return None + return loader def _find_entity_basestring(self, entities, token, raiseerr): - if token.endswith(":" + _WILDCARD_TOKEN): + if token.endswith(f":{_WILDCARD_TOKEN}"): if len(list(entities)) != 1: if raiseerr: raise sa_exc.ArgumentError( "Can't apply wildcard ('*') or load_only() " - "loader option to multiple entities %s. Specify " - "loader options for each entity individually, such " - "as %s." - % ( - ", ".join(str(ent) for ent in entities), + f"loader option to multiple entities " + f"{', '.join(str(ent) for ent in entities)}. Specify " + "loader options for each entity individually, such as " + f"""{ ", ".join( - "Load(%s).some_option('*')" % ent + f"Load({ent}).some_option('*')" for ent in entities - ), - ) + ) + }.""" ) elif token.endswith(_DEFAULT_TOKEN): raiseerr = False @@ -1145,623 +1293,866 @@ class _UnboundLoad(Load): if raiseerr: raise sa_exc.ArgumentError( "Query has only expression-based entities - " - 'can\'t find property named "%s".' % (token,) + f'can\'t find property named "{token}".' ) else: return None + def __getstate__(self): + return self.__dict__.copy() -class loader_option: - def __init__(self): - pass - - def __call__(self, fn): - self.name = name = fn.__name__ - self.fn = fn - if hasattr(Load, name): - raise TypeError("Load class already has a %s method." % (name)) - setattr(Load, name, fn) - - return self - - def _add_unbound_fn(self, fn): - self._unbound_fn = fn - fn_doc = self.fn.__doc__ - self.fn.__doc__ = """Produce a new :class:`_orm.Load` object with the -:func:`_orm.%(name)s` option applied. - -See :func:`_orm.%(name)s` for usage examples. - -""" % { - "name": self.name - } - - fn.__doc__ = fn_doc - return self - - def _add_unbound_all_fn(self, fn): - fn.__doc__ = """Produce a standalone "all" option for -:func:`_orm.%(name)s`. - -.. deprecated:: 0.9 + def __setstate__(self, state): + self.__dict__.update(state) - The :func:`_orm.%(name)s_all` function is deprecated, and will be removed - in a future release. Please use method chaining with - :func:`_orm.%(name)s` instead, as in:: - session.query(MyClass).options( - %(name)s("someattribute").%(name)s("anotherattribute") - ) +class _LoadElement(traversals.HasCacheKey): + """represents strategy information to select for a LoaderStrategy + and pass options to it. -""" % { - "name": self.name - } - fn = util.deprecated( - # This is used by `baked_lazyload_all` was only deprecated in - # version 1.2 so this must stick around until that is removed - "0.9", - "The :func:`.%(name)s_all` function is deprecated, and will be " - "removed in a future release. Please use method chaining with " - ":func:`.%(name)s` instead" % {"name": self.name}, - add_deprecation_to_docstring=False, - )(fn) - - self._unbound_all_fn = fn - return self + :class:`._LoadElement` objects provide the inner datastructure + stored by a :class:`_orm.Load` object and are also the object passed + to methods like :meth:`.LoaderStrategy.setup_query`. + .. versionadded:: 2.0 -@loader_option() -def contains_eager(loadopt, attr, alias=None): - r"""Indicate that the given attribute should be eagerly loaded from - columns stated manually in the query. + """ - This function is part of the :class:`_orm.Load` interface and supports - both method-chained and standalone operation. + _cache_key_traversal = [ + ("path", visitors.ExtendedInternalTraversal.dp_has_cache_key), + ("strategy", visitors.ExtendedInternalTraversal.dp_plain_obj), + ( + "local_opts", + visitors.ExtendedInternalTraversal.dp_string_multi_dict, + ), + ] - The option is used in conjunction with an explicit join that loads - the desired rows, i.e.:: + _extra_criteria = () - sess.query(Order).\ - join(Order.user).\ - options(contains_eager(Order.user)) + _reconcile_to_other = None + strategy = None + path: PathRegistry + propagate_to_loaders = False - The above query would join from the ``Order`` entity to its related - ``User`` entity, and the returned ``Order`` objects would have the - ``Order.user`` attribute pre-populated. + local_opts: Mapping[str, Any] - It may also be used for customizing the entries in an eagerly loaded - collection; queries will normally want to use the - :meth:`_query.Query.populate_existing` method assuming the primary - collection of parent objects may already have been loaded:: + is_token_strategy: bool + is_class_strategy: bool - sess.query(User).\ - join(User.addresses).\ - filter(Address.email_address.like('%@aol.com')).\ - options(contains_eager(User.addresses)).\ - populate_existing() + @property + def is_opts_only(self): + return bool(self.local_opts and self.strategy is None) - See the section :ref:`contains_eager` for complete usage details. + def __getstate__(self): + d = self.__dict__.copy() + d["path"] = self.path.serialize() - .. seealso:: + return d - :ref:`loading_toplevel` + def __setstate__(self, state): + state["path"] = PathRegistry.deserialize(state["path"]) + self.__dict__.update(state) - :ref:`contains_eager` + def _raise_for_no_match(self, parent_loader, mapper_entities): + path = parent_loader.path - """ - if alias is not None: - if not isinstance(alias, str): - info = inspect(alias) - alias = info.selectable + found_entities = False + for ent in mapper_entities: + ezero = ent.entity_zero + if ezero: + found_entities = True + break + if not found_entities: + raise sa_exc.ArgumentError( + "Query has only expression-based entities; " + f"attribute loader options for {path[0]} can't " + "be applied here." + ) else: - util.warn_deprecated( - "Passing a string name for the 'alias' argument to " - "'contains_eager()` is deprecated, and will not work in a " - "future release. Please use a sqlalchemy.alias() or " - "sqlalchemy.orm.aliased() construct.", - version="1.4", + raise sa_exc.ArgumentError( + f"Mapped class {path[0]} does not apply to any of the " + f"root entities in this query, e.g. " + f"""{ + ", ".join(str(x.entity_zero) + for x in mapper_entities if x.entity_zero + )}. Please """ + "specify the full path " + "from one of the root entities to the target " + "attribute. " ) - elif getattr(attr, "_of_type", None): - ot = inspect(attr._of_type) - alias = ot.selectable + def _adjust_effective_path_for_current_path( + self, effective_path, current_path + ): + """receives the 'current_path' entry from an :class:`.ORMCompileState` + instance, which is set during lazy loads and secondary loader strategy + loads, and adjusts the given path to be relative to the + current_path. - cloned = loadopt.set_relationship_strategy( - attr, {"lazy": "joined"}, propagate_to_loaders=False - ) - cloned.local_opts["eager_from_alias"] = alias - return cloned + E.g. given a loader path and current path:: + lp: User -> orders -> Order -> items -> Item -> keywords -> Keyword -@contains_eager._add_unbound_fn -def contains_eager(*keys, **kw): - return _UnboundLoad()._from_keys( - _UnboundLoad.contains_eager, keys, True, kw - ) + cp: User -> orders -> Order -> items + The adjusted path would be:: -@loader_option() -def load_only(loadopt, *attrs): - """Indicate that for a particular entity, only the given list - of column-based attribute names should be loaded; all others will be - deferred. + Item -> keywords -> Keyword - This function is part of the :class:`_orm.Load` interface and supports - both method-chained and standalone operation. - Example - given a class ``User``, load only the ``name`` and ``fullname`` - attributes:: + """ + chopped_start_path = Load._chop_path(effective_path, current_path) + if not chopped_start_path: + return None - session.query(User).options(load_only(User.name, User.fullname)) + tokens_removed_from_start_path = len(effective_path) - len( + chopped_start_path + ) - Example - given a relationship ``User.addresses -> Address``, specify - subquery loading for the ``User.addresses`` collection, but on each - ``Address`` object load only the ``email_address`` attribute:: + loader_lead_path_element = self.path[tokens_removed_from_start_path] - session.query(User).options( - subqueryload(User.addresses).load_only(Address.email_address) + effective_path = PathRegistry.coerce( + (loader_lead_path_element,) + chopped_start_path[1:] ) - For a :class:`_query.Query` that has multiple entities, - the lead entity can be - specifically referred to using the :class:`_orm.Load` constructor:: + return effective_path - session.query(User, Address).join(User.addresses).options( - Load(User).load_only(User.name, User.fullname), - Load(Address).load_only(Address.email_address) - ) + def _init_path(self, path, attr, wildcard_key, attr_group, raiseerr): + """Apply ORM attributes and/or wildcard to an existing path, producing + a new path. - .. note:: This method will still load a :class:`_schema.Column` even - if the column property is defined with ``deferred=True`` - for the :func:`.column_property` function. + This method is used within the :meth:`.create` method to initialize + a :class:`._LoadElement` object. - .. versionadded:: 0.9.0 + """ + raise NotImplementedError() - """ - cloned = loadopt.set_column_strategy( - attrs, {"deferred": False, "instrument": True} - ) - cloned.set_column_strategy( - "*", {"deferred": True, "instrument": True}, {"undefer_pks": True} - ) - return cloned + def _prepare_for_compile_state( + self, + parent_loader, + compile_state, + mapper_entities, + reconciled_lead_entity, + raiseerr, + ): + """implemented by subclasses.""" + raise NotImplementedError() + def process_compile_state( + self, + parent_loader, + compile_state, + mapper_entities, + reconciled_lead_entity, + raiseerr, + ): + """populate ORMCompileState.attributes with loader state for this + _LoadElement. -@load_only._add_unbound_fn -def load_only(*attrs): - return _UnboundLoad().load_only(*attrs) + """ + keys = self._prepare_for_compile_state( + parent_loader, + compile_state, + mapper_entities, + reconciled_lead_entity, + raiseerr, + ) + for key in keys: + if key in compile_state.attributes: + compile_state.attributes[key] = _LoadElement._reconcile( + self, compile_state.attributes[key] + ) + else: + compile_state.attributes[key] = self + @classmethod + def create( + cls, + path, + attr, + strategy, + wildcard_key, + local_opts, + propagate_to_loaders, + raiseerr=True, + attr_group=None, + reconcile_to_other=None, + ): + """Create a new :class:`._LoadElement` object.""" + + opt = cls.__new__(cls) + opt.path = path + opt.strategy = strategy + opt.propagate_to_loaders = propagate_to_loaders + opt.local_opts = ( + util.immutabledict(local_opts) if local_opts else util.EMPTY_DICT + ) -@loader_option() -def joinedload(loadopt, attr, innerjoin=None): - """Indicate that the given attribute should be loaded using joined - eager loading. + if reconcile_to_other is not None: + opt._reconcile_to_other = reconcile_to_other + elif strategy is None and not local_opts: + opt._reconcile_to_other = True - This function is part of the :class:`_orm.Load` interface and supports - both method-chained and standalone operation. + path = opt._init_path(path, attr, wildcard_key, attr_group, raiseerr) - examples:: + if not path: + return None - # joined-load the "orders" collection on "User" - query(User).options(joinedload(User.orders)) + assert opt.is_token_strategy == path.is_token - # joined-load Order.items and then Item.keywords - query(Order).options( - joinedload(Order.items).joinedload(Item.keywords)) + opt.path = path + return opt - # lazily load Order.items, but when Items are loaded, - # joined-load the keywords collection - query(Order).options( - lazyload(Order.items).joinedload(Item.keywords)) + def __init__(self, path, strategy, local_opts, propagate_to_loaders): + raise NotImplementedError() - :param innerjoin: if ``True``, indicates that the joined eager load should - use an inner join instead of the default of left outer join:: + def _clone(self): + cls = self.__class__ + s = cls.__new__(cls) + s.__dict__ = self.__dict__.copy() + return s - query(Order).options(joinedload(Order.user, innerjoin=True)) + def _prepend_path_from(self, parent): + """adjust the path of this :class:`._LoadElement` to be + a subpath of that of the given parent :class:`_orm.Load` object's + path. - In order to chain multiple eager joins together where some may be - OUTER and others INNER, right-nested joins are used to link them:: + This is used by the :meth:`_orm.Load._apply_to_parent` method, + which is in turn part of the :meth:`_orm.Load.options` method. - query(A).options( - joinedload(A.bs, innerjoin=False). - joinedload(B.cs, innerjoin=True) - ) + """ + cloned = self._clone() - The above query, linking A.bs via "outer" join and B.cs via "inner" join - would render the joins as "a LEFT OUTER JOIN (b JOIN c)". When using - older versions of SQLite (< 3.7.16), this form of JOIN is translated to - use full subqueries as this syntax is otherwise not directly supported. + assert cloned.strategy == self.strategy + assert cloned.local_opts == self.local_opts + assert cloned.is_class_strategy == self.is_class_strategy - The ``innerjoin`` flag can also be stated with the term ``"unnested"``. - This indicates that an INNER JOIN should be used, *unless* the join - is linked to a LEFT OUTER JOIN to the left, in which case it - will render as LEFT OUTER JOIN. For example, supposing ``A.bs`` - is an outerjoin:: + if not orm_util._entity_corresponds_to_use_path_impl( + parent.path[-1], cloned.path[0] + ): + raise sa_exc.ArgumentError( + f'Attribute "{cloned.path[1]}" does not link ' + f'from element "{parent.path[-1]}".' + ) - query(A).options( - joinedload(A.bs). - joinedload(B.cs, innerjoin="unnested") - ) + cloned.path = PathRegistry.coerce(parent.path[0:-1] + cloned.path[:]) - The above join will render as "a LEFT OUTER JOIN b LEFT OUTER JOIN c", - rather than as "a LEFT OUTER JOIN (b JOIN c)". + return cloned - .. note:: The "unnested" flag does **not** affect the JOIN rendered - from a many-to-many association table, e.g. a table configured - as :paramref:`_orm.relationship.secondary`, to the target table; for - correctness of results, these joins are always INNER and are - therefore right-nested if linked to an OUTER join. + @staticmethod + def _reconcile(replacement, existing): + """define behavior for when two Load objects are to be put into + the context.attributes under the same key. - .. versionchanged:: 1.0.0 ``innerjoin=True`` now implies - ``innerjoin="nested"``, whereas in 0.9 it implied - ``innerjoin="unnested"``. In order to achieve the pre-1.0 "unnested" - inner join behavior, use the value ``innerjoin="unnested"``. - See :ref:`migration_3008`. + :param replacement: ``_LoadElement`` that seeks to replace the + existing one - .. note:: + :param existing: ``_LoadElement`` that is already present. - The joins produced by :func:`_orm.joinedload` are **anonymously - aliased**. The criteria by which the join proceeds cannot be - modified, nor can the :class:`_query.Query` - refer to these joins in any way, - including ordering. See :ref:`zen_of_eager_loading` for further - detail. + """ + # mapper inheritance loading requires fine-grained "block other + # options" / "allow these options to be overridden" behaviors + # see test_poly_loading.py + + if replacement._reconcile_to_other: + return existing + elif replacement._reconcile_to_other is False: + return replacement + elif existing._reconcile_to_other: + return replacement + elif existing._reconcile_to_other is False: + return existing + + if existing is replacement: + return replacement + elif ( + existing.strategy == replacement.strategy + and existing.local_opts == replacement.local_opts + ): + return replacement + elif replacement.is_opts_only: + existing = existing._clone() + existing.local_opts = existing.local_opts.union( + replacement.local_opts + ) + existing._extra_criteria += replacement._extra_criteria + return existing + elif existing.is_opts_only: + replacement = replacement._clone() + replacement.local_opts = replacement.local_opts.union( + existing.local_opts + ) + replacement._extra_criteria += replacement._extra_criteria + return replacement + elif replacement.path.is_token: + # use 'last one wins' logic for wildcard options. this is also + # kind of inconsistent vs. options that are specific paths which + # will raise as below + return replacement + + raise sa_exc.InvalidRequestError( + f"Loader strategies for {replacement.path} conflict" + ) - To produce a specific SQL JOIN which is explicitly available, use - :meth:`_query.Query.join`. - To combine explicit JOINs with eager loading - of collections, use :func:`_orm.contains_eager`; see - :ref:`contains_eager`. - .. seealso:: +class _AttributeStrategyLoad(_LoadElement): + """Loader strategies against specific relationship or column paths. - :ref:`loading_toplevel` + e.g.:: - :ref:`joined_eager_loading` + joinedload(User.addresses) + defer(Order.name) + selectinload(User.orders).lazyload(Order.items) """ - loader = loadopt.set_relationship_strategy(attr, {"lazy": "joined"}) - if innerjoin is not None: - loader.local_opts["innerjoin"] = innerjoin - return loader + _cache_key_traversal = _LoadElement._cache_key_traversal + [ + ("_of_type", visitors.ExtendedInternalTraversal.dp_multi), + ("_extra_criteria", visitors.InternalTraversal.dp_clauseelement_list), + ] -@joinedload._add_unbound_fn -def joinedload(*keys, **kw): - return _UnboundLoad._from_keys(_UnboundLoad.joinedload, keys, False, kw) - + _of_type: Union["Mapper", AliasedInsp, None] = None + _path_with_polymorphic_path = None -@loader_option() -def subqueryload(loadopt, attr): - """Indicate that the given attribute should be loaded using - subquery eager loading. + inherit_cache = True + is_class_strategy = False + is_token_strategy = False + + def _init_path(self, path, attr, wildcard_key, attr_group, raiseerr): + assert attr is not None + insp, _, prop = _parse_attr_argument(attr) + + if insp.is_property: + # direct property can be sent from internal strategy logic + # that sets up specific loaders, such as + # emit_lazyload->_lazyload_reverse + # prop = found_property = attr + prop = attr + path = path[prop] - This function is part of the :class:`_orm.Load` interface and supports - both method-chained and standalone operation. + if path.has_entity: + path = path.entity_path + return path - examples:: + elif not insp.is_attribute: + # should not reach here; + assert False - # subquery-load the "orders" collection on "User" - query(User).options(subqueryload(User.orders)) + # here we assume we have user-passed InstrumentedAttribute + if not orm_util._entity_corresponds_to_use_path_impl( + path[-1], attr.parent + ): + if raiseerr: + if attr_group and attr is not attr_group[0]: + raise sa_exc.ArgumentError( + "Can't apply wildcard ('*') or load_only() " + "loader option to multiple entities in the " + "same option. Use separate options per entity." + ) + elif len(path) > 1: + path_is_of_type = ( + path[-1].entity is not path[-2].mapper.class_ + ) + raise sa_exc.ArgumentError( + f'ORM mapped attribute "{attr}" does not ' + f'link from relationship "{path[-2]}%s".%s' + % ( + f".of_type({path[-1]})" if path_is_of_type else "", + ( + " Did you mean to use " + f'"{path[-2]}' + f'.of_type({attr.class_.__name__})"?' + if not path_is_of_type + and not path[-1].is_aliased_class + and orm_util._entity_corresponds_to( + path.entity, attr.parent.mapper + ) + else "" + ), + ) + ) + else: + raise sa_exc.ArgumentError( + f'ORM mapped attribute "{attr}" does not ' + f'link mapped class "{path[-1]}"' + ) + else: + return None - # subquery-load Order.items and then Item.keywords - query(Order).options( - subqueryload(Order.items).subqueryload(Item.keywords)) + # note the essential logic of this attribute was very different in + # 1.4, where there were caching failures in e.g. + # test_relationship_criteria.py::RelationshipCriteriaTest:: + # test_selectinload_nested_criteria[True] if an existing + # "_extra_criteria" on a Load object were replaced with that coming + # from an attribute. This appears to have been an artifact of how + # _UnboundLoad / Load interacted together, which was opaque and + # poorly defined. + self._extra_criteria = attr._extra_criteria - # lazily load Order.items, but when Items are loaded, - # subquery-load the keywords collection - query(Order).options( - lazyload(Order.items).subqueryload(Item.keywords)) + if getattr(attr, "_of_type", None): + ac = attr._of_type + ext_info = inspect(ac) + self._of_type = ext_info + self._path_with_polymorphic_path = path.entity_path[prop] - .. seealso:: + path = path[prop][ext_info] - :ref:`loading_toplevel` + else: + path = path[prop] - :ref:`subquery_eager_loading` + if path.has_entity: + path = path.entity_path - """ - return loadopt.set_relationship_strategy(attr, {"lazy": "subquery"}) + return path + def _generate_extra_criteria(self, context): + """Apply the current bound parameters in a QueryContext to the + immediate "extra_criteria" stored with this Load object. -@subqueryload._add_unbound_fn -def subqueryload(*keys): - return _UnboundLoad._from_keys(_UnboundLoad.subqueryload, keys, False, {}) + Load objects are typically pulled from the cached version of + the statement from a QueryContext. The statement currently being + executed will have new values (and keys) for bound parameters in the + extra criteria which need to be applied by loader strategies when + they handle this criteria for a result set. + """ -@loader_option() -def selectinload(loadopt, attr): - """Indicate that the given attribute should be loaded using - SELECT IN eager loading. + assert ( + self._extra_criteria + ), "this should only be called if _extra_criteria is present" - This function is part of the :class:`_orm.Load` interface and supports - both method-chained and standalone operation. + orig_query = context.compile_state.select_statement + current_query = context.query - examples:: + # NOTE: while it seems like we should not do the "apply" operation + # here if orig_query is current_query, skipping it in the "optimized" + # case causes the query to be different from a cache key perspective, + # because we are creating a copy of the criteria which is no longer + # the same identity of the _extra_criteria in the loader option + # itself. cache key logic produces a different key for + # (A, copy_of_A) vs. (A, A), because in the latter case it shortens + # the second part of the key to just indicate on identity. - # selectin-load the "orders" collection on "User" - query(User).options(selectinload(User.orders)) + # if orig_query is current_query: + # not cached yet. just do the and_() + # return and_(*self._extra_criteria) - # selectin-load Order.items and then Item.keywords - query(Order).options( - selectinload(Order.items).selectinload(Item.keywords)) + k1 = orig_query._generate_cache_key() + k2 = current_query._generate_cache_key() - # lazily load Order.items, but when Items are loaded, - # selectin-load the keywords collection - query(Order).options( - lazyload(Order.items).selectinload(Item.keywords)) + return k2._apply_params_to_element(k1, and_(*self._extra_criteria)) - .. versionadded:: 1.2 + def _set_of_type_info(self, context, current_path): + assert self._path_with_polymorphic_path + + pwpi = self._of_type + assert pwpi + if not pwpi.is_aliased_class: + pwpi = inspect( + orm_util.with_polymorphic( + pwpi.mapper.base_mapper, + pwpi.mapper, + aliased=True, + _use_mapper_path=True, + ) + ) + start_path = self._path_with_polymorphic_path + if current_path: - .. seealso:: + start_path = self._adjust_effective_path_for_current_path( + start_path, current_path + ) + if start_path is None: + return - :ref:`loading_toplevel` + key = ("path_with_polymorphic", start_path.natural_path) + if key in context: + existing_aliased_insp = context[key] + this_aliased_insp = pwpi + new_aliased_insp = existing_aliased_insp._merge_with( + this_aliased_insp + ) + context[key] = new_aliased_insp + else: + context[key] = pwpi - :ref:`selectin_eager_loading` + def _prepare_for_compile_state( + self, + parent_loader, + compile_state, + mapper_entities, + reconciled_lead_entity, + raiseerr, + ): + # _AttributeStrategyLoad - """ - return loadopt.set_relationship_strategy(attr, {"lazy": "selectin"}) + current_path = compile_state.current_path + is_refresh = compile_state.compile_options._for_refresh_state + assert not self.path.is_token + if is_refresh and not self.propagate_to_loaders: + return [] -@selectinload._add_unbound_fn -def selectinload(*keys): - return _UnboundLoad._from_keys(_UnboundLoad.selectinload, keys, False, {}) + if self._of_type: + # apply additional with_polymorphic alias that may have been + # generated. this has to happen even if this is a defaultload + self._set_of_type_info(compile_state.attributes, current_path) + # omit setting loader attributes for a "defaultload" type of option + if not self.strategy and not self.local_opts: + return [] -@loader_option() -def lazyload(loadopt, attr): - """Indicate that the given attribute should be loaded using "lazy" - loading. + if raiseerr and not reconciled_lead_entity: + self._raise_for_no_match(parent_loader, mapper_entities) - This function is part of the :class:`_orm.Load` interface and supports - both method-chained and standalone operation. + if self.path.has_entity: + effective_path = self.path.parent + else: + effective_path = self.path - .. seealso:: + if current_path: + effective_path = self._adjust_effective_path_for_current_path( + effective_path, current_path + ) + if effective_path is None: + return [] - :ref:`loading_toplevel` + return [("loader", cast(PathRegistry, effective_path).natural_path)] - :ref:`lazy_loading` + def __getstate__(self): + d = self.__dict__.copy() + d["_extra_criteria"] = () + d["path"] = self.path.serialize() - """ - return loadopt.set_relationship_strategy(attr, {"lazy": "select"}) + # TODO: we hope to do this logic only at compile time so that + # we aren't carrying these extra attributes around + if self._path_with_polymorphic_path: + d[ + "_path_with_polymorphic_path" + ] = self._path_with_polymorphic_path.serialize() + + if self._of_type: + if self._of_type.is_aliased_class: + d["_of_type"] = None + elif self._of_type.is_mapper: + d["_of_type"] = self._of_type.class_ + else: + assert False, "unexpected object for _of_type" + return d -@lazyload._add_unbound_fn -def lazyload(*keys): - return _UnboundLoad._from_keys(_UnboundLoad.lazyload, keys, False, {}) + def __setstate__(self, state): + state["path"] = PathRegistry.deserialize(state["path"]) + self.__dict__.update(state) + if "_path_with_polymorphic_path" in state: + self._path_with_polymorphic_path = PathRegistry.deserialize( + self._path_with_polymorphic_path + ) + if self._of_type is not None: + self._of_type = inspect(self._of_type) -@loader_option() -def immediateload(loadopt, attr): - """Indicate that the given attribute should be loaded using - an immediate load with a per-attribute SELECT statement. +class _TokenStrategyLoad(_LoadElement): + """Loader strategies against wildcard attributes - The load is achieved using the "lazyloader" strategy and does not - fire off any additional eager loaders. + e.g.:: - The :func:`.immediateload` option is superseded in general - by the :func:`.selectinload` option, which performs the same task - more efficiently by emitting a SELECT for all loaded objects. + raiseload('*') + Load(User).lazyload('*') + defer('*') + load_only(User.name, User.email) # will create a defer('*') + joinedload(User.addresses).raiseload('*') - This function is part of the :class:`_orm.Load` interface and supports - both method-chained and standalone operation. + """ - .. seealso:: + inherit_cache = True + is_class_strategy = False + is_token_strategy = True - :ref:`loading_toplevel` + def _init_path(self, path, attr, wildcard_key, attr_group, raiseerr): + # assert isinstance(attr, str) or attr is None + if attr is not None: + default_token = attr.endswith(_DEFAULT_TOKEN) + if attr.endswith(_WILDCARD_TOKEN) or default_token: + if wildcard_key: + attr = f"{wildcard_key}:{attr}" - :ref:`selectin_eager_loading` + path = path.token(attr) + return path + else: + raise sa_exc.ArgumentError( + "Strings are not accepted for attribute names in loader " + "options; please use class-bound attributes directly." + ) + return path - """ - loader = loadopt.set_relationship_strategy(attr, {"lazy": "immediate"}) - return loader + def _prepare_for_compile_state( + self, + parent_loader, + compile_state, + mapper_entities, + reconciled_lead_entity, + raiseerr, + ): + # _TokenStrategyLoad + current_path = compile_state.current_path + is_refresh = compile_state.compile_options._for_refresh_state -@immediateload._add_unbound_fn -def immediateload(*keys): - return _UnboundLoad._from_keys(_UnboundLoad.immediateload, keys, False, {}) + assert self.path.is_token + if is_refresh and not self.propagate_to_loaders: + return [] -@loader_option() -def noload(loadopt, attr): - """Indicate that the given relationship attribute should remain unloaded. + # omit setting attributes for a "defaultload" type of option + if not self.strategy and not self.local_opts: + return [] - The relationship attribute will return ``None`` when accessed without - producing any loading effect. + effective_path = self.path + if reconciled_lead_entity: + effective_path = PathRegistry.coerce( + (reconciled_lead_entity,) + effective_path.path[1:] + ) - This function is part of the :class:`_orm.Load` interface and supports - both method-chained and standalone operation. + if current_path: + effective_path = self._adjust_effective_path_for_current_path( + effective_path, current_path + ) + if effective_path is None: + return [] + + # for a wildcard token, expand out the path we set + # to encompass everything from the query entity on + # forward. not clear if this is necessary when current_path + # is set. + + return [ + ("loader", _path.natural_path) + for _path in cast( + TokenRegistry, effective_path + ).generate_for_superclasses() + ] - :func:`_orm.noload` applies to :func:`_orm.relationship` attributes; for - column-based attributes, see :func:`_orm.defer`. - .. note:: Setting this loading strategy as the default strategy - for a relationship using the :paramref:`.orm.relationship.lazy` - parameter may cause issues with flushes, such if a delete operation - needs to load related objects and instead ``None`` was returned. +class _ClassStrategyLoad(_LoadElement): + """Loader strategies that deals with a class as a target, not + an attribute path - .. seealso:: + e.g.:: - :ref:`loading_toplevel` + q = s.query(Person).options( + selectin_polymorphic(Person, [Engineer, Manager]) + ) """ - return loadopt.set_relationship_strategy(attr, {"lazy": "noload"}) + inherit_cache = True + is_class_strategy = True + is_token_strategy = False + def _init_path(self, path, attr, wildcard_key, attr_group, raiseerr): + return path -@noload._add_unbound_fn -def noload(*keys): - return _UnboundLoad._from_keys(_UnboundLoad.noload, keys, False, {}) - + def _prepare_for_compile_state( + self, + parent_loader, + compile_state, + mapper_entities, + reconciled_lead_entity, + raiseerr, + ): + # _ClassStrategyLoad -@loader_option() -def raiseload(loadopt, attr, sql_only=False): - """Indicate that the given attribute should raise an error if accessed. + current_path = compile_state.current_path + is_refresh = compile_state.compile_options._for_refresh_state - A relationship attribute configured with :func:`_orm.raiseload` will - raise an :exc:`~sqlalchemy.exc.InvalidRequestError` upon access. The - typical way this is useful is when an application is attempting to ensure - that all relationship attributes that are accessed in a particular context - would have been already loaded via eager loading. Instead of having - to read through SQL logs to ensure lazy loads aren't occurring, this - strategy will cause them to raise immediately. + if is_refresh and not self.propagate_to_loaders: + return [] - :func:`_orm.raiseload` applies to :func:`_orm.relationship` - attributes only. - In order to apply raise-on-SQL behavior to a column-based attribute, - use the :paramref:`.orm.defer.raiseload` parameter on the :func:`.defer` - loader option. + # omit setting attributes for a "defaultload" type of option + if not self.strategy and not self.local_opts: + return [] - :param sql_only: if True, raise only if the lazy load would emit SQL, but - not if it is only checking the identity map, or determining that the - related value should just be None due to missing keys. When False, the - strategy will raise for all varieties of relationship loading. + effective_path = self.path - This function is part of the :class:`_orm.Load` interface and supports - both method-chained and standalone operation. + if current_path: + effective_path = self._adjust_effective_path_for_current_path( + effective_path, current_path + ) + if effective_path is None: + return [] + return [("loader", cast(PathRegistry, effective_path).natural_path)] - .. versionadded:: 1.1 - .. seealso:: +def _generate_from_keys(meth, keys, chained, kw): - :ref:`loading_toplevel` + lead_element = None - :ref:`prevent_lazy_with_raiseload` + for is_default, _keys in (True, keys[0:-1]), (False, keys[-1:]): + for attr in _keys: + if isinstance(attr, str): + if attr.startswith("." + _WILDCARD_TOKEN): + util.warn_deprecated( + "The undocumented `.{WILDCARD}` format is " + "deprecated " + "and will be removed in a future version as " + "it is " + "believed to be unused. " + "If you have been using this functionality, " + "please " + "comment on Issue #4390 on the SQLAlchemy project " + "tracker.", + version="1.4", + ) + attr = attr[1:] - :ref:`deferred_raiseload` + if attr == _WILDCARD_TOKEN: + if is_default: + raise sa_exc.ArgumentError( + "Wildcard token cannot be followed by " + "another entity", + ) - """ + if lead_element is None: + lead_element = _WildcardLoad() - return loadopt.set_relationship_strategy( - attr, {"lazy": "raise_on_sql" if sql_only else "raise"} - ) + lead_element = meth(lead_element, _DEFAULT_TOKEN, **kw) + else: + raise sa_exc.ArgumentError( + "Strings are not accepted for attribute names in " + "loader options; please use class-bound " + "attributes directly.", + ) + else: + if lead_element is None: + _, lead_entity, _ = _parse_attr_argument(attr) + lead_element = Load(lead_entity) + + if is_default: + if not chained: + lead_element = lead_element.defaultload(attr) + else: + lead_element = meth( + lead_element, attr, _is_chain=True, **kw + ) + else: + lead_element = meth(lead_element, attr, **kw) -@raiseload._add_unbound_fn -def raiseload(*keys, **kw): - return _UnboundLoad._from_keys(_UnboundLoad.raiseload, keys, False, kw) + return lead_element -@loader_option() -def defaultload(loadopt, attr): - """Indicate an attribute should load using its default loader style. +def _parse_attr_argument(attr): + """parse an attribute or wildcard argument to produce an + :class:`._AbstractLoad` instance. - This method is used to link to other loader options further into - a chain of attributes without altering the loader style of the links - along the chain. For example, to set joined eager loading for an - element of an element:: + This is used by the standalone loader strategy functions like + ``joinedload()``, ``defer()``, etc. to produce :class:`_orm.Load` or + :class:`._WildcardLoad` objects. - session.query(MyClass).options( - defaultload(MyClass.someattribute). - joinedload(MyOtherClass.someotherattribute) + """ + try: + insp = inspect(attr) + except sa_exc.NoInspectionAvailable as err: + util.raise_( + sa_exc.ArgumentError( + "expected ORM mapped attribute for loader strategy argument" + ), + from_=err, ) - :func:`.defaultload` is also useful for setting column-level options - on a related class, namely that of :func:`.defer` and :func:`.undefer`:: - - session.query(MyClass).options( - defaultload(MyClass.someattribute). - defer("some_column"). - undefer("some_other_column") + if insp.is_property: + lead_entity = insp.parent + prop = insp + elif insp.is_attribute: + lead_entity = insp.parent + prop = insp.prop + else: + raise sa_exc.ArgumentError( + "expected ORM mapped attribute for loader strategy argument" ) - .. seealso:: - - :meth:`_orm.Load.options` - allows for complex hierarchical - loader option structures with less verbosity than with individual - :func:`.defaultload` directives. + return insp, lead_entity, prop - :ref:`relationship_loader_options` - :ref:`deferred_loading_w_multiple` +def loader_unbound_fn(fn): + """decorator that applies docstrings between standalone loader functions + and the loader methods on :class:`._AbstractLoad`. """ - return loadopt.set_relationship_strategy(attr, None) + bound_fn = getattr(_AbstractLoad, fn.__name__) + fn_doc = bound_fn.__doc__ + bound_fn.__doc__ = f"""Produce a new :class:`_orm.Load` object with the +:func:`_orm.{fn.__name__}` option applied. +See :func:`_orm.{fn.__name__}` for usage examples. -@defaultload._add_unbound_fn -def defaultload(*keys): - return _UnboundLoad._from_keys(_UnboundLoad.defaultload, keys, False, {}) - +""" -@loader_option() -def defer(loadopt, key, raiseload=False): - r"""Indicate that the given column-oriented attribute should be deferred, - e.g. not loaded until accessed. + fn.__doc__ = fn_doc + return fn - This function is part of the :class:`_orm.Load` interface and supports - both method-chained and standalone operation. - e.g.:: +# standalone functions follow. docstrings are filled in +# by the ``@loader_unbound_fn`` decorator. - from sqlalchemy.orm import defer - session.query(MyClass).options( - defer("attribute_one"), - defer("attribute_two")) +@loader_unbound_fn +def contains_eager(*keys, **kw): + return _generate_from_keys(Load.contains_eager, keys, True, kw) - session.query(MyClass).options( - defer(MyClass.attribute_one), - defer(MyClass.attribute_two)) - To specify a deferred load of an attribute on a related class, - the path can be specified one token at a time, specifying the loading - style for each link along the chain. To leave the loading style - for a link unchanged, use :func:`_orm.defaultload`:: +@loader_unbound_fn +def load_only(*attrs): + # TODO: attrs against different classes. we likely have to + # add some extra state to Load of some kind + _, lead_element, _ = _parse_attr_argument(attrs[0]) + return Load(lead_element).load_only(*attrs) - session.query(MyClass).options(defaultload("someattr").defer("some_column")) - A :class:`_orm.Load` object that is present on a certain path can have - :meth:`_orm.Load.defer` called multiple times, - each will operate on the same - parent entity:: +@loader_unbound_fn +def joinedload(*keys, **kw): + return _generate_from_keys(Load.joinedload, keys, False, kw) - session.query(MyClass).options( - defaultload("someattr"). - defer("some_column"). - defer("some_other_column"). - defer("another_column") - ) +@loader_unbound_fn +def subqueryload(*keys): + return _generate_from_keys(Load.subqueryload, keys, False, {}) - :param key: Attribute to be deferred. - :param raiseload: raise :class:`.InvalidRequestError` if the column - value is to be loaded from emitting SQL. Used to prevent unwanted - SQL from being emitted. +@loader_unbound_fn +def selectinload(*keys): + return _generate_from_keys(Load.selectinload, keys, False, {}) - .. versionadded:: 1.4 - .. seealso:: +@loader_unbound_fn +def lazyload(*keys): + return _generate_from_keys(Load.lazyload, keys, False, {}) - :ref:`deferred_raiseload` - :param \*addl_attrs: This option supports the old 0.8 style - of specifying a path as a series of attributes, which is now superseded - by the method-chained style. +@loader_unbound_fn +def immediateload(*keys): + return _generate_from_keys(Load.immediateload, keys, False, {}) - .. deprecated:: 0.9 The \*addl_attrs on :func:`_orm.defer` is - deprecated and will be removed in a future release. Please - use method chaining in conjunction with defaultload() to - indicate a path. +@loader_unbound_fn +def noload(*keys): + return _generate_from_keys(Load.noload, keys, False, {}) - .. seealso:: - :ref:`deferred` +@loader_unbound_fn +def raiseload(*keys, **kw): + return _generate_from_keys(Load.raiseload, keys, False, kw) - :func:`_orm.undefer` - """ - strategy = {"deferred": True, "instrument": True} - if raiseload: - strategy["raiseload"] = True - return loadopt.set_column_strategy((key,), strategy) +@loader_unbound_fn +def defaultload(*keys): + return _generate_from_keys(Load.defaultload, keys, False, {}) -@defer._add_unbound_fn +@loader_unbound_fn def defer(key, *addl_attrs, **kw): if addl_attrs: util.warn_deprecated( @@ -1770,61 +2161,10 @@ def defer(key, *addl_attrs, **kw): "indicate a path.", version="1.3", ) - return _UnboundLoad._from_keys( - _UnboundLoad.defer, (key,) + addl_attrs, False, kw - ) - - -@loader_option() -def undefer(loadopt, key): - r"""Indicate that the given column-oriented attribute should be undeferred, - e.g. specified within the SELECT statement of the entity as a whole. - - The column being undeferred is typically set up on the mapping as a - :func:`.deferred` attribute. - - This function is part of the :class:`_orm.Load` interface and supports - both method-chained and standalone operation. - - Examples:: - - # undefer two columns - session.query(MyClass).options(undefer("col1"), undefer("col2")) + return _generate_from_keys(Load.defer, (key,) + addl_attrs, False, kw) - # undefer all columns specific to a single class using Load + * - session.query(MyClass, MyOtherClass).options( - Load(MyClass).undefer("*")) - # undefer a column on a related object - session.query(MyClass).options( - defaultload(MyClass.items).undefer('text')) - - :param key: Attribute to be undeferred. - - :param \*addl_attrs: This option supports the old 0.8 style - of specifying a path as a series of attributes, which is now superseded - by the method-chained style. - - .. deprecated:: 0.9 The \*addl_attrs on :func:`_orm.undefer` is - deprecated and will be removed in a future release. Please - use method chaining in conjunction with defaultload() to - indicate a path. - - .. seealso:: - - :ref:`deferred` - - :func:`_orm.defer` - - :func:`_orm.undefer_group` - - """ - return loadopt.set_column_strategy( - (key,), {"deferred": False, "instrument": True} - ) - - -@undefer._add_unbound_fn +@loader_unbound_fn def undefer(key, *addl_attrs): if addl_attrs: util.warn_deprecated( @@ -1833,132 +2173,23 @@ def undefer(key, *addl_attrs): "indicate a path.", version="1.3", ) - return _UnboundLoad._from_keys( - _UnboundLoad.undefer, (key,) + addl_attrs, False, {} - ) - - -@loader_option() -def undefer_group(loadopt, name): - """Indicate that columns within the given deferred group name should be - undeferred. + return _generate_from_keys(Load.undefer, (key,) + addl_attrs, False, {}) - The columns being undeferred are set up on the mapping as - :func:`.deferred` attributes and include a "group" name. - E.g:: - - session.query(MyClass).options(undefer_group("large_attrs")) - - To undefer a group of attributes on a related entity, the path can be - spelled out using relationship loader options, such as - :func:`_orm.defaultload`:: - - session.query(MyClass).options( - defaultload("someattr").undefer_group("large_attrs")) - - .. versionchanged:: 0.9.0 :func:`_orm.undefer_group` is now specific to a - particular entity load path. - - .. seealso:: - - :ref:`deferred` - - :func:`_orm.defer` - - :func:`_orm.undefer` - - """ - return loadopt.set_column_strategy( - "*", None, {"undefer_group_%s" % name: True}, opts_only=True - ) - - -@undefer_group._add_unbound_fn +@loader_unbound_fn def undefer_group(name): - return _UnboundLoad().undefer_group(name) - + element = _WildcardLoad() + return element.undefer_group(name) -@loader_option() -def with_expression(loadopt, key, expression): - r"""Apply an ad-hoc SQL expression to a "deferred expression" attribute. - This option is used in conjunction with the :func:`_orm.query_expression` - mapper-level construct that indicates an attribute which should be the - target of an ad-hoc SQL expression. - - E.g.:: - - - sess.query(SomeClass).options( - with_expression(SomeClass.x_y_expr, SomeClass.x + SomeClass.y) - ) - - .. versionadded:: 1.2 - - :param key: Attribute to be undeferred. - - :param expr: SQL expression to be applied to the attribute. - - .. note:: the target attribute is populated only if the target object - is **not currently loaded** in the current :class:`_orm.Session` - unless the :meth:`_query.Query.populate_existing` method is used. - Please refer to :ref:`mapper_querytime_expression` for complete - usage details. - - .. seealso:: - - :ref:`mapper_querytime_expression` - - """ - - expression = coercions.expect( - roles.LabeledColumnExprRole, _orm_full_deannotate(expression) - ) - - return loadopt.set_column_strategy( - (key,), {"query_expression": True}, opts={"expression": expression} - ) - - -@with_expression._add_unbound_fn +@loader_unbound_fn def with_expression(key, expression): - return _UnboundLoad._from_keys( - _UnboundLoad.with_expression, (key,), False, {"expression": expression} - ) - - -@loader_option() -def selectin_polymorphic(loadopt, classes): - """Indicate an eager load should take place for all attributes - specific to a subclass. - - This uses an additional SELECT with IN against all matched primary - key values, and is the per-query analogue to the ``"selectin"`` - setting on the :paramref:`.mapper.polymorphic_load` parameter. - - .. versionadded:: 1.2 - - .. seealso:: - - :ref:`polymorphic_selectin` - - """ - loadopt.set_class_strategy( - {"selectinload_polymorphic": True}, - opts={ - "entities": tuple( - sorted((inspect(cls) for cls in classes), key=id) - ) - }, + return _generate_from_keys( + Load.with_expression, (key,), False, {"expression": expression} ) - return loadopt -@selectin_polymorphic._add_unbound_fn +@loader_unbound_fn def selectin_polymorphic(base_cls, classes): - ul = _UnboundLoad() - ul.is_class_strategy = True - ul.path = (inspect(base_cls),) - ul.selectin_polymorphic(classes) - return ul + ul = Load(base_cls) + return ul.selectin_polymorphic(classes) diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py index fef65f73c..58ad3ab5f 100644 --- a/lib/sqlalchemy/orm/util.py +++ b/lib/sqlalchemy/orm/util.py @@ -385,6 +385,9 @@ class ORMAdapter(sql_util.ColumnAdapter): """ + is_aliased_class = False + aliased_insp = None + def __init__( self, entity, @@ -397,11 +400,9 @@ class ORMAdapter(sql_util.ColumnAdapter): self.mapper = info.mapper selectable = info.selectable - is_aliased_class = info.is_aliased_class - if is_aliased_class: - self.aliased_class = entity - else: - self.aliased_class = None + if info.is_aliased_class: + self.is_aliased_class = True + self.aliased_insp = info sql_util.ColumnAdapter.__init__( self, @@ -517,12 +518,12 @@ class AliasedClass: represents_outer_join, ) - self.__name__ = "AliasedClass_%s" % mapper.class_.__name__ + self.__name__ = f"aliased({mapper.class_.__name__})" @classmethod def _reconstitute_from_aliased_insp(cls, aliased_insp): obj = cls.__new__(cls) - obj.__name__ = "AliasedClass_%s" % aliased_insp.mapper.class_.__name__ + obj.__name__ = f"aliased({aliased_insp.mapper.class_.__name__})" obj._aliased_insp = aliased_insp if aliased_insp._is_with_polymorphic: @@ -754,7 +755,7 @@ class AliasedInsp( return self.mapper.class_ @property - def _path_registry(self): + def _path_registry(self) -> PathRegistry: if self._use_mapper_path: return self.mapper._path_registry else: @@ -788,6 +789,37 @@ class AliasedInsp( state["represents_outer_join"], ) + def _merge_with(self, other): + # assert self._is_with_polymorphic + # assert other._is_with_polymorphic + + primary_mapper = other.mapper + + assert self.mapper is primary_mapper + + our_classes = util.to_set( + mp.class_ for mp in self.with_polymorphic_mappers + ) + new_classes = set([mp.class_ for mp in other.with_polymorphic_mappers]) + if our_classes == new_classes: + return other + else: + classes = our_classes.union(new_classes) + + mappers, selectable = primary_mapper._with_polymorphic_args( + classes, None, innerjoin=not other.represents_outer_join + ) + selectable = selectable._anonymous_fromclause(flat=True) + + return AliasedClass( + primary_mapper, + selectable, + with_polymorphic_mappers=mappers, + with_polymorphic_discriminator=other.polymorphic_on, + use_mapper_path=other._use_mapper_path, + represents_outer_join=other.represents_outer_join, + ) + def _adapt_element(self, elem, key=None): d = { "parententity": self, @@ -1101,6 +1133,7 @@ class LoaderCriteriaOption(CriteriaOption): .. versionadded:: 1.4.0b2 """ + entity = inspection.inspect(entity_or_base, False) if entity is None: self.root_entity = entity_or_base @@ -1326,7 +1359,6 @@ def with_polymorphic( aliased=False, innerjoin=False, _use_mapper_path=False, - _existing_alias=None, ): """Produce an :class:`.AliasedClass` construct which specifies columns for descendant mappers of the given base. @@ -1397,16 +1429,6 @@ def with_polymorphic( "simultaneously to with_polymorphic()" ) - if _existing_alias: - assert _existing_alias.mapper is primary_mapper - classes = util.to_set(classes) - new_classes = set( - [mp.class_ for mp in _existing_alias.with_polymorphic_mappers] - ) - if classes == new_classes: - return _existing_alias - else: - classes = classes.union(new_classes) mappers, selectable = primary_mapper._with_polymorphic_args( classes, selectable, innerjoin=innerjoin ) @@ -1969,10 +1991,10 @@ def _entity_corresponds_to_use_path_impl(given, entity): return ( entity.is_aliased_class and not entity._use_mapper_path - and (given is entity or given in entity._with_polymorphic_entities) + and (given is entity or entity in given._with_polymorphic_entities) ) elif not entity.is_aliased_class: - return given.common_parent(entity.mapper) + return given.isa(entity.mapper) else: return ( entity._use_mapper_path diff --git a/lib/sqlalchemy/sql/base.py b/lib/sqlalchemy/sql/base.py index 4165751ca..65afc57dd 100644 --- a/lib/sqlalchemy/sql/base.py +++ b/lib/sqlalchemy/sql/base.py @@ -109,7 +109,9 @@ def _generative(fn): self = self._generate() x = fn(self, *args, **kw) - assert x is None, "generative methods must have no return value" + assert ( + x is None or x is self + ), "generative methods must return None or self" return self decorated = _generative(fn) @@ -835,7 +837,7 @@ class Executable(roles.StatementRole, Generative): For background on specific kinds of options for specific kinds of statements, refer to the documentation for those option objects. - .. versionchanged:: 1.4 - added :meth:`.Generative.options` to + .. versionchanged:: 1.4 - added :meth:`.Executable.options` to Core statement objects towards the goal of allowing unified Core / ORM querying capabilities. diff --git a/lib/sqlalchemy/testing/plugin/pytestplugin.py b/lib/sqlalchemy/testing/plugin/pytestplugin.py index 7caa50438..02cb0ac32 100644 --- a/lib/sqlalchemy/testing/plugin/pytestplugin.py +++ b/lib/sqlalchemy/testing/plugin/pytestplugin.py @@ -557,7 +557,7 @@ def _pytest_fn_decorator(target): metadata.update(format_argspec_plus(spec, grouped=False)) code = ( """\ -def %(name)s(%(args)s): +def %(name)s%(grouped_args)s: return %(__target_fn)s(%(__orig_fn)s, %(apply_kw)s) """ % metadata diff --git a/lib/sqlalchemy/util/compat.py b/lib/sqlalchemy/util/compat.py index dfa5fa825..37f157698 100644 --- a/lib/sqlalchemy/util/compat.py +++ b/lib/sqlalchemy/util/compat.py @@ -165,7 +165,7 @@ def _formatannotation(annotation, base_module=None): return repr(annotation).replace("typing.", "") if isinstance(annotation, type): if annotation.__module__ in ("builtins", base_module): - return annotation.__qualname__ + return repr(annotation.__qualname__) return annotation.__module__ + "." + annotation.__qualname__ return repr(annotation) diff --git a/lib/sqlalchemy/util/langhelpers.py b/lib/sqlalchemy/util/langhelpers.py index b759490c5..780af2bfe 100644 --- a/lib/sqlalchemy/util/langhelpers.py +++ b/lib/sqlalchemy/util/langhelpers.py @@ -20,12 +20,22 @@ import re import sys import textwrap import types +from typing import Any +from typing import Callable +from typing import Generic +from typing import Optional +from typing import overload +from typing import TypeVar +from typing import Union import warnings from . import _collections from . import compat from .. import exc +_T = TypeVar("_T") +_MP = TypeVar("_MP", bound="memoized_property[Any]") + def md5_hex(x): x = x.encode("utf-8") @@ -179,7 +189,7 @@ def decorator(target): metadata["name"] = fn.__name__ code = ( """\ -def %(name)s(%(args)s): +def %(name)s%(grouped_args)s: return %(target)s(%(fn)s, %(apply_kw)s) """ % metadata @@ -255,7 +265,7 @@ def public_factory(target, location, class_location=None): metadata["name"] = location_name code = ( """\ -def %(name)s(%(args)s): +def %(name)s%(grouped_args)s: return cls(%(apply_kw)s) """ % metadata @@ -501,7 +511,7 @@ def format_argspec_plus(fn, grouped=True): Example:: >>> format_argspec_plus(lambda self, a, b, c=3, **d: 123) - {'args': '(self, a, b, c=3, **d)', + {'grouped_args': '(self, a, b, c=3, **d)', 'self_arg': 'self', 'apply_kw': '(self, a, b, c=c, **d)', 'apply_pos': '(self, a, b, c, **d)'} @@ -567,7 +577,7 @@ def format_argspec_plus(fn, grouped=True): if grouped: return dict( - args=args, + grouped_args=args, self_arg=self_arg, apply_pos=apply_pos, apply_kw=apply_kw, @@ -576,7 +586,7 @@ def format_argspec_plus(fn, grouped=True): ) else: return dict( - args=args[1:-1], + grouped_args=args, self_arg=self_arg, apply_pos=apply_pos[1:-1], apply_kw=apply_kw[1:-1], @@ -596,21 +606,19 @@ def format_argspec_init(method, grouped=True): """ if method is object.__init__: + grouped_args = "(self)" args = "(self)" if grouped else "self" proxied = "()" if grouped else "" else: try: return format_argspec_plus(method, grouped=grouped) except TypeError: - args = ( - "(self, *args, **kwargs)" - if grouped - else "self, *args, **kwargs" - ) + grouped_args = "(self, *args, **kwargs)" + args = grouped_args if grouped else "self, *args, **kwargs" proxied = "(*args, **kwargs)" if grouped else "*args, **kwargs" return dict( self_arg="self", - args=args, + grouped_args=grouped_args, apply_pos=args, apply_kw=args, apply_pos_proxied=proxied, @@ -645,20 +653,20 @@ def create_proxy_methods( "name": fn.__name__, "apply_pos_proxied": caller_argspec["apply_pos_proxied"], "apply_kw_proxied": caller_argspec["apply_kw_proxied"], - "args": caller_argspec["args"], + "grouped_args": caller_argspec["grouped_args"], "self_arg": caller_argspec["self_arg"], } if clslevel: code = ( - "def %(name)s(%(args)s):\n" + "def %(name)s%(grouped_args)s:\n" " return target_cls.%(name)s(%(apply_kw_proxied)s)" % metadata ) env["target_cls"] = target_cls else: code = ( - "def %(name)s(%(args)s):\n" + "def %(name)s%(grouped_args)s:\n" " return %(self_arg)s._proxied.%(name)s(%(apply_kw_proxied)s)" # noqa E501 % metadata ) @@ -1072,15 +1080,27 @@ def as_interface(obj, cls=None, methods=None, required=None): ) -class memoized_property: +class memoized_property(Generic[_T]): """A read-only @property that is only evaluated once.""" - def __init__(self, fget, doc=None): + fget: Callable[..., _T] + __doc__: Optional[str] + __name__: str + + def __init__(self, fget: Callable[..., _T], doc: Optional[str] = None): self.fget = fget self.__doc__ = doc or fget.__doc__ self.__name__ = fget.__name__ - def __get__(self, obj, cls): + @overload + def __get__(self: _MP, obj: None, cls: Any) -> _MP: + ... + + @overload + def __get__(self, obj: Any, cls: Any) -> _T: + ... + + def __get__(self: _MP, obj: Any, cls: Any) -> Union[_MP, _T]: if obj is None: return self obj.__dict__[self.__name__] = result = self.fget(obj) @@ -1090,7 +1110,7 @@ class memoized_property: memoized_property.reset(obj, self.__name__) @classmethod - def reset(cls, obj, name): + def reset(cls, obj: Any, name: str) -> None: obj.__dict__.pop(name, None) |
