From: Mike Bayer Date: Thu, 9 Dec 2021 01:27:16 +0000 (-0500) Subject: factor out UnboundLoad and rearchitect strategy_options.py X-Git-Tag: rel_2_0_0b1~577^2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=91501e06a17d873902114275d7149ba24973db6a;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git 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 --- diff --git a/doc/build/changelog/unreleased_20/6986.rst b/doc/build/changelog/unreleased_20/6986.rst new file mode 100644 index 0000000000..217a05c493 --- /dev/null +++ b/doc/build/changelog/unreleased_20/6986.rst @@ -0,0 +1,9 @@ +.. change:: + :tags: bug, orm + :tickets: 6986 + + The internals for the :class:`_orm.Load` object and related loader strategy + patterns have been mostly rewritten, to take advantage of the fact that + only attribute-bound paths, not strings, are now supported. The rewrite + hopes to make it more straightforward to address new use cases and subtle + issues within the loader strategy system going forward. diff --git a/doc/build/orm/loading_relationships.rst b/doc/build/orm/loading_relationships.rst index 5a1d5151d4..2b93bc84af 100644 --- a/doc/build/orm/loading_relationships.rst +++ b/doc/build/orm/loading_relationships.rst @@ -1269,8 +1269,9 @@ Relationship Loader API .. autofunction:: lazyload -.. autoclass:: Load +.. autoclass:: sqlalchemy.orm.Load :members: + :inherited-members: Generative .. autofunction:: noload diff --git a/lib/sqlalchemy/ext/baked.py b/lib/sqlalchemy/ext/baked.py index e91277311a..a2db9dbec5 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 c49ff4ec64..69a3e64da6 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 e834b22e85..f438b0b3ad 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 2c6818a939..92ecbdd2df 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 40a76181c0..a94c6bfa72 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 "" % (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 0aa9de817b..f2768a6b66 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 de46f84a75..04b7f89c2b 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 b4aef874f1..4293cf656c 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 d5f5f8527d..844b0f007a 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 a5c5397739..02068adce8 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` - 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 + :ref:`deferred_loading_w_multiple` - if getattr(attr, "_of_type", None): - ac = attr._of_type - ext_info = of_type_info = inspect(ac) + """ + return self._set_relationship_strategy(attr, None) - if polymorphic_entity_context is None: - polymorphic_entity_context = self.context + def defer(self, key, raiseload=False): + r"""Indicate that the given column-oriented attribute should be + deferred, e.g. not loaded until accessed. - existing = path.entity_path[prop].get( - polymorphic_entity_context, "path_with_polymorphic" - ) + This function is part of the :class:`_orm.Load` interface and supports + both method-chained and standalone operation. - 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, - ) + e.g.:: + + from sqlalchemy.orm import defer + + session.query(MyClass).options( + defer(MyClass.attribute_one), + defer(MyClass.attribute_two) + ) - ext_info = inspect(ac) + 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`:: - path.entity_path[prop].set( - polymorphic_entity_context, "path_with_polymorphic", ac + session.query(MyClass).options( + defaultload(MyClass.someattr).defer(RelatedClass.some_column) + ) + + Multiple deferral options related to a relationship can be bundled + at once using :meth:`_orm.Load.options`:: + + + 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. + + 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:: - def _apply_to_parent(self, parent, applied, bound): - raise NotImplementedError( - "Only 'unbound' loader options may be used with the " - "Load.options() method" + :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. + + 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:: + + stmt = select(MyClass, MyOtherClass).options(myopt) + + One case where :class:`_orm.Load` + is useful as public API is when specifying + "wildcard" options that only take effect for a certain class:: + + 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. - This provides compatibility with the traditional system - of freestanding options, e.g. ``joinedload('x.y.z')``. + .. seealso:: + + :ref:`deferred_options` + + :ref:`deferred_loading_w_multiple` + + :ref:`relationship_loader_options` """ - def __init__(self): - self.path = () - self._to_bind = [] - self.local_opts = {} - self._extra_criteria = () + _cache_key_traversal = [ + ("path", visitors.ExtendedInternalTraversal.dp_has_cache_key), + ( + "context", + visitors.InternalTraversal.dp_has_cache_key_list, + ), + ] + + path: PathRegistry + context: Tuple["_LoadElement", ...] - def _gen_cache_key(self, anon_map, bindparams, _unbound_option_seen=None): - """Inlined gen_cache_key + def __init__(self, entity): + insp = cast(Union["Mapper", AliasedInsp], inspect(entity)) + insp._post_inspect + + self.path = insp._path_registry + self.context = () + + def __str__(self): + return f"Load({self.path[0]})" + + @classmethod + def _construct_for_existing_path(cls, path): + load = cls.__new__(cls) + load.path = path + load.context = () + return load - Original traversal is:: + 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. + + """ + orig_query = context.compile_state.select_statement + + orig_cache_key = None + replacement_cache_key = None + + def process(opt): + if not opt._extra_criteria: + return opt + + nonlocal orig_cache_key, replacement_cache_key + + # 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() + + opt._extra_criteria = tuple( + replacement_cache_key._apply_params_to_element( + orig_cache_key, crit + ) + for crit in opt._extra_criteria + ) + return opt + cloned = self._generate() - _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, - ), - ] + if self.context: + cloned.context = tuple( + process(value._clone()) for value in self.context + ) - The inlining is so that the "_to_bind" list can be flattened to not - repeat the same UnboundLoad options over and over again. + return cloned - See #6869 + 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. """ + path = self.path - idself = id(self) - cls = self.__class__ - - if idself in anon_map: - return (anon_map[idself], cls) - else: - id_ = anon_map[idself] + ezero = None + for ent in mapper_entities: + ezero = ent.entity_zero + if ezero and orm_util._entity_corresponds_to(ezero, path[0]): + return ezero - vis = traversals._cache_key_traversal_visitor + return None - seen = _unbound_option_seen - if seen is None: - seen = set() + def _process(self, compile_state, mapper_entities, raiseerr): - 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 () - ) + reconciled_lead_entity = self._reconcile_query_entities_with_us( + mapper_entities, raiseerr ) - _is_chain_link = False - - def _set_path_strategy(self): - self._to_bind.append(self) + for loader in self.context: + loader.process_compile_state( + self, + compile_state, + mapper_entities, + reconciled_lead_entity, + raiseerr, + ) - # remove cycles; _set_path_strategy is always invoked on an - # anonymous clone of the Load / UnboundLoad object since #5056 - self._to_bind = None + def _apply_to_parent(self, parent): + """apply this :class:`_orm.Load` object as a sub-option of another + :class:`_orm.Load` object. - def _deep_clone(self, applied, process): - if self in applied: - return applied[self] + This method is used by the :meth:`_orm.Load.options` method. + """ cloned = self._generate() - applied[self] = cloned - - cloned.strategy = self.strategy - 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 - cloned._to_bind = [ - elem._deep_clone(applied, process) for elem in self._to_bind or () - ] + 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.local_opts.update(self.local_opts) + cloned.path = PathRegistry.coerce(parent.path[0:-1] + cloned.path[:]) - process(cloned) + if self.context: + cloned.context = tuple( + value._prepend_path_from(parent) for value in self.context + ) - return cloned + if cloned.context: + parent.context += cloned.context - def _apply_to_parent(self, parent, applied, bound, to_bind=None): - if self in applied: - return applied[self] + @_generative + def options(self, *opts) -> "_AbstractLoad": + r"""Apply a series of options as sub-options to this + :class:`_orm.Load` + object. - if to_bind is None: - to_bind = self._to_bind + E.g.:: - cloned = self._generate() + query = session.query(Author) + query = query.options( + joinedload(Author.book).options( + load_only(Book.summary, Book.excerpt), + joinedload(Book.citations).options( + joinedload(Citation.author) + ) + ) + ) - applied[self] = cloned + :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. - 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 - ) + .. versionadded:: 1.3.6 - # these assertions can go away once the "sub options" API is - # mature - 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 + .. seealso:: - uniq = set() + :func:`.defaultload` - cloned._to_bind = parent._to_bind + :ref:`relationship_loader_options` - 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) - ] + :ref:`deferred_loading_w_multiple` - cloned.local_opts.update(self.local_opts) + """ + for opt in opts: + opt._apply_to_parent(self) + return self - return cloned + 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: + # 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" + ) - 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", ()) + 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,) - return path + else: + 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: + load_element = _AttributeStrategyLoad.create( + self.path, + attr, + strategy, + wildcard_key, + opts, + propagate_to_loaders, + attr_group=attr_group, + reconcile_to_other=reconcile_to_other, + ) + + 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,) def __getstate__(self): d = self.__dict__.copy() - - # can't pickle this right now; warning is raised by strategies - d["_extra_criteria"] = () - - d["path"] = self._serialize_path(self.path, filter_aliased_class=True) + d["path"] = self.path.serialize() return d 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 + self.__dict__.update(state) + self.path = PathRegistry.deserialize(self.path) - 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, - ) - @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,) +class _WildcardLoad(_AbstractLoad): + """represent a standalone '*' load operation""" - all_tokens = [token for key in keys for token in _split_key(key)] + _cache_key_traversal = [ + ("strategy", visitors.ExtendedInternalTraversal.dp_plain_obj), + ( + "local_opts", + visitors.ExtendedInternalTraversal.dp_string_multi_dict, + ), + ] - 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) - else: - opt = opt.defaultload(token) + local_opts = util.EMPTY_DICT + path: Tuple[str, ...] = () - opt = meth(opt, all_tokens[-1], **kw) - opt._is_chain_link = False - return opt + 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) + ) - 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)) - else: - ret.append( - ( - token._parentmapper.class_, - token.key, - token._of_type.entity if token._of_type else None, - ) - ) - elif isinstance(token, PropComparator): - ret.append((token._parentmapper.class_, token.key, None)) - else: - ret.append(token) - return ret + 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 - def _bind_loader(self, entities, current_path, context, raiseerr): - """Convert from an _UnboundLoad() object into a Load() object. + attr = f"{wildcard_key}:{attr}" - 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. + self.strategy = strategy + self.path = (attr,) + if opts: + self.local_opts = util.immutabledict(opts) - Example:: + def options(self, *opts) -> "_AbstractLoad": + raise NotImplementedError("Star option does not support sub-options") + def _apply_to_parent(self, parent): + """apply this :class:`_orm._WildcardLoad` object as a sub-option of + a :class:`_orm.Load` object. - query = session.query(User).options( - joinedload("orders").joinedload("items")) + 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. - The above options will be an _UnboundLoad object along the lines - of (note this is not the exact API of _UnboundLoad):: + """ - _UnboundLoad( - _to_bind=[ - _UnboundLoad(["orders"], {"lazy": "joined"}), - _UnboundLoad(["orders", "items"], {"lazy": "joined"}), - ] - ) + attr = self.path[0] + if attr.endswith(_DEFAULT_TOKEN): + attr = f"{attr.split(':')[0]}:{_WILDCARD_TOKEN}" - After this method, we get something more like this (again this is - not exact API):: + effective_path = parent.path.token(attr) - Load( - User, - (User, User.orders.property)) - Load( - User, - (User, User.orders.property, Order, Order.items.property)) + assert effective_path.is_token - """ + loader = _TokenStrategyLoad.create( + effective_path, + None, + self.strategy, + None, + self.local_opts, + self.propagate_to_loaders, + ) - start_path = self.path + 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 - if self.is_class_strategy and current_path: - start_path += (entities[0],) + entities = [ent.entity_zero for ent in mapper_entities] + current_path = compile_state.current_path - # _current_path implies we're in a - # secondary load with an existing 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 - -@joinedload._add_unbound_fn -def joinedload(*keys, **kw): - return _UnboundLoad._from_keys(_UnboundLoad.joinedload, keys, False, kw) + _cache_key_traversal = _LoadElement._cache_key_traversal + [ + ("_of_type", visitors.ExtendedInternalTraversal.dp_multi), + ("_extra_criteria", visitors.InternalTraversal.dp_clauseelement_list), + ] + _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. -@defaultload._add_unbound_fn -def defaultload(*keys): - return _UnboundLoad._from_keys(_UnboundLoad.defaultload, keys, False, {}) +See :func:`_orm.{fn.__name__}` for usage examples. +""" -@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. + return _generate_from_keys(Load.defer, (key,) + addl_attrs, False, kw) - 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. - - :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. - - 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 - ) + return _generate_from_keys(Load.undefer, (key,) + addl_attrs, False, {}) -@undefer_group._add_unbound_fn +@loader_unbound_fn def undefer_group(name): - return _UnboundLoad().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} - ) + element = _WildcardLoad() + return element.undefer_group(name) -@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 fef65f73c2..58ad3ab5f9 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 4165751ca1..65afc57ddd 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 7caa50438c..02cb0ac328 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 dfa5fa825a..37f1576986 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 b759490c57..780af2bfe4 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) diff --git a/test/base/test_utils.py b/test/base/test_utils.py index 836778bc99..9278bd2688 100644 --- a/test/base/test_utils.py +++ b/test/base/test_utils.py @@ -2389,6 +2389,9 @@ class _Py3KFixtures: def _kw_opt_fixture(self, a, *, b, c="c"): pass + def _ret_annotation_fixture(self, a, b) -> int: + return 1 + py3k_fixtures = _Py3KFixtures() @@ -2398,7 +2401,7 @@ class TestFormatArgspec(_Py3KFixtures, fixtures.TestBase): ( lambda: None, { - "args": "()", + "grouped_args": "()", "self_arg": None, "apply_kw": "()", "apply_pos": "()", @@ -2410,7 +2413,7 @@ class TestFormatArgspec(_Py3KFixtures, fixtures.TestBase): ( lambda: None, { - "args": "", + "grouped_args": "()", "self_arg": None, "apply_kw": "", "apply_pos": "", @@ -2422,7 +2425,7 @@ class TestFormatArgspec(_Py3KFixtures, fixtures.TestBase): ( lambda self: None, { - "args": "(self)", + "grouped_args": "(self)", "self_arg": "self", "apply_kw": "(self)", "apply_pos": "(self)", @@ -2434,7 +2437,7 @@ class TestFormatArgspec(_Py3KFixtures, fixtures.TestBase): ( lambda self: None, { - "args": "self", + "grouped_args": "(self)", "self_arg": "self", "apply_kw": "self", "apply_pos": "self", @@ -2446,7 +2449,7 @@ class TestFormatArgspec(_Py3KFixtures, fixtures.TestBase): ( lambda *a: None, { - "args": "(*a)", + "grouped_args": "(*a)", "self_arg": "a[0]", "apply_kw": "(*a)", "apply_pos": "(*a)", @@ -2458,7 +2461,7 @@ class TestFormatArgspec(_Py3KFixtures, fixtures.TestBase): ( lambda **kw: None, { - "args": "(**kw)", + "grouped_args": "(**kw)", "self_arg": None, "apply_kw": "(**kw)", "apply_pos": "(**kw)", @@ -2470,7 +2473,7 @@ class TestFormatArgspec(_Py3KFixtures, fixtures.TestBase): ( lambda *a, **kw: None, { - "args": "(*a, **kw)", + "grouped_args": "(*a, **kw)", "self_arg": "a[0]", "apply_kw": "(*a, **kw)", "apply_pos": "(*a, **kw)", @@ -2482,7 +2485,7 @@ class TestFormatArgspec(_Py3KFixtures, fixtures.TestBase): ( lambda a, *b: None, { - "args": "(a, *b)", + "grouped_args": "(a, *b)", "self_arg": "a", "apply_kw": "(a, *b)", "apply_pos": "(a, *b)", @@ -2494,7 +2497,7 @@ class TestFormatArgspec(_Py3KFixtures, fixtures.TestBase): ( lambda a, **b: None, { - "args": "(a, **b)", + "grouped_args": "(a, **b)", "self_arg": "a", "apply_kw": "(a, **b)", "apply_pos": "(a, **b)", @@ -2506,7 +2509,7 @@ class TestFormatArgspec(_Py3KFixtures, fixtures.TestBase): ( lambda a, *b, **c: None, { - "args": "(a, *b, **c)", + "grouped_args": "(a, *b, **c)", "self_arg": "a", "apply_kw": "(a, *b, **c)", "apply_pos": "(a, *b, **c)", @@ -2518,7 +2521,7 @@ class TestFormatArgspec(_Py3KFixtures, fixtures.TestBase): ( lambda a, b=1, **c: None, { - "args": "(a, b=1, **c)", + "grouped_args": "(a, b=1, **c)", "self_arg": "a", "apply_kw": "(a, b=b, **c)", "apply_pos": "(a, b, **c)", @@ -2530,7 +2533,7 @@ class TestFormatArgspec(_Py3KFixtures, fixtures.TestBase): ( lambda a=1, b=2: None, { - "args": "(a=1, b=2)", + "grouped_args": "(a=1, b=2)", "self_arg": "a", "apply_kw": "(a=a, b=b)", "apply_pos": "(a, b)", @@ -2542,7 +2545,7 @@ class TestFormatArgspec(_Py3KFixtures, fixtures.TestBase): ( lambda a=1, b=2: None, { - "args": "a=1, b=2", + "grouped_args": "(a=1, b=2)", "self_arg": "a", "apply_kw": "a=a, b=b", "apply_pos": "a, b", @@ -2551,10 +2554,22 @@ class TestFormatArgspec(_Py3KFixtures, fixtures.TestBase): }, False, ), + ( + py3k_fixtures._ret_annotation_fixture, + { + "grouped_args": "(self, a, b) -> 'int'", + "self_arg": "self", + "apply_pos": "self, a, b", + "apply_kw": "self, a, b", + "apply_pos_proxied": "a, b", + "apply_kw_proxied": "a, b", + }, + False, + ), ( py3k_fixtures._kw_only_fixture, { - "args": "self, a, *, b, c", + "grouped_args": "(self, a, *, b, c)", "self_arg": "self", "apply_pos": "self, a, *, b, c", "apply_kw": "self, a, b=b, c=c", @@ -2566,7 +2581,7 @@ class TestFormatArgspec(_Py3KFixtures, fixtures.TestBase): ( py3k_fixtures._kw_plus_posn_fixture, { - "args": "self, a, *args, b, c", + "grouped_args": "(self, a, *args, b, c)", "self_arg": "self", "apply_pos": "self, a, *args, b, c", "apply_kw": "self, a, b=b, c=c, *args", @@ -2578,7 +2593,7 @@ class TestFormatArgspec(_Py3KFixtures, fixtures.TestBase): ( py3k_fixtures._kw_opt_fixture, { - "args": "self, a, *, b, c='c'", + "grouped_args": "(self, a, *, b, c='c')", "self_arg": "self", "apply_pos": "self, a, *, b, c", "apply_kw": "self, a, b=b, c=c", @@ -2609,7 +2624,7 @@ class TestFormatArgspec(_Py3KFixtures, fixtures.TestBase): @testing.requires.cpython def test_init_grouped(self): object_spec = { - "args": "(self)", + "grouped_args": "(self)", "self_arg": "self", "apply_pos": "(self)", "apply_kw": "(self)", @@ -2617,7 +2632,7 @@ class TestFormatArgspec(_Py3KFixtures, fixtures.TestBase): "apply_kw_proxied": "()", } wrapper_spec = { - "args": "(self, *args, **kwargs)", + "grouped_args": "(self, *args, **kwargs)", "self_arg": "self", "apply_pos": "(self, *args, **kwargs)", "apply_kw": "(self, *args, **kwargs)", @@ -2625,7 +2640,7 @@ class TestFormatArgspec(_Py3KFixtures, fixtures.TestBase): "apply_kw_proxied": "(*args, **kwargs)", } custom_spec = { - "args": "(slef, a=123)", + "grouped_args": "(slef, a=123)", "self_arg": "slef", # yes, slef "apply_pos": "(slef, a)", "apply_pos_proxied": "(a)", @@ -2639,7 +2654,7 @@ class TestFormatArgspec(_Py3KFixtures, fixtures.TestBase): @testing.requires.cpython def test_init_bare(self): object_spec = { - "args": "self", + "grouped_args": "(self)", "self_arg": "self", "apply_pos": "self", "apply_kw": "self", @@ -2647,7 +2662,7 @@ class TestFormatArgspec(_Py3KFixtures, fixtures.TestBase): "apply_kw_proxied": "", } wrapper_spec = { - "args": "self, *args, **kwargs", + "grouped_args": "(self, *args, **kwargs)", "self_arg": "self", "apply_pos": "self, *args, **kwargs", "apply_kw": "self, *args, **kwargs", @@ -2655,7 +2670,7 @@ class TestFormatArgspec(_Py3KFixtures, fixtures.TestBase): "apply_kw_proxied": "*args, **kwargs", } custom_spec = { - "args": "slef, a=123", + "grouped_args": "(slef, a=123)", "self_arg": "slef", # yes, slef "apply_pos": "slef, a", "apply_kw": "slef, a=a", diff --git a/test/orm/declarative/test_basic.py b/test/orm/declarative/test_basic.py index 902caca6fb..a5c1ba08ab 100644 --- a/test/orm/declarative/test_basic.py +++ b/test/orm/declarative/test_basic.py @@ -1190,7 +1190,7 @@ class DeclarativeTest(DeclarativeTestBase): sa.exc.InvalidRequestError, "^One or more mappers failed to initialize" " - can't proceed with initialization of other mappers. " - r"Triggering mapper: 'mapped class User->users'. " + r"Triggering mapper: 'Mapper\[User\(users\)\]'. " "Original exception was: When initializing.*", configure_mappers, ) diff --git a/test/orm/inheritance/test_assorted_poly.py b/test/orm/inheritance/test_assorted_poly.py index f51fb17a40..c11dd1bf71 100644 --- a/test/orm/inheritance/test_assorted_poly.py +++ b/test/orm/inheritance/test_assorted_poly.py @@ -2266,7 +2266,7 @@ class ColSubclassTest( s = fixture_session() with testing.expect_warnings( "An alias is being generated automatically " - "against joined entity mapped class B->b due to overlapping" + r"against joined entity Mapper\[B\(b\)\] due to overlapping" ): self.assert_compile( s.query(A).join(B).filter(B.x == "test"), diff --git a/test/orm/inheritance/test_basic.py b/test/orm/inheritance/test_basic.py index 869cdc8899..5fc69bf9ee 100644 --- a/test/orm/inheritance/test_basic.py +++ b/test/orm/inheritance/test_basic.py @@ -2233,7 +2233,7 @@ class DistinctPKTest(fixtures.MappedTest): ) assert_raises_message( sa_exc.SAWarning, - r"On mapper mapped class Employee->employees, " + r"On mapper Mapper\[Employee\(employees\)\], " "primary key column 'persons.id' is being " "combined with distinct primary key column 'employees.eid' " "in attribute 'id'. Use explicit properties to give " @@ -3842,8 +3842,9 @@ class UnexpectedPolymorphicIdentityTest(fixtures.DeclarativeMappedTest): sa_exc.InvalidRequestError, r"Row with identity key \(.*ASingle.*\) can't be loaded into an " r"object; the polymorphic discriminator column '.*.type' refers " - r"to mapped class ASingleSubB->asingle, which is not a " - r"sub-mapper of the requested mapped class ASingleSubA->asingle", + r"to Mapper\[ASingleSubB\(asingle\)\], which is not a " + r"sub-mapper of the requested " + r"Mapper\[ASingleSubA\(asingle\)\]", q.all, ) @@ -3858,9 +3859,10 @@ class UnexpectedPolymorphicIdentityTest(fixtures.DeclarativeMappedTest): sa_exc.InvalidRequestError, r"Row with identity key \(.*AJoined.*\) can't be loaded into an " r"object; the polymorphic discriminator column '.*.type' refers " - r"to mapped class AJoinedSubB->ajoinedsubb, which is not a " - r"sub-mapper of the requested mapped class " - r"AJoinedSubA->ajoinedsuba", + r"to Mapper\[AJoinedSubB\(ajoinedsubb\)\], which is " + "not a " + r"sub-mapper of the requested " + r"Mapper\[AJoinedSubA\(ajoinedsuba\)\]", q.all, ) diff --git a/test/orm/inheritance/test_poly_loading.py b/test/orm/inheritance/test_poly_loading.py index 0a03388314..4fe13887e1 100644 --- a/test/orm/inheritance/test_poly_loading.py +++ b/test/orm/inheritance/test_poly_loading.py @@ -191,6 +191,7 @@ class LoadBaseAndSubWEagerRelMapped( class FixtureLoadTest(_Polymorphic, testing.AssertsExecutionResults): def test_person_selectin_subclasses(self): s = fixture_session() + q = s.query(Person).options( selectin_polymorphic(Person, [Engineer, Manager]) ) @@ -786,12 +787,14 @@ class IgnoreOptionsOnSubclassAttrLoad(fixtures.DeclarativeMappedTest): will_lazyload = first_option in (defaultload, lazyload) - opt = first_option(Parent.entity) - if second_argument == "name": second_argument = SubEntity.name + opt = first_option(Parent.entity.of_type(SubEntity)) elif second_argument == "id": + opt = first_option(Parent.entity) second_argument = Entity.id + else: + opt = first_option(Parent.entity) if second_option is None: sub_opt = opt @@ -831,7 +834,10 @@ class IgnoreOptionsOnSubclassAttrLoad(fixtures.DeclarativeMappedTest): ) ) - if second_option in ("undefer", "load_only", None): + if second_option in ("load_only", None) or ( + second_option == "undefer" + and first_option in (defaultload, lazyload) + ): # load will be a mapper optimized load for the name alone expected.append( CompiledSQL( diff --git a/test/orm/inheritance/test_relationship.py b/test/orm/inheritance/test_relationship.py index 6d071a1e22..fa66f90b82 100644 --- a/test/orm/inheritance/test_relationship.py +++ b/test/orm/inheritance/test_relationship.py @@ -1,5 +1,6 @@ from contextlib import nullcontext +from sqlalchemy import exc from sqlalchemy import ForeignKey from sqlalchemy import func from sqlalchemy import Integer @@ -21,6 +22,7 @@ from sqlalchemy.testing import AssertsCompiledSQL from sqlalchemy.testing import eq_ from sqlalchemy.testing import fixtures from sqlalchemy.testing import is_ +from sqlalchemy.testing.assertions import expect_raises_message from sqlalchemy.testing.entities import ComparableEntity from sqlalchemy.testing.fixtures import fixture_session from sqlalchemy.testing.schema import Column @@ -57,8 +59,9 @@ class Paperwork(fixtures.ComparableEntity): def _aliased_join_warning(arg): return testing.expect_warnings( - "An alias is being generated automatically against joined entity " - "mapped class %s due to overlapping tables" % (arg,) + r"An alias is being generated automatically against joined entity " + r"Mapper\[%s\] due to overlapping tables" % (arg,), + raise_on_any_unexpected=True, ) @@ -297,7 +300,7 @@ class SelfReferentialJ2JTest(fixtures.MappedTest): if autoalias: # filter aliasing applied to Engineer doesn't whack Manager - with _aliased_join_warning("Engineer->engineers"): + with _aliased_join_warning(r"Engineer\(engineers\)"): eq_( sess.query(Manager) .join(Manager.engineers) @@ -306,7 +309,7 @@ class SelfReferentialJ2JTest(fixtures.MappedTest): [m1], ) - with _aliased_join_warning("Engineer->engineers"): + with _aliased_join_warning(r"Engineer\(engineers\)"): eq_( sess.query(Manager) .join(Manager.engineers) @@ -315,7 +318,7 @@ class SelfReferentialJ2JTest(fixtures.MappedTest): [m2], ) - with _aliased_join_warning("Engineer->engineers"): + with _aliased_join_warning(r"Engineer\(engineers\)"): eq_( sess.query(Manager, Engineer) .join(Manager.engineers) @@ -367,7 +370,7 @@ class SelfReferentialJ2JTest(fixtures.MappedTest): sess.expunge_all() if autoalias: - with _aliased_join_warning("Engineer->engineers"): + with _aliased_join_warning(r"Engineer\(engineers\)"): eq_( sess.query(Manager) .join(Manager.engineers) @@ -376,7 +379,7 @@ class SelfReferentialJ2JTest(fixtures.MappedTest): [], ) - with _aliased_join_warning("Engineer->engineers"): + with _aliased_join_warning(r"Engineer\(engineers\)"): eq_( sess.query(Manager) .join(Manager.engineers) @@ -795,13 +798,13 @@ class SelfReferentialM2MTest(fixtures.MappedTest, AssertsCompiledSQL): stmt = select(Child1).join(Child1.left_child2) - with _aliased_join_warning("Child2->child2"): + with _aliased_join_warning(r"Child2\(child2\)"): eq_( set(sess.execute(stmt).scalars().unique()), set([c11, c12, c13]), ) - with _aliased_join_warning("Child2->child2"): + with _aliased_join_warning(r"Child2\(child2\)"): eq_( set(sess.query(Child1, Child2).join(Child1.left_child2)), set([(c11, c22), (c12, c22), (c13, c23)]), @@ -829,7 +832,7 @@ class SelfReferentialM2MTest(fixtures.MappedTest, AssertsCompiledSQL): .join(Child2.right_children) .where(Child1.left_child2 == c22) ) - with _aliased_join_warning("Child1->child1"): + with _aliased_join_warning(r"Child1\(child1\)"): eq_( set(sess.execute(stmt).scalars().unique()), set([c22]), @@ -848,7 +851,7 @@ class SelfReferentialM2MTest(fixtures.MappedTest, AssertsCompiledSQL): ) # test the same again - with _aliased_join_warning("Child1->child1"): + with _aliased_join_warning(r"Child1\(child1\)"): self.assert_compile( sess.query(Child2) .join(Child2.right_children) @@ -2380,9 +2383,43 @@ class JoinedloadOverWPolyAliased( Link = self.classes.Link session = fixture_session() - q = session.query(cls).options( - joinedload(cls.links).joinedload(Link.child).joinedload(cls.links) - ) + + if ( + cls is self.classes.Sub1 + and Link.child.entity.class_ is self.classes.Parent + ): + # in 1.x we werent checking for this: + # query(Sub1).options( + # joinedload(Sub1.links).joinedload(Link.child).joinedload(Sub1.links) + # ) + # + # where Link.child points to Parent. the above is illegal because + # Link.child will return Parent instances that are not Sub1, + # so we cannot assume we will have Sub1.links available. this now + # raises + + with expect_raises_message( + exc.ArgumentError, + r'ORM mapped attribute "Sub1.links" does not link from ' + r'relationship "Link.child". Did you mean to use ' + r'"Link.child.of_type\(Sub1\)"\?', + ): + session.query(cls).options( + joinedload(cls.links) + .joinedload(Link.child) + .joinedload(cls.links) + ) + q = session.query(cls).options( + joinedload(cls.links) + .joinedload(Link.child.of_type(cls)) + .joinedload(cls.links) + ) + else: + q = session.query(cls).options( + joinedload(cls.links) + .joinedload(Link.child) + .joinedload(cls.links) + ) if cls is self.classes.Sub1: extra = " WHERE parent.type IN (__[POSTCOMPILE_type_1])" else: @@ -2740,7 +2777,9 @@ class MultipleAdaptUsesEntityOverTableTest( def test_two_joins_adaption(self): a, c, d = self.tables.a, self.tables.c, self.tables.d - with _aliased_join_warning("C->c"), _aliased_join_warning("D->d"): + with _aliased_join_warning(r"C\(c\)"), _aliased_join_warning( + r"D\(d\)" + ): q = self._two_join_fixture()._compile_state() btoc = q.from_clauses[0].left @@ -2771,7 +2810,9 @@ class MultipleAdaptUsesEntityOverTableTest( def test_two_joins_sql(self): q = self._two_join_fixture() - with _aliased_join_warning("C->c"), _aliased_join_warning("D->d"): + with _aliased_join_warning(r"C\(c\)"), _aliased_join_warning( + r"D\(d\)" + ): self.assert_compile( q, "SELECT a.name AS a_name, a_1.name AS a_1_name, " @@ -2922,7 +2963,7 @@ class BetweenSubclassJoinWExtraJoinedLoad( q = sess.query(Engineer, m1).join(Engineer.manager.of_type(m1)) with _aliased_join_warning( - "Manager->managers" + r"Manager\(managers\)" ) if autoalias else nullcontext(): self.assert_compile( q, diff --git a/test/orm/inheritance/test_single.py b/test/orm/inheritance/test_single.py index 93ec643045..1b3c6db74d 100644 --- a/test/orm/inheritance/test_single.py +++ b/test/orm/inheritance/test_single.py @@ -32,7 +32,7 @@ from sqlalchemy.testing.schema import Table def _aliased_join_warning(arg): return testing.expect_warnings( "An alias is being generated automatically against joined entity " - "mapped class %s due to overlapping tables" % (arg,) + "%s due to overlapping tables" % (arg,) ) @@ -1826,7 +1826,7 @@ class SingleFromPolySelectableTest( q = s.query(Boss).join(e1, e1.manager_id == Boss.id) with _aliased_join_warning( - "Engineer->engineer" + r"Mapper\[Engineer\(engineer\)\]" ) if autoalias else nullcontext(): self.assert_compile( q, @@ -1891,7 +1891,7 @@ class SingleFromPolySelectableTest( q = s.query(Engineer).join(b1, Engineer.manager_id == b1.id) with _aliased_join_warning( - "Boss->manager" + r"Mapper\[Boss\(manager\)\]" ) if autoalias else nullcontext(): self.assert_compile( q, diff --git a/test/orm/test_ac_relationships.py b/test/orm/test_ac_relationships.py index f59d704f3f..702b3e15f6 100644 --- a/test/orm/test_ac_relationships.py +++ b/test/orm/test_ac_relationships.py @@ -180,19 +180,20 @@ class AliasedClassRelationshipTest( s = Session(testing.db) partitioned_b = self.partitioned_b - if use_of_type: - opt = selectinload( - A.partitioned_bs.of_type(partitioned_b) - ).joinedload(B.cs) - else: - opt = selectinload(A.partitioned_bs).joinedload(B.cs) - - q = s.query(A).options(opt) - with expect_raises_message( exc.ArgumentError, - r'Attribute "B.cs" does not link from element "aliased\(B\)"', + r'ORM mapped attribute "B.cs" does not link from ' + r'relationship "A.partitioned_bs.of_type\(aliased\(B\)\)"', ): + if use_of_type: + opt = selectinload( + A.partitioned_bs.of_type(partitioned_b) + ).joinedload(B.cs) + else: + opt = selectinload(A.partitioned_bs).joinedload(B.cs) + + q = s.query(A).options(opt) + q._compile_context() diff --git a/test/orm/test_bind.py b/test/orm/test_bind.py index 11e34645dc..1d5af50643 100644 --- a/test/orm/test_bind.py +++ b/test/orm/test_bind.py @@ -646,7 +646,7 @@ class GetBindTest(fixtures.MappedTest): session = self._fixture({}) assert_raises_message( sa.exc.UnboundExecutionError, - "Could not locate a bind configured on mapper mapped class", + "Could not locate a bind configured on mapper ", session.get_bind, self.classes.BaseClass, ) @@ -657,7 +657,7 @@ class GetBindTest(fixtures.MappedTest): assert_raises_message( sa.exc.UnboundExecutionError, - "Could not locate a bind configured on mapper mapped class", + "Could not locate a bind configured on mapper ", session.get_bind, self.classes.ConcreteSubClass, ) diff --git a/test/orm/test_cache_key.py b/test/orm/test_cache_key.py index e3ba870f2d..7beadc08cf 100644 --- a/test/orm/test_cache_key.py +++ b/test/orm/test_cache_key.py @@ -122,6 +122,12 @@ class CacheKeyTest(CacheKeyFixture, _fixtures.FixtureTest): with_expression(User.name, null()), with_expression(User.name, func.foobar()), with_expression(User.name, User.name == "test"), + ), + compare_values=True, + ) + + self._run_cache_key_fixture( + lambda: ( Load(User).with_expression(User.name, true()), Load(User).with_expression(User.name, null()), Load(User).with_expression(User.name, func.foobar()), @@ -195,22 +201,18 @@ class CacheKeyTest(CacheKeyFixture, _fixtures.FixtureTest): lambda: ( joinedload(User.addresses), joinedload(User.addresses.of_type(aliased(Address))), - joinedload("addresses"), joinedload(User.orders), joinedload(User.orders.and_(Order.id != 5)), joinedload(User.orders.and_(Order.id == 5)), joinedload(User.orders.and_(Order.description != "somename")), - joinedload(User.orders).selectinload("items"), joinedload(User.orders).selectinload(Order.items), defer(User.id), - defer("id"), defer("*"), defer(Address.id), subqueryload(User.orders), selectinload(User.orders), joinedload(User.addresses).defer(Address.id), joinedload(aliased(User).addresses).defer(Address.id), - joinedload(User.addresses).defer("id"), joinedload(User.orders).joinedload(Order.items), joinedload(User.orders).subqueryload(Order.items), subqueryload(User.orders).subqueryload(Order.items), @@ -229,6 +231,7 @@ class CacheKeyTest(CacheKeyFixture, _fixtures.FixtureTest): User, Address, Keyword, Order, Item = self.classes( "User", "Address", "Keyword", "Order", "Item" ) + Dingaling = self.classes.Dingaling self._run_cache_key_fixture( lambda: ( @@ -236,7 +239,9 @@ class CacheKeyTest(CacheKeyFixture, _fixtures.FixtureTest): joinedload(Address.dingaling) ), joinedload(User.addresses).options( - joinedload(Address.dingaling).options(load_only("name")) + joinedload(Address.dingaling).options( + load_only(Dingaling.id) + ) ), joinedload(User.orders).options( joinedload(Order.items).options(joinedload(Item.keywords)) diff --git a/test/orm/test_core_compilation.py b/test/orm/test_core_compilation.py index 4bf196fa0d..000a96a422 100644 --- a/test/orm/test_core_compilation.py +++ b/test/orm/test_core_compilation.py @@ -347,7 +347,7 @@ class JoinTest(QueryTest, AssertsCompiledSQL): # the display of the attribute here is not consistent vs. # the straight aliased class, should improve this. r"explicit from clause .*User.* does not match left side .*" - r"of relationship attribute AliasedClass_User.addresses", + r"of relationship attribute aliased\(User\).addresses", stmt.compile, ) diff --git a/test/orm/test_default_strategies.py b/test/orm/test_default_strategies.py index 9162d63ecd..8b3cd84193 100644 --- a/test/orm/test_default_strategies.py +++ b/test/orm/test_default_strategies.py @@ -5,8 +5,8 @@ from sqlalchemy.orm import defaultload from sqlalchemy.orm import joinedload from sqlalchemy.orm import relationship from sqlalchemy.orm import subqueryload -from sqlalchemy.testing import assert_raises_message from sqlalchemy.testing import eq_ +from sqlalchemy.testing.assertions import expect_raises_message from sqlalchemy.testing.fixtures import fixture_session from test.orm import _fixtures @@ -255,14 +255,27 @@ class DefaultStrategyOptionsTest(_fixtures.FixtureTest): self._assert_addresses_loaded(users) def test_star_must_be_alone(self): - sess = self._downgrade_fixture() + self._downgrade_fixture() User = self.classes.User - opt = subqueryload("*", User.addresses) - assert_raises_message( + + with expect_raises_message( sa.exc.ArgumentError, "Wildcard token cannot be followed by another entity", - sess.query(User).options(opt)._compile_context, - ) + ): + subqueryload("*", User.addresses) + + def test_star_cant_be_followed(self): + self._downgrade_fixture() + User = self.classes.User + Order = self.classes.Order + + with expect_raises_message( + sa.exc.ArgumentError, + "Wildcard token cannot be followed by another entity", + ): + subqueryload(User.addresses).joinedload("*").selectinload( + Order.items + ) def test_global_star_ignored_no_entities_unbound(self): sess = self._downgrade_fixture() @@ -337,6 +350,8 @@ class DefaultStrategyOptionsTest(_fixtures.FixtureTest): # Verify lazyload('*') prevented orders.items load # users[0].orders[0] has 3 items, each with keywords: 2 sql # ('items' and 'items.keywords' subquery) + # but! the subqueryload for further sub-items *does* load. + # so at the moment the wildcard load is shut off for this load def go(): for i in users[0].orders[0].items: i.keywords diff --git a/test/orm/test_deferred.py b/test/orm/test_deferred.py index 7afaad1e9d..0d7ccde90b 100644 --- a/test/orm/test_deferred.py +++ b/test/orm/test_deferred.py @@ -29,6 +29,7 @@ from sqlalchemy.sql import literal from sqlalchemy.testing import assert_raises_message from sqlalchemy.testing import AssertsCompiledSQL from sqlalchemy.testing import eq_ +from sqlalchemy.testing import expect_raises_message from sqlalchemy.testing import fixtures from sqlalchemy.testing.fixtures import fixture_session from sqlalchemy.testing.schema import Column @@ -395,9 +396,28 @@ class DeferredOptionsTest(AssertsCompiledSQL, _fixtures.FixtureTest): ) sess.expunge_all() + # hypothetical for 2.0 - don't overwrite conflicting user-defined + # options, raise instead. + + # not sure if this behavior will fly with the userbase. however, + # it at least gives us a clear place to affirmatively resolve + # conflicts like this if we see that we need to re-enable overwriting + # of conflicting options. q2 = q.options(undefer(Order.user_id)) + with expect_raises_message( + sa.exc.InvalidRequestError, + r"Loader strategies for ORM Path\[Mapper\[Order\(orders\)\] -> " + r"Order.user_id\] conflict", + ): + q2.all() + + q3 = ( + sess.query(Order) + .order_by(Order.id) + .options(undefer(Order.user_id)) + ) self.sql_eq_( - q2.all, + q3.all, [ ( "SELECT orders.id AS orders_id, " @@ -1612,6 +1632,8 @@ class InheritanceTest(_Polymorphic): q = s.query(Company).options( joinedload(Company.employees.of_type(Manager)).defer("*") ) + # TODO: this is wrong! there's no employee columns!! + # see https://github.com/sqlalchemy/sqlalchemy/issues/7495 self.assert_compile( q, "SELECT companies.company_id AS companies_company_id, " @@ -1667,19 +1689,24 @@ class InheritanceTest(_Polymorphic): "ORDER BY people.person_id", ) - def test_load_only_from_with_polymorphic(self): + def test_load_only_from_with_polymorphic_mismatch(self): s = fixture_session() wp = with_polymorphic(Person, [Manager], flat=True) assert_raises_message( sa.exc.ArgumentError, - 'Mapped attribute "Manager.status" does not apply to any of the ' - "root entities in this query, e.g. " + r"Mapped class Mapper\[Manager\(managers\)\] does not apply to " + "any of the root entities in this query, e.g. " r"with_polymorphic\(Person, \[Manager\]\).", s.query(wp).options(load_only(Manager.status))._compile_context, ) + def test_load_only_from_with_polymorphic_applied(self): + s = fixture_session() + + wp = with_polymorphic(Person, [Manager], flat=True) + q = s.query(wp).options(load_only(wp.Manager.status)) self.assert_compile( q, @@ -1697,18 +1724,17 @@ class InheritanceTest(_Polymorphic): wp = with_polymorphic(Person, [Manager], flat=True) - assert_raises_message( + with expect_raises_message( sa.exc.ArgumentError, - 'Attribute "Manager.status" does not link from element ' - r'"with_polymorphic\(Person, \[Manager\]\)"', - s.query(Company) - .options( + 'ORM mapped attribute "Manager.status" does not link from ' + r'relationship "Company.employees.' + r'of_type\(with_polymorphic\(Person, \[Manager\]\)\)".', + ): + s.query(Company).options( joinedload(Company.employees.of_type(wp)).load_only( Manager.status ) - ) - ._compile_context, - ) + )._compile_context() self.assert_compile( s.query(Company).options( diff --git a/test/orm/test_deprecations.py b/test/orm/test_deprecations.py index 535196c253..bae2a9707a 100644 --- a/test/orm/test_deprecations.py +++ b/test/orm/test_deprecations.py @@ -11,7 +11,6 @@ from sqlalchemy import event from sqlalchemy import exc as sa_exc from sqlalchemy import ForeignKey from sqlalchemy import func -from sqlalchemy import inspect from sqlalchemy import Integer from sqlalchemy import join from sqlalchemy import LABEL_STYLE_TABLENAME_PLUS_COL @@ -41,7 +40,6 @@ from sqlalchemy.orm import defaultload from sqlalchemy.orm import defer from sqlalchemy.orm import deferred from sqlalchemy.orm import eagerload -from sqlalchemy.orm import exc as orm_exc from sqlalchemy.orm import foreign from sqlalchemy.orm import instrumentation from sqlalchemy.orm import joinedload @@ -51,7 +49,6 @@ from sqlalchemy.orm import relationship from sqlalchemy.orm import scoped_session from sqlalchemy.orm import Session from sqlalchemy.orm import sessionmaker -from sqlalchemy.orm import strategy_options from sqlalchemy.orm import subqueryload from sqlalchemy.orm import synonym from sqlalchemy.orm import undefer @@ -153,14 +150,14 @@ dep_exc_wildcard = ( def _aliased_join_warning(arg=None): return testing.expect_warnings( "An alias is being generated automatically against joined entity " - "mapped class " + (arg if arg else "") + "Mapper" + (arg if arg else "") ) def _aliased_join_deprecation(arg=None): return testing.expect_deprecated( "An alias is being generated automatically against joined entity " - "mapped class " + (arg if arg else "") + "Mapper" + (arg if arg else "") ) @@ -2480,7 +2477,7 @@ class DeprecatedInhTest(_poly_fixtures._Polymorphic): # note python 2 does not allow parens here; reformat in py3 only with DeprecatedQueryTest._expect_implicit_subquery(), _aliased_join_warning( # noqa E501 - "Person->people" + r"\[Person\(people\)\]" ): self.assert_compile( sess.query(Company) @@ -2722,10 +2719,14 @@ class DeprecatedOptionAllTest(OptionsPathTest, _fixtures.FixtureTest): sess = fixture_session() with testing.expect_deprecated(undefer_needs_chaining): - sess.query(User).options(defer("addresses", "email_address")) + sess.query(User).options( + defer(User.addresses, Address.email_address) + ) with testing.expect_deprecated(undefer_needs_chaining): - sess.query(User).options(undefer("addresses", "email_address")) + sess.query(User).options( + undefer(User.addresses, Address.email_address) + ) class InstrumentationTest(fixtures.ORMTest): @@ -3545,14 +3546,16 @@ class TextTest(QueryTest): ) -class TestDeprecation20(fixtures.TestBase): +class TestDeprecation20(QueryTest): def test_relation(self): + User = self.classes.User with testing.expect_deprecated_20(".*relationship"): - relation("foo") + relation(User.addresses) def test_eagerloading(self): + User = self.classes.User with testing.expect_deprecated_20(".*joinedload"): - eagerload("foo") + eagerload(User.addresses) class DistinctOrderByImplicitTest(QueryTest, AssertsCompiledSQL): @@ -4943,31 +4946,6 @@ class DeferredOptionsTest(AssertsCompiledSQL, _fixtures.FixtureTest): eq_(item.description, "item 4") -class OptionsTest(PathTest, OptionsQueryTest): - def _option_fixture(self, *arg): - return strategy_options._UnboundLoad._from_keys( - strategy_options._UnboundLoad.joinedload, arg, True, {} - ) - - def test_with_current_nonmatching_string(self): - Item, User, Order = ( - self.classes.Item, - self.classes.User, - self.classes.Order, - ) - - sess = fixture_session() - q = sess.query(Item)._with_current_path( - self._make_path_registry([User, "orders", Order, "items"]) - ) - - opt = self._option_fixture("keywords") - self._assert_path_result(opt, q, []) - - opt = self._option_fixture("items.keywords") - self._assert_path_result(opt, q, []) - - class SubOptionsTest(PathTest, OptionsQueryTest): run_create_tables = False run_inserts = None @@ -5021,164 +4999,6 @@ class SubOptionsTest(PathTest, OptionsQueryTest): ) -class OptionsNoPropTest(_fixtures.FixtureTest): - """test the error messages emitted when using property - options in conjunction with column-only entities, or - for not existing options - - """ - - run_create_tables = False - run_inserts = None - run_deletes = None - - def test_option_with_column_basestring(self): - Item = self.classes.Item - - message = ( - "Query has only expression-based entities - can't " - 'find property named "keywords".' - ) - self._assert_eager_with_just_column_exception( - Item.id, "keywords", message - ) - - @testing.fails_if( - lambda: True, - "PropertyOption doesn't yet check for relation/column on end result", - ) - def test_option_against_non_relation_basestring(self): - Item = self.classes.Item - Keyword = self.classes.Keyword - self._assert_eager_with_entity_exception( - [Keyword, Item], - (joinedload("keywords"),), - r"Attribute 'keywords' of entity 'Mapper\|Keyword\|keywords' " - "does not refer to a mapped entity", - ) - - @testing.fails_if( - lambda: True, - "PropertyOption doesn't yet check for relation/column on end result", - ) - def test_option_against_multi_non_relation_basestring(self): - Item = self.classes.Item - Keyword = self.classes.Keyword - self._assert_eager_with_entity_exception( - [Keyword, Item], - (joinedload("keywords"),), - r"Attribute 'keywords' of entity 'Mapper\|Keyword\|keywords' " - "does not refer to a mapped entity", - ) - - def test_option_against_multi_no_entities_basestring(self): - Item = self.classes.Item - Keyword = self.classes.Keyword - self._assert_eager_with_entity_exception( - [Keyword.id, Item.id], - (joinedload("keywords"),), - r"Query has only expression-based entities - can't find property " - 'named "keywords".', - ) - - @classmethod - def setup_mappers(cls): - users, User, addresses, Address, orders, Order = ( - cls.tables.users, - cls.classes.User, - cls.tables.addresses, - cls.classes.Address, - cls.tables.orders, - cls.classes.Order, - ) - cls.mapper_registry.map_imperatively( - User, - users, - properties={ - "addresses": relationship(Address), - "orders": relationship(Order), - }, - ) - cls.mapper_registry.map_imperatively(Address, addresses) - cls.mapper_registry.map_imperatively(Order, orders) - keywords, items, item_keywords, Keyword, Item = ( - cls.tables.keywords, - cls.tables.items, - cls.tables.item_keywords, - cls.classes.Keyword, - cls.classes.Item, - ) - cls.mapper_registry.map_imperatively( - Keyword, - keywords, - properties={ - "keywords": column_property(keywords.c.name + "some keyword") - }, - ) - cls.mapper_registry.map_imperatively( - Item, - items, - properties=dict( - keywords=relationship(Keyword, secondary=item_keywords) - ), - ) - - class OrderWProp(cls.classes.Order): - @property - def some_attr(self): - return "hi" - - cls.mapper_registry.map_imperatively( - OrderWProp, None, inherits=cls.classes.Order - ) - - def _assert_option(self, entity_list, option): - Item = self.classes.Item - - context = ( - fixture_session() - .query(*entity_list) - .options(joinedload(option)) - ._compile_state() - ) - key = ("loader", (inspect(Item), inspect(Item).attrs.keywords)) - assert key in context.attributes - - def _assert_loader_strategy_exception(self, entity_list, options, message): - assert_raises_message( - orm_exc.LoaderStrategyException, - message, - fixture_session() - .query(*entity_list) - .options(*options) - ._compile_state, - ) - - def _assert_eager_with_entity_exception( - self, entity_list, options, message - ): - assert_raises_message( - sa.exc.ArgumentError, - message, - fixture_session() - .query(*entity_list) - .options(*options) - ._compile_state, - ) - - def _assert_eager_with_just_column_exception( - self, column, eager_option, message - ): - assert_raises_message( - sa.exc.ArgumentError, - message, - fixture_session() - .query(column) - .options(joinedload(eager_option)) - ._compile_state, - ) - - class PolyCacheKeyTest(CacheKeyFixture, _poly_fixtures._Polymorphic): run_setup_mappers = "once" run_inserts = None diff --git a/test/orm/test_eager_relations.py b/test/orm/test_eager_relations.py index 6d6d10deaf..42b3bd1ea5 100644 --- a/test/orm/test_eager_relations.py +++ b/test/orm/test_eager_relations.py @@ -2399,7 +2399,8 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL): "ON orders_1.id = order_items_1.order_id", ) - def test_inner_join_nested_chaining_negative_options(self): + @testing.fixture + def _inner_join_nested_fixture(self): users, items, order_items, Order, Item, User, orders = ( self.tables.users, self.tables.items, @@ -2434,6 +2435,12 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL): ) self.mapper_registry.map_imperatively(Item, items) + return User, Order, Item + + def test_inner_join_nested_chaining_negative_options_one( + self, _inner_join_nested_fixture + ): + User, Order, Item = _inner_join_nested_fixture sess = fixture_session() self.assert_compile( sess.query(User), @@ -2453,6 +2460,11 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL): "order_items_1.item_id ORDER BY orders_1.id, items_1.id", ) + def test_inner_join_nested_chaining_negative_options_two( + self, _inner_join_nested_fixture + ): + User, Order, Item = _inner_join_nested_fixture + sess = fixture_session() q = sess.query(User).options(joinedload(User.orders, innerjoin=False)) self.assert_compile( q, @@ -2501,6 +2513,11 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL): q.order_by(User.id).all(), ) + def test_inner_join_nested_chaining_negative_options_three( + self, _inner_join_nested_fixture + ): + User, Order, Item = _inner_join_nested_fixture + sess = fixture_session() self.assert_compile( sess.query(User).options( joinedload(User.orders, Order.items, innerjoin=False) @@ -6098,7 +6115,8 @@ class EntityViaMultiplePathTestTwo(fixtures.DeclarativeMappedTest): lz_test = ( s.query(LDA) .join(LDA.ld) - .options(contains_eager(LDA.ld)) + # this conflicts in 2.0 + # .options(contains_eager(LDA.ld)) .join(LDA.a) .join(LDA.ld.of_type(l_ac)) .join(l_ac.user.of_type(u_ac)) @@ -6257,7 +6275,6 @@ class LazyLoadOptSpecificityTest(fixtures.DeclarativeMappedTest): .filter(aa.id != A.id) .options(defaultload(aa.bs).joinedload(B.cs)) ) - self._run_tests(q, 2) def test_joinedload_aliased_abs_bcs(self): @@ -6500,8 +6517,7 @@ class DeepOptionsTest(_fixtures.FixtureTest): self.sql_count_(0, go) def test_deep_options_4(self): - Item, User, Order = ( - self.classes.Item, + User, Order = ( self.classes.User, self.classes.Order, ) @@ -6510,8 +6526,9 @@ class DeepOptionsTest(_fixtures.FixtureTest): assert_raises_message( sa.exc.ArgumentError, - 'Mapped attribute "Order.items" does not apply to any of the ' - "root entities in this query, e.g. mapped class User->users. " + r"Mapped class Mapper\[Order\(orders\)\] does not apply to any of " + "the " + r"root entities in this query, e.g. Mapper\[User\(users\)\]. " "Please specify the full path from one of the root entities " "to the target attribute.", sess.query(User) @@ -6519,6 +6536,15 @@ class DeepOptionsTest(_fixtures.FixtureTest): ._compile_context, ) + def test_deep_options_5(self): + Item, User, Order = ( + self.classes.Item, + self.classes.User, + self.classes.Order, + ) + + sess = fixture_session() + # joinedload "keywords" on items. it will lazy load "orders", then # lazy load the "items" on the order, but on "items" it will eager # load the "keywords" @@ -6538,11 +6564,23 @@ class DeepOptionsTest(_fixtures.FixtureTest): self.sql_count_(2, go) + def test_deep_options_6(self): + Item, User, Order = ( + self.classes.Item, + self.classes.User, + self.classes.Order, + ) + sess = fixture_session() q3 = ( sess.query(User) .order_by(User.id) .options( + # this syntax means: + # defautload(User.orders).defaultload(Order.items). + # joinedload(Item.keywords) + # + # intuitive right ? :) sa.orm.joinedload(User.orders, Order.items, Item.keywords) ) ) diff --git a/test/orm/test_expire.py b/test/orm/test_expire.py index a5abcb3554..3e3488d526 100644 --- a/test/orm/test_expire.py +++ b/test/orm/test_expire.py @@ -882,7 +882,7 @@ class ExpireTest(_fixtures.FixtureTest): # for up front with expect_raises_message( sa.exc.ArgumentError, - 'Mapped attribute "User.addresses" does not apply to ' + r"Mapped class Mapper\[User\(users\)\] does not apply to " "any of the root entities in this query", ): row = sess.execute( diff --git a/test/orm/test_joins.py b/test/orm/test_joins.py index 351938a512..58b09b67d7 100644 --- a/test/orm/test_joins.py +++ b/test/orm/test_joins.py @@ -167,9 +167,9 @@ class InheritedJoinTest(InheritedTest, AssertsCompiledSQL): with testing.expect_warnings( "An alias is being generated automatically against joined entity " - "mapped class Manager->managers due to overlapping", + r"Mapper\[Manager\(managers\)\] due to overlapping", "An alias is being generated automatically against joined entity " - "mapped class Boss->boss due to overlapping", + r"Mapper\[Boss\(boss\)\] due to overlapping", raise_on_any_unexpected=True, ): self.assert_compile( @@ -2470,8 +2470,8 @@ class SelfReferentialTest(fixtures.MappedTest, AssertsCompiledSQL): s = fixture_session() assert_raises_message( sa.exc.InvalidRequestError, - "Can't construct a join from mapped class Node->nodes to mapped " - "class Node->nodes, they are the same entity", + r"Can't construct a join from Mapper\[Node\(nodes\)\] to " + r"Mapper\[Node\(nodes\)\], they are the same entity", s.query(Node).join(Node.children)._compile_context, ) diff --git a/test/orm/test_mapper.py b/test/orm/test_mapper.py index e5e819bbbf..b491604f30 100644 --- a/test/orm/test_mapper.py +++ b/test/orm/test_mapper.py @@ -263,7 +263,7 @@ class MapperTest(_fixtures.FixtureTest, AssertsCompiledSQL): sa.exc.InvalidRequestError, "One or more mappers failed to initialize - can't " "proceed with initialization of other mappers. " - "Triggering mapper: 'mapped class Address->addresses'. " + r"Triggering mapper: 'Mapper\[Address\(addresses\)\]'. " "Original exception was: Class 'test.orm._fixtures.User' " "is not mapped", configure_mappers, @@ -825,7 +825,7 @@ class MapperTest(_fixtures.FixtureTest, AssertsCompiledSQL): with testing.expect_warnings( "An alias is being generated automatically against joined " - "entity mapped class SubUser" + r"entity Mapper\[SubUser" ): self.assert_compile( q, @@ -3149,7 +3149,9 @@ class ConfigureOrNotConfigureTest(_fixtures.FixtureTest, AssertsCompiledSQL): stmt = select(User).options( load_only(User.name), ) - is_false(um.configured) + # all options are "bound" Load objects now, + # so this operation configures mappers + is_true(um.configured) self.assert_compile( stmt, diff --git a/test/orm/test_options.py b/test/orm/test_options.py index 25d5dcc6e5..7098220801 100644 --- a/test/orm/test_options.py +++ b/test/orm/test_options.py @@ -10,6 +10,7 @@ from sqlalchemy.orm import aliased from sqlalchemy.orm import attributes from sqlalchemy.orm import class_mapper from sqlalchemy.orm import column_property +from sqlalchemy.orm import contains_eager from sqlalchemy.orm import defaultload from sqlalchemy.orm import defer from sqlalchemy.orm import exc as orm_exc @@ -20,6 +21,7 @@ from sqlalchemy.orm import relationship from sqlalchemy.orm import strategy_options from sqlalchemy.orm import subqueryload from sqlalchemy.orm import synonym +from sqlalchemy.orm import undefer from sqlalchemy.orm import util as orm_util from sqlalchemy.orm import with_polymorphic from sqlalchemy.testing import fixtures @@ -91,21 +93,9 @@ class PathTest: def _assert_path_result(self, opt, q, paths): attr = {} - if isinstance(opt, strategy_options._UnboundLoad): - for val in opt._to_bind: - val._bind_loader( - [ - ent.entity_zero - for ent in q._compile_state()._lead_mapper_entities - ], - q._compile_options._current_path, - attr, - False, - ) - else: - compile_state = q._compile_state() - compile_state.attributes = attr = {} - opt._process(compile_state, [], True) + compile_state = q._compile_state() + compile_state.attributes = attr = {} + opt.process_compile_state(compile_state) assert_paths = [k[1] for k in attr] eq_( @@ -118,35 +108,48 @@ class LoadTest(PathTest, QueryTest): def test_str(self): User = self.classes.User result = Load(User) - result.strategy = (("deferred", False), ("instrument", True)) eq_( str(result), - "Load(strategy=(('deferred', False), ('instrument', True)))", + "Load(Mapper[User(users)])", + ) + + result = Load(aliased(User)) + eq_( + str(result), + "Load(aliased(User))", ) def test_gen_path_attr_entity(self): User = self.classes.User Address = self.classes.Address - result = Load(User) + ll = Load(User) + eq_( - result._generate_path( - inspect(User)._path_registry, + strategy_options._AttributeStrategyLoad.create( + ll.path, User.addresses, - None, + ("strategy", True), "relationship", - ), + {}, + True, + ).path, self._make_path_registry([User, "addresses", Address]), ) def test_gen_path_attr_column(self): User = self.classes.User - result = Load(User) + ll = Load(User) eq_( - result._generate_path( - inspect(User)._path_registry, User.name, None, "column" - ), + strategy_options._AttributeStrategyLoad.create( + ll.path, + User.name, + ("strategy", True), + "column", + {}, + True, + ).path, self._make_path_registry([User, "name"]), ) @@ -159,9 +162,8 @@ class LoadTest(PathTest, QueryTest): sa.exc.ArgumentError, "Attribute 'name' of entity 'Mapper|User|users' does " "not refer to a mapped entity", - result._generate_path, - result.path, - User.addresses, + result._clone_for_bind_strategy, + (User.addresses,), None, "relationship", ) @@ -176,9 +178,8 @@ class LoadTest(PathTest, QueryTest): sa.exc.ArgumentError, "Attribute 'Order.items' does not link from element " "'Mapper|User|users'", - result._generate_path, - inspect(User)._path_registry, - Order.items, + result._clone_for_bind_strategy, + (Order.items,), None, "relationship", ) @@ -187,15 +188,17 @@ class LoadTest(PathTest, QueryTest): User = self.classes.User Order = self.classes.Order - result = Load(User) + ll = Load(User) eq_( - result._generate_path( - inspect(User)._path_registry, + strategy_options._AttributeStrategyLoad.create( + ll.path, Order.items, - None, + ("strategy", True), "relationship", - False, + {}, + True, + raiseerr=False, ), None, ) @@ -205,10 +208,14 @@ class LoadTest(PathTest, QueryTest): l1 = Load(User) l2 = l1.joinedload(User.addresses) - to_bind = list(l2.context.values())[0] + + s = fixture_session() + q1 = s.query(User).options(l2) + attr = q1._compile_context().attributes + eq_( - l1.context, - {("loader", self._make_path([User, "addresses"])): to_bind}, + attr[("loader", self._make_path([User, "addresses"]))], + l2.context[0], ) def test_set_strat_col(self): @@ -216,8 +223,11 @@ class LoadTest(PathTest, QueryTest): l1 = Load(User) l2 = l1.defer(User.name) - l3 = list(l2.context.values())[0] - eq_(l1.context, {("loader", self._make_path([User, "name"])): l3}) + s = fixture_session() + q1 = s.query(User).options(l2) + attr = q1._compile_context().attributes + + eq_(attr[("loader", self._make_path([User, "name"]))], l2.context[0]) class OfTypePathingTest(PathTest, QueryTest): @@ -397,8 +407,14 @@ class WithEntitiesTest(QueryTest, AssertsCompiledSQL): class OptionsTest(PathTest, QueryTest): def _option_fixture(self, *arg): - return strategy_options._UnboundLoad._from_keys( - strategy_options._UnboundLoad.joinedload, arg, True, {} + # note contains_eager() passes chained=True to _from_keys, + # which means an expression like contains_eager(a, b, c) + # is expected to produce + # contains_eager(a).contains_eager(b).contains_eager(c). no other + # loader option works this way right now; the rest all use + # defaultload() for the "chain" elements + return strategy_options._generate_from_keys( + strategy_options.Load.contains_eager, arg, True, {} ) @testing.combinations( @@ -410,30 +426,12 @@ class OptionsTest(PathTest, QueryTest): def test_error_for_string_names_unbound(self, test_case): User, Address = self.classes("User", "Address") - query = fixture_session().query(User) - with expect_raises_message( sa.exc.ArgumentError, "Strings are not accepted for attribute names in loader " "options; please use class-bound attributes directly.", ): - unbound_opt = testing.resolve_lambda( - test_case, User=User, Address=Address - ) - - # TODO: the strings above should raise immediately, we should - # not have to bind_loader for this to occur. - attr = {} - for opt in unbound_opt._to_bind: - opt._bind_loader( - [ - ent.entity_zero - for ent in query._compile_state()._lead_mapper_entities - ], - query._compile_options._current_path, - attr, - False, - ) + testing.resolve_lambda(test_case, User=User, Address=Address) @testing.combinations( lambda User: Load(User).joinedload("addresses"), @@ -462,22 +460,6 @@ class OptionsTest(PathTest, QueryTest): opt = self._option_fixture(User.addresses) self._assert_path_result(opt, q, [(User, "addresses")]) - def test_path_on_entity_but_doesnt_match_currentpath(self): - User, Address = self.classes.User, self.classes.Address - - # ensure "current path" is fully consumed before - # matching against current entities. - # see [ticket:2098] - sess = fixture_session() - q = sess.query(User) - opt = self._option_fixture("email_address", "id") - q = sess.query(Address)._with_current_path( - orm_util.PathRegistry.coerce( - [inspect(User), inspect(User).attrs.addresses] - ) - ) - self._assert_path_result(opt, q, []) - def test_get_path_one_level_with_unrelated(self): Order = self.classes.Order User = self.classes.User @@ -485,7 +467,13 @@ class OptionsTest(PathTest, QueryTest): sess = fixture_session() q = sess.query(Order) opt = self._option_fixture(User.addresses) - self._assert_path_result(opt, q, []) + + with expect_raises_message( + sa.exc.ArgumentError, + r"Mapped class Mapper\[User\(users\)\] does not apply to any " + "of the root entities in this query", + ): + self._assert_path_result(opt, q, []) def test_path_multilevel_attribute(self): Item, User, Order = ( @@ -739,10 +727,12 @@ class OptionsTest(PathTest, QueryTest): sess = fixture_session() ualias = aliased(User) - q = sess.query(ualias)._with_current_path( + q = sess.query(Address)._with_current_path( self._make_path_registry([Address, "user"]) ) - opt = self._option_fixture(Address.user, ualias.addresses) + opt = self._option_fixture( + Address.user.of_type(ualias), ualias.addresses + ) self._assert_path_result(opt, q, [(inspect(ualias), "addresses")]) def test_with_current_aliased_single_nonmatching_option(self): @@ -751,9 +741,11 @@ class OptionsTest(PathTest, QueryTest): sess = fixture_session() ualias = aliased(User) q = sess.query(User)._with_current_path( - self._make_path_registry([Address, "user"]) + self._make_path_registry([User, "addresses", Address, "user"]) + ) + opt = self._option_fixture( + Address.user.of_type(ualias), ualias.addresses ) - opt = self._option_fixture(Address.user, ualias.addresses) self._assert_path_result(opt, q, []) def test_with_current_aliased_single_nonmatching_entity(self): @@ -762,7 +754,12 @@ class OptionsTest(PathTest, QueryTest): sess = fixture_session() ualias = aliased(User) q = sess.query(ualias)._with_current_path( - self._make_path_registry([Address, "user"]) + # this was: + # self._make_path_registry([Address, "user"]) + # .. which seems like an impossible "current_path" + # + # this one makes a little more sense + self._make_path_registry([ualias, "addresses", Address, "user"]) ) opt = self._option_fixture(Address.user, User.addresses) self._assert_path_result(opt, q, []) @@ -775,14 +772,6 @@ class OptionsTest(PathTest, QueryTest): q = sess.query(Item, Order) self._assert_path_result(opt, q, [(Order, "items")]) - def test_multi_entity_no_mapped_entities(self): - Item = self.classes.Item - Order = self.classes.Order - opt = self._option_fixture("items") - sess = fixture_session() - q = sess.query(Item.id, Order.id) - self._assert_path_result(opt, q, []) - def test_path_exhausted(self): User = self.classes.User Item = self.classes.Item @@ -813,6 +802,7 @@ class OptionsTest(PathTest, QueryTest): opt = self._option_fixture(User.orders, Order.items).joinedload( Item.keywords ) + self._assert_path_result( opt, q, @@ -903,8 +893,8 @@ class OptionsNoPropTest(_fixtures.FixtureTest): self._assert_eager_with_just_column_exception( Item.id, Item.keywords, - "Query has only expression-based entities, which do not apply " - 'to relationship property "Item.keywords"', + r"Query has only expression-based entities; attribute loader " + r"options for Mapper\[Item\(items\)\] can't be applied here.", ) def test_option_against_nonexistent_PropComparator(self): @@ -913,9 +903,10 @@ class OptionsNoPropTest(_fixtures.FixtureTest): self._assert_eager_with_entity_exception( [Keyword], (joinedload(Item.keywords),), - 'Mapped attribute "Item.keywords" does not apply to any of the ' - "root entities in this query, e.g. mapped class " - "Keyword->keywords. Please specify the full path from one of " + r"Mapped class Mapper\[Item\(items\)\] does not apply to any of " + "the root entities in this query, e.g. " + r"Mapper\[Keyword\(keywords\)\]. " + "Please specify the full path from one of " "the root entities to the target attribute. ", ) @@ -924,20 +915,17 @@ class OptionsNoPropTest(_fixtures.FixtureTest): Item = self.classes.Item self._assert_eager_with_entity_exception( [User, Item], - (load_only(User.id, Item.id),), + lambda: (load_only(User.id, Item.id),), r"Can't apply wildcard \('\*'\) or load_only\(\) loader option " - "to multiple entities mapped class User->users, mapped class " - "Item->items. Specify loader options for each entity " - "individually, such as " - r"Load\(mapped class User->users\).some_option\('\*'\), " - r"Load\(mapped class Item->items\).some_option\('\*'\).", + r"to multiple entities in the same option. Use separate options " + "per entity.", ) def test_col_option_against_relationship_attr(self): Item = self.classes.Item self._assert_loader_strategy_exception( [Item], - (load_only(Item.keywords),), + lambda: (load_only(Item.keywords),), 'Can\'t apply "column loader" strategy to property ' '"Item.keywords", which is a "relationship property"; this ' 'loader strategy is intended to be used with a "column property".', @@ -948,7 +936,7 @@ class OptionsNoPropTest(_fixtures.FixtureTest): Keyword = self.classes.Keyword self._assert_loader_strategy_exception( [Keyword, Item], - (joinedload(Keyword.id).joinedload(Item.keywords),), + lambda: (joinedload(Keyword.id).joinedload(Item.keywords),), 'Can\'t apply "joined loader" strategy to property "Keyword.id", ' 'which is a "column property"; this loader strategy is intended ' 'to be used with a "relationship property".', @@ -959,7 +947,7 @@ class OptionsNoPropTest(_fixtures.FixtureTest): Keyword = self.classes.Keyword self._assert_loader_strategy_exception( [Keyword, Item], - (joinedload(Keyword.keywords).joinedload(Item.keywords),), + lambda: (joinedload(Keyword.keywords).joinedload(Item.keywords),), 'Can\'t apply "joined loader" strategy to property ' '"Keyword.keywords", which is a "column property"; this loader ' 'strategy is intended to be used with a "relationship property".', @@ -970,36 +958,57 @@ class OptionsNoPropTest(_fixtures.FixtureTest): Keyword = self.classes.Keyword self._assert_eager_with_entity_exception( [Keyword.id, Item.id], - (joinedload(Keyword.keywords).joinedload(Item.keywords),), - "Query has only expression-based entities, which do not apply to " - 'column property "Keyword.keywords"', + lambda: (joinedload(Keyword.keywords),), + r"Query has only expression-based entities; attribute loader " + r"options for Mapper\[Keyword\(keywords\)\] can't be applied " + "here.", ) - def test_wrong_type_in_option_cls(self): + @testing.combinations(True, False, argnames="first_element") + def test_wrong_type_in_option_cls(self, first_element): Item = self.classes.Item Keyword = self.classes.Keyword self._assert_eager_with_entity_exception( [Item], - (joinedload(Keyword),), - r"mapper option expects string key or list of attributes", + lambda: (joinedload(Keyword),) + if first_element + else (Load(Item).joinedload(Keyword),), + "expected ORM mapped attribute for loader " "strategy argument", + ) + + @testing.combinations( + (15,), (object(),), (type,), ({"foo": "bar"},), argnames="rando" + ) + @testing.combinations(True, False, argnames="first_element") + def test_wrong_type_in_option_any_random_type(self, rando, first_element): + Item = self.classes.Item + self._assert_eager_with_entity_exception( + [Item], + lambda: (joinedload(rando),) + if first_element + else (Load(Item).joinedload(rando)), + "expected ORM mapped attribute for loader strategy argument", ) - def test_wrong_type_in_option_descriptor(self): + @testing.combinations(True, False, argnames="first_element") + def test_wrong_type_in_option_descriptor(self, first_element): OrderWProp = self.classes.OrderWProp self._assert_eager_with_entity_exception( [OrderWProp], - (joinedload(OrderWProp.some_attr),), - r"mapper option expects string key or list of attributes", + lambda: (joinedload(OrderWProp.some_attr),) + if first_element + else (Load(OrderWProp).joinedload(OrderWProp.some_attr),), + "expected ORM mapped attribute for loader strategy argument", ) def test_non_contiguous_all_option(self): User = self.classes.User self._assert_eager_with_entity_exception( [User], - (joinedload(User.addresses).joinedload(User.orders),), - r"Attribute 'User.orders' does not link " - "from element 'Mapper|Address|addresses'", + lambda: (joinedload(User.addresses).joinedload(User.orders),), + r'ORM mapped attribute "User.orders" does not link ' + r'from relationship "User.addresses"', ) def test_non_contiguous_all_option_of_type(self): @@ -1007,13 +1016,13 @@ class OptionsNoPropTest(_fixtures.FixtureTest): Order = self.classes.Order self._assert_eager_with_entity_exception( [User], - ( + lambda: ( joinedload(User.addresses).joinedload( User.orders.of_type(Order) ), ), - r"Attribute 'User.orders' does not link " - "from element 'Mapper|Address|addresses'", + r'ORM mapped attribute "User.orders" does not link ' + r'from relationship "User.addresses"', ) @classmethod @@ -1080,26 +1089,30 @@ class OptionsNoPropTest(_fixtures.FixtureTest): assert key in context.attributes def _assert_loader_strategy_exception(self, entity_list, options, message): - assert_raises_message( - orm_exc.LoaderStrategyException, - message, - fixture_session() - .query(*entity_list) - .options(*options) - ._compile_state, - ) + sess = fixture_session() + with expect_raises_message(orm_exc.LoaderStrategyException, message): + # accommodate Load() objects that will raise + # on construction + if callable(options): + options = options() + + # accommodate UnboundLoad objects that will raise + # only when compile state is set up + sess.query(*entity_list).options(*options)._compile_state() def _assert_eager_with_entity_exception( self, entity_list, options, message ): - assert_raises_message( - sa.exc.ArgumentError, - message, - fixture_session() - .query(*entity_list) - .options(*options) - ._compile_state, - ) + sess = fixture_session() + with expect_raises_message(sa.exc.ArgumentError, message): + # accommodate Load() objects that will raise + # on construction + if callable(options): + options = options() + + # accommodate UnboundLoad objects that will raise + # only when compile state is set up + sess.query(*entity_list).options(*options)._compile_state() def _assert_eager_with_just_column_exception( self, column, eager_option, message @@ -1122,26 +1135,46 @@ class OptionsNoPropTestInh(_Polymorphic): assert_raises_message( sa.exc.ArgumentError, - r'Mapped attribute "Manager.status" does not apply to any of ' + r"Mapped class Mapper\[Manager\(managers\)\] does not apply to " + "any of " r"the root entities in this query, e.g. " r"with_polymorphic\(Person, \[Manager\]\).", s.query(wp).options(load_only(Manager.status))._compile_state, ) - def test_missing_attr_of_type_subclass(self): + def test_missing_attr_of_type_subclass_one(self): s = fixture_session() + e1 = with_polymorphic(Person, [Engineer]) assert_raises_message( sa.exc.ArgumentError, - r'Attribute "Manager.manager_name" does not link from element ' - r'"with_polymorphic\(Person, \[Engineer\]\)".$', - s.query(Company) + r'ORM mapped attribute "Manager.manager_name" does not link from ' + r'relationship "Company.employees.' + r'of_type\(with_polymorphic\(Person, \[Engineer\]\)\)".$', + lambda: s.query(Company) + .options( + joinedload(Company.employees.of_type(e1)).load_only( + Manager.manager_name + ) + ) + ._compile_state(), + ) + + def test_missing_attr_of_type_subclass_two(self): + s = fixture_session() + + assert_raises_message( + sa.exc.ArgumentError, + r'ORM mapped attribute "Manager.manager_name" does not link from ' + r'relationship "Company.employees.' + r'of_type\(Mapper\[Engineer\(engineers\)\]\)".$', + lambda: s.query(Company) .options( joinedload(Company.employees.of_type(Engineer)).load_only( Manager.manager_name ) ) - ._compile_state, + ._compile_state(), ) def test_missing_attr_of_type_subclass_name_matches(self): @@ -1151,15 +1184,16 @@ class OptionsNoPropTestInh(_Polymorphic): # that doesn't get mixed up here assert_raises_message( sa.exc.ArgumentError, - r'Attribute "Manager.status" does not link from element ' - r'"with_polymorphic\(Person, \[Engineer\]\)".$', - s.query(Company) + r'ORM mapped attribute "Manager.status" does not link from ' + r'relationship "Company.employees.' + r'of_type\(Mapper\[Engineer\(engineers\)\]\)".$', + lambda: s.query(Company) .options( joinedload(Company.employees.of_type(Engineer)).load_only( Manager.status ) ) - ._compile_state, + ._compile_state(), ) def test_missing_attr_of_type_wpoly_subclass(self): @@ -1169,15 +1203,16 @@ class OptionsNoPropTestInh(_Polymorphic): assert_raises_message( sa.exc.ArgumentError, - r'Attribute "Manager.manager_name" does not link from ' - r'element "with_polymorphic\(Person, \[Manager\]\)".$', - s.query(Company) + r'ORM mapped attribute "Manager.manager_name" does not link from ' + r'relationship "Company.employees.' + r'of_type\(with_polymorphic\(Person, \[Manager\]\)\)".$', + lambda: s.query(Company) .options( joinedload(Company.employees.of_type(wp)).load_only( Manager.manager_name ) ) - ._compile_state, + ._compile_state(), ) def test_missing_attr_is_missing_of_type_for_alias(self): @@ -1187,12 +1222,12 @@ class OptionsNoPropTestInh(_Polymorphic): assert_raises_message( sa.exc.ArgumentError, - r'Attribute "AliasedClass_Person.name" does not link from ' - r'element "mapped class Person->people". Did you mean to use ' - r"Company.employees.of_type\(AliasedClass_Person\)\?", - s.query(Company) + r'ORM mapped attribute "aliased\(Person\).name" does not link ' + r'from relationship "Company.employees". Did you mean to use ' + r'"Company.employees.of_type\(aliased\(Person\)\)\"?', + lambda: s.query(Company) .options(joinedload(Company.employees).load_only(pa.name)) - ._compile_state, + ._compile_state(), ) q = s.query(Company).options( @@ -1208,153 +1243,54 @@ class OptionsNoPropTestInh(_Polymorphic): class PickleTest(PathTest, QueryTest): def _option_fixture(self, *arg): - return strategy_options._UnboundLoad._from_keys( - strategy_options._UnboundLoad.joinedload, arg, True, {} + return strategy_options._generate_from_keys( + strategy_options.Load.joinedload, arg, True, {} ) def test_modern_opt_getstate(self): User = self.classes.User opt = self._option_fixture(User.addresses) - to_bind = list(opt._to_bind) - eq_( - opt.__getstate__(), - { - "_is_chain_link": False, - "local_opts": {}, - "is_class_strategy": False, - "path": [(User, "addresses", None)], - "propagate_to_loaders": True, - "_of_type": None, - "_to_bind": to_bind, - "_extra_criteria": (), - }, - ) - def test_modern_opt_setstate(self): - User = self.classes.User + q1 = fixture_session().query(User).options(opt) + c1 = q1._compile_context() - inner_opt = strategy_options._UnboundLoad.__new__( - strategy_options._UnboundLoad - ) - inner_state = { - "_is_chain_link": False, - "local_opts": {}, - "is_class_strategy": False, - "path": [(User, "addresses", None)], - "propagate_to_loaders": True, - "_to_bind": None, - "strategy": (("lazy", "joined"),), - } - inner_opt.__setstate__(inner_state) - - opt = strategy_options._UnboundLoad.__new__( - strategy_options._UnboundLoad - ) - state = { - "_is_chain_link": False, - "local_opts": {}, - "is_class_strategy": False, - "path": [(User, "addresses", None)], - "propagate_to_loaders": True, - "_to_bind": [inner_opt], - } + state = opt.__getstate__() - opt.__setstate__(state) + opt2 = Load.__new__(Load) + opt2.__setstate__(state) - query = fixture_session().query(User) - attr = {} - load = opt._bind_loader( - [ - ent.entity_zero - for ent in query._compile_state()._lead_mapper_entities - ], - query._compile_options._current_path, - attr, - False, - ) + eq_(opt.__dict__, opt2.__dict__) - eq_( - load.path, - inspect(User)._path_registry[User.addresses.property][ - inspect(self.classes.Address) - ], - ) + q2 = fixture_session().query(User).options(opt2) + c2 = q2._compile_context() - def test_legacy_opt_setstate(self): - User = self.classes.User - - opt = strategy_options._UnboundLoad.__new__( - strategy_options._UnboundLoad - ) - state = { - "_is_chain_link": False, - "local_opts": {}, - "is_class_strategy": False, - "path": [(User, "addresses")], - "propagate_to_loaders": True, - "_to_bind": [opt], - "strategy": (("lazy", "joined"),), - } - - opt.__setstate__(state) - - query = fixture_session().query(User) - attr = {} - load = opt._bind_loader( - [ - ent.entity_zero - for ent in query._compile_state()._lead_mapper_entities - ], - query._compile_options._current_path, - attr, - False, - ) - - eq_( - load.path, - inspect(User)._path_registry[User.addresses.property][ - inspect(self.classes.Address) - ], - ) + eq_(c1.attributes, c2.attributes) class LocalOptsTest(PathTest, QueryTest): @classmethod def setup_test_class(cls): - @strategy_options.loader_option() - def some_col_opt_only(loadopt, key, opts): - return loadopt.set_column_strategy( - (key,), None, opts, opts_only=True - ) + def some_col_opt_only(self, key, opts): + return self._set_column_strategy((key,), None, opts) + + strategy_options._AbstractLoad.some_col_opt_only = some_col_opt_only - @strategy_options.loader_option() def some_col_opt_strategy(loadopt, key, opts): - return loadopt.set_column_strategy( + return loadopt._set_column_strategy( (key,), {"deferred": True, "instrument": True}, opts ) - cls.some_col_opt_only = some_col_opt_only - cls.some_col_opt_strategy = some_col_opt_strategy + strategy_options._AbstractLoad.some_col_opt_strategy = ( + some_col_opt_strategy + ) def _assert_attrs(self, opts, expected): User = self.classes.User - query = fixture_session().query(User) - attr = {} - - for opt in opts: - if isinstance(opt, strategy_options._UnboundLoad): - ctx = query._compile_state() - for tb in opt._to_bind: - tb._bind_loader( - [ent.entity_zero for ent in ctx._lead_mapper_entities], - query._compile_options._current_path, - attr, - False, - ) - else: - attr.update(opt.context) + s = fixture_session() + q1 = s.query(User).options(*opts) + attr = q1._compile_context().attributes key = ( "loader", @@ -1365,23 +1301,11 @@ class LocalOptsTest(PathTest, QueryTest): def test_single_opt_only(self): User = self.classes.User - opt = strategy_options._UnboundLoad().some_col_opt_only( + opt = strategy_options.Load(User).some_col_opt_only( User.name, {"foo": "bar"} ) self._assert_attrs([opt], {"foo": "bar"}) - def test_unbound_multiple_opt_only(self): - User = self.classes.User - opts = [ - strategy_options._UnboundLoad().some_col_opt_only( - User.name, {"foo": "bar"} - ), - strategy_options._UnboundLoad().some_col_opt_only( - User.name, {"bat": "hoho"} - ), - ] - self._assert_attrs(opts, {"foo": "bar", "bat": "hoho"}) - def test_bound_multiple_opt_only(self): User = self.classes.User opts = [ @@ -1400,30 +1324,6 @@ class LocalOptsTest(PathTest, QueryTest): ] self._assert_attrs(opts, {"foo": "bar", "bat": "hoho"}) - def test_unbound_strat_opt_recvs_from_optonly(self): - User = self.classes.User - opts = [ - strategy_options._UnboundLoad().some_col_opt_only( - User.name, {"foo": "bar"} - ), - strategy_options._UnboundLoad().some_col_opt_strategy( - User.name, {"bat": "hoho"} - ), - ] - self._assert_attrs(opts, {"foo": "bar", "bat": "hoho"}) - - def test_unbound_opt_only_adds_to_strat(self): - User = self.classes.User - opts = [ - strategy_options._UnboundLoad().some_col_opt_strategy( - User.name, {"bat": "hoho"} - ), - strategy_options._UnboundLoad().some_col_opt_only( - User.name, {"foo": "bar"} - ), - ] - self._assert_attrs(opts, {"foo": "bar", "bat": "hoho"}) - def test_bound_opt_only_adds_to_strat(self): User = self.classes.User opts = [ @@ -1442,41 +1342,25 @@ class SubOptionsTest(PathTest, QueryTest): def _assert_opts(self, q, sub_opt, non_sub_opts): attr_a = {} - for val in sub_opt._to_bind: - val._bind_loader( - [ - ent.entity_zero - for ent in q._compile_state()._lead_mapper_entities - ], - q._compile_options._current_path, - attr_a, - False, - ) - - attr_b = {} - - for opt in non_sub_opts: - for val in opt._to_bind: - val._bind_loader( - [ - ent.entity_zero - for ent in q._compile_state()._lead_mapper_entities - ], - q._compile_options._current_path, - attr_b, - False, - ) + q1 = q.options(sub_opt)._compile_context() + q2 = q.options(*non_sub_opts)._compile_context() - for k, l in attr_b.items(): - if not l.strategy: - del attr_b[k] + attr_a = { + k: v + for k, v in q1.attributes.items() + if isinstance(k, tuple) and k[0] == "loader" + } + attr_b = { + k: v + for k, v in q2.attributes.items() + if isinstance(k, tuple) and k[0] == "loader" + } def strat_as_tuple(strat): return ( strat.strategy, strat.local_opts, - strat.propagate_to_loaders, - strat._of_type, + getattr(strat, "_of_type", None), strat.is_class_strategy, strat.is_opts_only, ) @@ -1509,6 +1393,7 @@ class SubOptionsTest(PathTest, QueryTest): User, Address, Order, Item, SubItem = self.classes( "User", "Address", "Order", "Item", "SubItem" ) + sub_opt = defaultload(User.orders).options( joinedload(Order.items), defaultload(Order.items).options(subqueryload(Item.keywords)), @@ -1579,46 +1464,23 @@ class SubOptionsTest(PathTest, QueryTest): "User", "Address", "Order", "Item", "SubItem" ) - # these options are "invalid", in that User.orders -> Item.keywords - # is not a path. However, the "normal" option is not generating - # an error for now, which is bad, but we're testing here only that - # it works the same way, so there you go. If and when we make this - # case raise, then both cases should raise in the same way. - sub_opt = joinedload(User.orders).options( - joinedload(Item.keywords), joinedload(Order.items) - ) - non_sub_opts = [ - joinedload(User.orders).joinedload(Item.keywords), - defaultload(User.orders).joinedload(Order.items), - ] - sess = fixture_session() - self._assert_opts(sess.query(User), sub_opt, non_sub_opts) - - def test_not_implemented_fromload(self): - User, Address, Order, Item, SubItem = self.classes( - "User", "Address", "Order", "Item", "SubItem" - ) - - assert_raises_message( - NotImplementedError, - r"The options\(\) method is currently only supported " - "for 'unbound' loader options", - Load(User).joinedload(User.orders).options, - joinedload(Order.items), - ) - - def test_not_implemented_toload(self): - User, Address, Order, Item, SubItem = self.classes( - "User", "Address", "Order", "Item", "SubItem" - ) - - assert_raises_message( - NotImplementedError, - r"Only 'unbound' loader options may be used with the " - r"Load.options\(\) method", - joinedload(User.orders).options, - Load(Order).joinedload(Order.items), - ) + with expect_raises_message( + sa.exc.ArgumentError, + r'ORM mapped attribute "Item.keywords" does not link from ' + r'relationship "User.orders"', + ): + [ + joinedload(User.orders).joinedload(Item.keywords), + defaultload(User.orders).joinedload(Order.items), + ] + with expect_raises_message( + sa.exc.ArgumentError, + r'Attribute "Item.keywords" does not link from ' + r'element "Mapper\[Order\(orders\)\]"', + ): + joinedload(User.orders).options( + joinedload(Item.keywords), joinedload(Order.items) + ) class MapperOptionsTest(_fixtures.FixtureTest): @@ -1962,8 +1824,26 @@ class MapperOptionsTest(_fixtures.FixtureTest): sess = fixture_session() oalias = aliased(Order) + + # this one is *really weird* + # here's what the test originally had. note two different strategies + # for Order.items + # + # opt1 = sa.orm.joinedload(User.orders, Order.items) + # opt2 = sa.orm.contains_eager(User.orders, Order.items, alias=oalias) + + # here's how it would translate. note that the second + # contains_eager() for Order.items just got cancelled out, + # I guess the joinedload() would somehow overrule the contains_eager + # + # opt1 = Load(User).defaultload(User.orders).joinedload(Order.items) + # opt2 = Load(User).contains_eager(User.orders, alias=oalias) + + # setting up the options more specifically works however with + # both the old way and the new way opt1 = sa.orm.joinedload(User.orders, Order.items) - opt2 = sa.orm.contains_eager(User.orders, Order.items, alias=oalias) + opt2 = sa.orm.contains_eager(User.orders, alias=oalias) + u1 = ( sess.query(User) .join(oalias, User.orders) @@ -1973,3 +1853,74 @@ class MapperOptionsTest(_fixtures.FixtureTest): ustate = attributes.instance_state(u1) assert opt1 in ustate.load_options assert opt2 not in ustate.load_options + + @testing.combinations( + ( + lambda User, Order: ( + joinedload(User.orders), + contains_eager(User.orders), + ), + r"Loader strategies for ORM Path\[Mapper\[User\(users\)\] -> " + r"User.orders -> Mapper\[Order\(orders\)\]\] conflict", + ), + ( + lambda User, Order: ( + joinedload(User.orders), + joinedload(User.orders).joinedload(Order.items), + ), + None, + ), + ( + lambda User, Order: ( + joinedload(User.orders), + joinedload(User.orders, innerjoin=True).joinedload( + Order.items + ), + ), + r"Loader strategies for ORM Path\[Mapper\[User\(users\)\] -> " + r"User.orders -> Mapper\[Order\(orders\)\]\] conflict", + ), + ( + lambda User: (defer(User.name), undefer(User.name)), + r"Loader strategies for ORM Path\[Mapper\[User\(users\)\] -> " + r"User.name\] conflict", + ), + ) + def test_conflicts(self, make_opt, errmsg): + """introduce a new error for conflicting options in SQLAlchemy 2.0. + + This case seems to be fairly difficult to come up with randomly + so let's see if we can refuse to guess for this case. + + """ + users, items, order_items, Order, Item, User, orders = ( + self.tables.users, + self.tables.items, + self.tables.order_items, + self.classes.Order, + self.classes.Item, + self.classes.User, + self.tables.orders, + ) + + self.mapper_registry.map_imperatively( + User, users, properties=dict(orders=relationship(Order)) + ) + self.mapper_registry.map_imperatively( + Order, + orders, + properties=dict(items=relationship(Item, secondary=order_items)), + ) + self.mapper_registry.map_imperatively(Item, items) + + sess = fixture_session() + + opt = testing.resolve_lambda( + make_opt, User=User, Order=Order, Item=Item + ) + + if errmsg: + with expect_raises_message(sa.exc.InvalidRequestError, errmsg): + sess.query(User).options(opt)._compile_context() + else: + sess.query(User).options(opt)._compile_context() diff --git a/test/orm/test_pickled.py b/test/orm/test_pickled.py index 4bccb5ee71..8e4c6ab17a 100644 --- a/test/orm/test_pickled.py +++ b/test/orm/test_pickled.py @@ -533,30 +533,8 @@ class PickleTest(fixtures.MappedTest): opt2 = pickle.loads(pickle.dumps(opt)) eq_(opt.path, opt2.path) - eq_(opt.context.keys(), opt2.context.keys()) - eq_(opt.local_opts, opt2.local_opts) - - u1 = sess.query(User).options(opt).first() - pickle.loads(pickle.dumps(u1)) - - def test_became_bound_options(self): - sess, User, Address, Dingaling = self._option_test_fixture() - - for opt in [ - sa.orm.joinedload(User.addresses), - sa.orm.defer(User.name), - sa.orm.joinedload(User.addresses).joinedload(Address.dingaling), - ]: - context = sess.query(User).options(opt)._compile_context() - opt = [ - v - for v in context.attributes.values() - if isinstance(v, sa.orm.Load) - ][0] - - opt2 = pickle.loads(pickle.dumps(opt)) - eq_(opt.path, opt2.path) - eq_(opt.local_opts, opt2.local_opts) + for v1, v2 in zip(opt.context, opt2.context): + eq_(v1.local_opts, v2.local_opts) u1 = sess.query(User).options(opt).first() pickle.loads(pickle.dumps(u1)) @@ -694,19 +672,25 @@ class OptionsTest(_Polymorphic): def test_options_of_type(self): with_poly = with_polymorphic(Person, [Engineer, Manager], flat=True) - for opt, serialized in [ + for opt, serialized_path, serialized_of_type in [ ( sa.orm.joinedload(Company.employees.of_type(Engineer)), - [(Company, "employees", Engineer)], + [(Company, "employees"), (Engineer, None)], + Engineer, ), ( sa.orm.joinedload(Company.employees.of_type(with_poly)), - [(Company, "employees", None)], + [(Company, "employees"), (Person, None)], + None, ), ]: opt2 = pickle.loads(pickle.dumps(opt)) - eq_(opt.__getstate__()["path"], serialized) - eq_(opt2.__getstate__()["path"], serialized) + eq_(opt.__getstate__()["path"], serialized_path) + eq_(opt2.__getstate__()["path"], serialized_path) + + for v1, v2 in zip(opt.context, opt2.context): + eq_(v1.__getstate__()["_of_type"], serialized_of_type) + eq_(v2.__getstate__()["_of_type"], serialized_of_type) def test_load(self): s = fixture_session() diff --git a/test/orm/test_relationships.py b/test/orm/test_relationships.py index 94e74992fb..9ceaf4b6c8 100644 --- a/test/orm/test_relationships.py +++ b/test/orm/test_relationships.py @@ -38,6 +38,7 @@ from sqlalchemy.testing import eq_ from sqlalchemy.testing import fixtures from sqlalchemy.testing import in_ from sqlalchemy.testing import is_ +from sqlalchemy.testing.assertions import expect_raises_message from sqlalchemy.testing.assertions import expect_warnings from sqlalchemy.testing.assertsql import assert_engine from sqlalchemy.testing.assertsql import CompiledSQL @@ -2377,7 +2378,7 @@ class ManualBackrefTest(_fixtures.FixtureTest): r"User.addresses references " r"relationship Address.dingaling, " r"which does not " - r"reference mapper mapped class User->users", + r"reference mapper Mapper\[User\(users\)\]", configure_mappers, ) @@ -6208,6 +6209,42 @@ class RaiseLoadTest(_fixtures.FixtureTest): lambda: a1.user, ) + def test_raiseload_from_eager_load(self): + Address, addresses, users, User = ( + self.classes.Address, + self.tables.addresses, + self.tables.users, + self.classes.User, + ) + Dingaling, dingalings = self.classes.Dingaling, self.tables.dingalings + self.mapper_registry.map_imperatively(Dingaling, dingalings) + + self.mapper_registry.map_imperatively( + Address, + addresses, + properties=dict(dingaling=relationship(Dingaling)), + ) + + self.mapper_registry.map_imperatively( + User, + users, + properties=dict(addresses=relationship(Address)), + ) + + q = ( + fixture_session() + .query(User) + .options(joinedload(User.addresses).raiseload("*")) + .filter_by(id=7) + ) + u1 = q.first() + assert "addresses" in u1.__dict__ + with expect_raises_message( + sa.exc.InvalidRequestError, + "'Address.dingaling' is not available due to lazy='raise'", + ): + u1.addresses[0].dingaling + def test_raiseload_wildcard_all_classes_option(self): Address, addresses, users, User = ( self.classes.Address, diff --git a/test/orm/test_selectin_relations.py b/test/orm/test_selectin_relations.py index 7a5bb0e7ed..1daf7b0538 100644 --- a/test/orm/test_selectin_relations.py +++ b/test/orm/test_selectin_relations.py @@ -3002,17 +3002,22 @@ class SelfRefInheritanceAliasedTest( def test_twolevel_selectin_w_polymorphic(self): Foo, Bar = self.classes("Foo", "Bar") - for count in range(3): + for count in range(1): r = with_polymorphic(Foo, "*", aliased=True) attr1 = Foo.foo.of_type(r) attr2 = r.foo s = fixture_session() - q = ( - s.query(Foo) - .filter(Foo.id == 2) - .options(selectinload(attr1).selectinload(attr2)) - ) + + from sqlalchemy.orm import Load + + opt1 = selectinload(attr1).selectinload(attr2) # noqa + opt2 = Load(Foo).selectinload(attr1).selectinload(attr2) # noqa + + q = s.query(Foo).filter(Foo.id == 2).options(opt2) + # q.all() + # return + results = self.assert_sql_execution( testing.db, q.all, diff --git a/test/orm/test_sync.py b/test/orm/test_sync.py index 796ad84f98..c8f511f447 100644 --- a/test/orm/test_sync.py +++ b/test/orm/test_sync.py @@ -101,7 +101,7 @@ class SyncTest( assert_raises_message( orm_exc.UnmappedColumnError, "Can't execute sync rule for source column 't2.id'; " - r"mapper 'mapped class A->t1' does not map this column.", + r"mapper 'Mapper\[A\(t1\)\]' does not map this column.", sync.populate, a1, a_mapper, @@ -119,7 +119,7 @@ class SyncTest( orm_exc.UnmappedColumnError, r"Can't execute sync rule for destination " r"column 't1.id'; " - r"mapper 'mapped class B->t2' does not map this column.", + r"mapper 'Mapper\[B\(t2\)\]' does not map this column.", sync.populate, a1, a_mapper, @@ -159,7 +159,7 @@ class SyncTest( assert_raises_message( orm_exc.UnmappedColumnError, "Can't execute sync rule for destination " - r"column 't1.foo'; mapper 'mapped class B->t2' does not " + r"column 't1.foo'; mapper 'Mapper\[B\(t2\)\]' does not " "map this column.", sync.clear, b1, @@ -184,7 +184,7 @@ class SyncTest( assert_raises_message( orm_exc.UnmappedColumnError, "Can't execute sync rule for source column 't2.id'; " - r"mapper 'mapped class A->t1' does not map this column.", + r"mapper 'Mapper\[A\(t1\)\]' does not map this column.", sync.update, a1, a_mapper, @@ -209,7 +209,7 @@ class SyncTest( assert_raises_message( orm_exc.UnmappedColumnError, "Can't execute sync rule for source column 't2.id'; " - r"mapper 'mapped class A->t1' does not map this column.", + r"mapper 'Mapper\[A\(t1\)\]' does not map this column.", sync.populate_dict, a1, a_mapper, @@ -262,7 +262,7 @@ class SyncTest( assert_raises_message( orm_exc.UnmappedColumnError, "Can't execute sync rule for source column 't2.id'; " - r"mapper 'mapped class A->t1' does not map this column.", + r"mapper 'Mapper\[A\(t1\)\]' does not map this column.", sync.source_modified, uowcommit, a1, diff --git a/test/orm/test_utils.py b/test/orm/test_utils.py index 122524cc05..03c31dc0ff 100644 --- a/test/orm/test_utils.py +++ b/test/orm/test_utils.py @@ -5,7 +5,6 @@ from sqlalchemy import MetaData from sqlalchemy import select from sqlalchemy import Table from sqlalchemy import testing -from sqlalchemy import util from sqlalchemy.ext.hybrid import hybrid_method from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.orm import aliased @@ -15,13 +14,16 @@ from sqlalchemy.orm import synonym from sqlalchemy.orm import util as orm_util from sqlalchemy.orm import with_polymorphic from sqlalchemy.orm.path_registry import PathRegistry +from sqlalchemy.orm.path_registry import PathToken from sqlalchemy.orm.path_registry import RootRegistry from sqlalchemy.testing import assert_raises from sqlalchemy.testing import AssertsCompiledSQL from sqlalchemy.testing import eq_ +from sqlalchemy.testing import expect_raises from sqlalchemy.testing import expect_warnings from sqlalchemy.testing import fixtures from sqlalchemy.testing import is_ +from sqlalchemy.testing.assertions import is_true from sqlalchemy.testing.fixtures import fixture_session from test.orm import _fixtures from .inheritance import _poly_fixtures @@ -202,7 +204,7 @@ class AliasedClassTest(fixtures.MappedTest, AssertsCompiledSQL): alias = aliased(Point) eq_(str(Point.double_x), "Point.double_x") - eq_(str(alias.double_x), "AliasedClass_Point.double_x") + eq_(str(alias.double_x), "aliased(Point).double_x") eq_(str(Point.double_x.__clause_element__()), "point.x * :x_1") eq_(str(alias.double_x.__clause_element__()), "point_1.x * :x_1") @@ -228,7 +230,7 @@ class AliasedClassTest(fixtures.MappedTest, AssertsCompiledSQL): alias = aliased(Point) eq_(str(Point.x_alone), "Point.x_alone") - eq_(str(alias.x_alone), "AliasedClass_Point.x_alone") + eq_(str(alias.x_alone), "aliased(Point).x_alone") # from __clause_element__() perspective, Point.x_alone # and Point.x return the same thing, so that's good @@ -296,7 +298,7 @@ class AliasedClassTest(fixtures.MappedTest, AssertsCompiledSQL): alias = aliased(Point) eq_(str(Point.x_syn), "Point.x_syn") - eq_(str(alias.x_syn), "AliasedClass_Point.x_syn") + eq_(str(alias.x_syn), "aliased(Point).x_syn") sess = fixture_session() self.assert_compile( @@ -319,7 +321,7 @@ class AliasedClassTest(fixtures.MappedTest, AssertsCompiledSQL): alias = aliased(Point) eq_(str(Point.x_syn), "Point.x") - eq_(str(alias.x_syn), "AliasedClass_Point.x") + eq_(str(alias.x_syn), "aliased(Point).x") # from __clause_element__() perspective, Point.x_syn # and Point.x return the same thing, so that's good @@ -362,7 +364,7 @@ class AliasedClassTest(fixtures.MappedTest, AssertsCompiledSQL): alias = aliased(Point) eq_(str(Point.double_x), "Point._impl_double_x") - eq_(str(alias.double_x), "AliasedClass_Point._impl_double_x") + eq_(str(alias.double_x), "aliased(Point)._impl_double_x") eq_(str(Point.double_x.__clause_element__()), "point.x * :x_1") eq_(str(alias.double_x.__clause_element__()), "point_1.x * :x_1") @@ -586,6 +588,38 @@ class PathRegistryTest(_fixtures.FixtureTest): is_(path[0], umapper) is_(path[2], amapper) + def test_indexed_key_token(self): + umapper = inspect(self.classes.User) + amapper = inspect(self.classes.Address) + path = PathRegistry.coerce( + ( + umapper, + umapper.attrs.addresses, + amapper, + PathToken.intern(":*"), + ) + ) + is_true(path.is_token) + eq_(path[1], umapper.attrs.addresses) + eq_(path[3], ":*") + + with expect_raises(IndexError): + path[amapper] + + def test_slice_token(self): + umapper = inspect(self.classes.User) + amapper = inspect(self.classes.Address) + path = PathRegistry.coerce( + ( + umapper, + umapper.attrs.addresses, + amapper, + PathToken.intern(":*"), + ) + ) + is_true(path.is_token) + eq_(path[1:3], (umapper.attrs.addresses, amapper)) + def test_indexed_key(self): umapper = inspect(self.classes.User) amapper = inspect(self.classes.Address) @@ -840,62 +874,6 @@ class PathRegistryTest(_fixtures.FixtureTest): eq_(p2.serialize(), [(User, "addresses"), (Address, None)]) eq_(p3.serialize(), [(User, "addresses")]) - def test_serialize_context_dict(self): - reg = util.OrderedDict() - umapper = inspect(self.classes.User) - amapper = inspect(self.classes.Address) - - p1 = PathRegistry.coerce((umapper, umapper.attrs.addresses)) - p2 = PathRegistry.coerce((umapper, umapper.attrs.addresses, amapper)) - p3 = PathRegistry.coerce((amapper, amapper.attrs.email_address)) - - p1.set(reg, "p1key", "p1value") - p2.set(reg, "p2key", "p2value") - p3.set(reg, "p3key", "p3value") - eq_( - reg, - { - ("p1key", p1.path): "p1value", - ("p2key", p2.path): "p2value", - ("p3key", p3.path): "p3value", - }, - ) - - serialized = PathRegistry.serialize_context_dict( - reg, ("p1key", "p2key") - ) - eq_( - serialized, - [ - (("p1key", p1.serialize()), "p1value"), - (("p2key", p2.serialize()), "p2value"), - ], - ) - - def test_deseralize_context_dict(self): - umapper = inspect(self.classes.User) - amapper = inspect(self.classes.Address) - - p1 = PathRegistry.coerce((umapper, umapper.attrs.addresses)) - p2 = PathRegistry.coerce((umapper, umapper.attrs.addresses, amapper)) - p3 = PathRegistry.coerce((amapper, amapper.attrs.email_address)) - - serialized = [ - (("p1key", p1.serialize()), "p1value"), - (("p2key", p2.serialize()), "p2value"), - (("p3key", p3.serialize()), "p3value"), - ] - deserialized = PathRegistry.deserialize_context_dict(serialized) - - eq_( - deserialized, - { - ("p1key", p1.path): "p1value", - ("p2key", p2.path): "p2value", - ("p3key", p3.path): "p3value", - }, - ) - def test_deseralize(self): User = self.classes.User Address = self.classes.Address diff --git a/test/profiles.txt b/test/profiles.txt index 50907e9844..eaf08cfab4 100644 --- a/test/profiles.txt +++ b/test/profiles.txt @@ -1,15 +1,15 @@ # /home/classic/dev/sqlalchemy/test/profiles.txt # This file is written out on a per-environment basis. -# For each test in aaa_profiling, the corresponding function and +# For each test in aaa_profiling, the corresponding function and # environment is located within this file. If it doesn't exist, # the test is skipped. -# If a callcount does exist, it is compared to what we received. +# If a callcount does exist, it is compared to what we received. # assertions are raised if the counts do not match. -# -# To add a new callcount test, apply the function_call_count -# decorator and re-run the tests using the --write-profiles +# +# To add a new callcount test, apply the function_call_count +# decorator and re-run the tests using the --write-profiles # option - this file will be rewritten including the new count. -# +# # TEST: test.aaa_profiling.test_compiler.CompileTest.test_insert @@ -153,13 +153,13 @@ test.aaa_profiling.test_orm.AttributeOverheadTest.test_collection_append_remove # TEST: test.aaa_profiling.test_orm.BranchedOptionTest.test_query_opts_key_bound_branching -test.aaa_profiling.test_orm.BranchedOptionTest.test_query_opts_key_bound_branching x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_cextensions 73 -test.aaa_profiling.test_orm.BranchedOptionTest.test_query_opts_key_bound_branching x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_nocextensions 73 +test.aaa_profiling.test_orm.BranchedOptionTest.test_query_opts_key_bound_branching x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_cextensions 124 +test.aaa_profiling.test_orm.BranchedOptionTest.test_query_opts_key_bound_branching x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_nocextensions 124 # TEST: test.aaa_profiling.test_orm.BranchedOptionTest.test_query_opts_unbound_branching -test.aaa_profiling.test_orm.BranchedOptionTest.test_query_opts_unbound_branching x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_cextensions 388 -test.aaa_profiling.test_orm.BranchedOptionTest.test_query_opts_unbound_branching x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_nocextensions 388 +test.aaa_profiling.test_orm.BranchedOptionTest.test_query_opts_unbound_branching x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_cextensions 124 +test.aaa_profiling.test_orm.BranchedOptionTest.test_query_opts_unbound_branching x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_nocextensions 124 # TEST: test.aaa_profiling.test_orm.DeferOptionsTest.test_baseline @@ -238,8 +238,9 @@ test.aaa_profiling.test_orm.QueryTest.test_query_cols x86_64_linux_cpython_3.10_ # TEST: test.aaa_profiling.test_orm.SelectInEagerLoadTest.test_round_trip_results -test.aaa_profiling.test_orm.SelectInEagerLoadTest.test_round_trip_results x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_cextensions 262605 -test.aaa_profiling.test_orm.SelectInEagerLoadTest.test_round_trip_results x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_nocextensions 281605 +# these should keep getting better with some more changes I have in store +test.aaa_profiling.test_orm.SelectInEagerLoadTest.test_round_trip_results x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_cextensions 249505 +test.aaa_profiling.test_orm.SelectInEagerLoadTest.test_round_trip_results x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_nocextensions 269205 # TEST: test.aaa_profiling.test_orm.SessionTest.test_expire_lots