]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
factor out UnboundLoad and rearchitect strategy_options.py
authorMike Bayer <mike_mp@zzzcomputing.com>
Thu, 9 Dec 2021 01:27:16 +0000 (20:27 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 27 Dec 2021 17:30:38 +0000 (12:30 -0500)
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

42 files changed:
doc/build/changelog/unreleased_20/6986.rst [new file with mode: 0644]
doc/build/orm/loading_relationships.rst
lib/sqlalchemy/ext/baked.py
lib/sqlalchemy/orm/__init__.py
lib/sqlalchemy/orm/context.py
lib/sqlalchemy/orm/interfaces.py
lib/sqlalchemy/orm/mapper.py
lib/sqlalchemy/orm/path_registry.py
lib/sqlalchemy/orm/properties.py
lib/sqlalchemy/orm/relationships.py
lib/sqlalchemy/orm/strategies.py
lib/sqlalchemy/orm/strategy_options.py
lib/sqlalchemy/orm/util.py
lib/sqlalchemy/sql/base.py
lib/sqlalchemy/testing/plugin/pytestplugin.py
lib/sqlalchemy/util/compat.py
lib/sqlalchemy/util/langhelpers.py
test/base/test_utils.py
test/orm/declarative/test_basic.py
test/orm/inheritance/test_assorted_poly.py
test/orm/inheritance/test_basic.py
test/orm/inheritance/test_poly_loading.py
test/orm/inheritance/test_relationship.py
test/orm/inheritance/test_single.py
test/orm/test_ac_relationships.py
test/orm/test_bind.py
test/orm/test_cache_key.py
test/orm/test_core_compilation.py
test/orm/test_default_strategies.py
test/orm/test_deferred.py
test/orm/test_deprecations.py
test/orm/test_eager_relations.py
test/orm/test_expire.py
test/orm/test_joins.py
test/orm/test_mapper.py
test/orm/test_options.py
test/orm/test_pickled.py
test/orm/test_relationships.py
test/orm/test_selectin_relations.py
test/orm/test_sync.py
test/orm/test_utils.py
test/profiles.txt

diff --git a/doc/build/changelog/unreleased_20/6986.rst b/doc/build/changelog/unreleased_20/6986.rst
new file mode 100644 (file)
index 0000000..217a05c
--- /dev/null
@@ -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.
index 5a1d5151d4211528c9457fe5e5e3febb6c8209a9..2b93bc84af872c7e3a0e3c96b426e94e91811367 100644 (file)
@@ -1269,8 +1269,9 @@ Relationship Loader API
 
 .. autofunction:: lazyload
 
-.. autoclass:: Load
+.. autoclass:: sqlalchemy.orm.Load
     :members:
+    :inherited-members: Generative
 
 .. autofunction:: noload
 
index e91277311ae04535c71e1275dd848875049fd999..a2db9dbec5188b558ab6f8f848f72b682987dfa8 100644 (file)
@@ -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
index c49ff4ec64adc9487b1a5f235c81e05154a3c0f5..69a3e64da6c3d5f26ce50a84ba83e0f72610a1bf 100644 (file)
@@ -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()`."""
index e834b22e8518259f9f557f8b862aa3adf1623f83..f438b0b3ad685dd2c100deb730524d2e52086a4b 100644 (file)
@@ -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
 
index 2c6818a93924a1533347755923585cc86ae3edc7..92ecbdd2dfad9bfea8fecc0bf15d653db8f5e315 100644 (file)
@@ -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.
index 40a76181c067a3039e495779a593508375052e8b..a94c6bfa72b5bed1f8642583167acd205e46b966 100644 (file)
@@ -966,7 +966,7 @@ class Mapper(
         return self.persist_selectable
 
     @util.memoized_property
-    def _path_registry(self):
+    def _path_registry(self) -> PathRegistry:
         return PathRegistry.per_mapper(self)
 
     def _configure_inheritance(self):
@@ -1985,7 +1985,7 @@ class Mapper(
         return "<Mapper at 0x%x; %s>" % (id(self), self.class_.__name__)
 
     def __str__(self):
-        return "mapped class %s%s->%s" % (
+        return "Mapper[%s%s(%s)]" % (
             self.class_.__name__,
             self.non_primary and " (non-primary)" or "",
             self.local_table.description
@@ -3113,21 +3113,23 @@ class Mapper(
             if prop.parent is self or prop in keep_props:
                 # "enable" options, to turn on the properties that we want to
                 # load by default (subject to options from the query)
-                enable_opt.set_generic_strategy(
+                enable_opt = enable_opt._set_generic_strategy(
                     # convert string name to an attribute before passing
                     # to loader strategy
                     (getattr(entity.entity_namespace, prop.key),),
                     dict(prop.strategy_key),
+                    _reconcile_to_other=True,
                 )
             else:
                 # "disable" options, to turn off the properties from the
                 # superclass that we *don't* want to load, applied after
                 # the options from the query to override them
-                disable_opt.set_generic_strategy(
+                disable_opt = disable_opt._set_generic_strategy(
                     # convert string name to an attribute before passing
                     # to loader strategy
                     (getattr(entity.entity_namespace, prop.key),),
                     {"do_nothing": True},
+                    _reconcile_to_other=False,
                 )
 
         primary_key = [
index 0aa9de817b47c731c0cbc244356924adc02347e5..f2768a6b664698d7492d07032331ec22f88f0aa7 100644 (file)
@@ -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)
 
index de46f84a75e9b449f924871b0369663d0f72fa83..04b7f89c2b73cbfb80210ed5f5908cc70dc639d8 100644 (file)
@@ -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
 
index b4aef874f1039f71ee1401be971adad566fdd4e2..4293cf656c6c2dab17ae8b4d1c75a4a006970a36 100644 (file)
@@ -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
index d5f5f8527d34879f1e0132e4ab758650d7708c6a..844b0f007ac603a6ebcfa482e2dfa482d588a863 100644 (file)
@@ -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,
index a5c539773935e1c91b45471a87021f52b9aa37d3..02068adce89877b350be9bc03680b14b49214279 100644 (file)
@@ -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)
index fef65f73c2e4f9159835ece49d8835433193e481..58ad3ab5f9559b021b2fdc4b99caa8e0815bac47 100644 (file)
@@ -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
index 4165751ca1f0439f7f648aa42053156eb112ee8a..65afc57dddc6147a560ca8b03fcfc9906f70bca6 100644 (file)
@@ -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.
 
index 7caa50438c1f3c0aa36689a2200747691da04075..02cb0ac3283b204a2e90a9764fe3f47fc6af297d 100644 (file)
@@ -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
index dfa5fa825a9f50b352006ee1f4903a7c6da6f7cd..37f15769869bf249acee109e3b06f08e4f88a8a8 100644 (file)
@@ -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)
 
index b759490c57747531369cd0ff833eb880631f887f..780af2bfe4a56175825149bf3e771f199d9231ae 100644 (file)
@@ -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)
 
 
index 836778bc998a2a8b9fd9a311ca31483db7078634..9278bd268882abab2ec72eeb408ffc8bee0c8f6a 100644 (file)
@@ -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",
index 902caca6fbf496863364eb04df606ac50461b846..a5c1ba08ab3e1bee206fec30181f83e5ce61c2fa 100644 (file)
@@ -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,
             )
index f51fb17a40d7f25dae1353c51425d45c0421b760..c11dd1bf7192f91c5302fba913c954c33f5e8b93 100644 (file)
@@ -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"),
index 869cdc88991ea0c5dd64fda3d761b8906c18d86b..5fc69bf9eeed67fbb40997572b2bb30d292b854c 100644 (file)
@@ -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,
         )
 
index 0a03388314793ddf894b69a51c6f896c1e3fae8c..4fe13887e1c905dc3b57e57301f8f1de69c10687 100644 (file)
@@ -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(
index 6d071a1e22195827b6352ac09b2f861e69fbf89b..fa66f90b8266a502de475899323c235b90b961b0 100644 (file)
@@ -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,
index 93ec6430455ba31c9194aadcaf936636ae6a9a91..1b3c6db74da2f9b1ef0f484677db41b309eb3433 100644 (file)
@@ -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,
index f59d704f3f218cce06353f1358dbaa3cb75c8513..702b3e15f6086617995ad2245b204be20337050a 100644 (file)
@@ -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()
 
 
index 11e34645dc1a57ea387109d36f2d1987b817ab25..1d5af50643271dd3b94a12214f3bf63e81dac197 100644 (file)
@@ -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,
         )
index e3ba870f2d825a2e9ebddacffa84548fb48d725c..7beadc08cf99ba15ee345758c6fdb842b233a457 100644 (file)
@@ -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))
index 4bf196fa0d0e0e2ae9fe56d96b93d146416a34d0..000a96a422a5d0fa24ca1120df219ead5865e0da 100644 (file)
@@ -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,
         )
 
index 9162d63ecda31ea725132aabd70fafbc0ce8952e..8b3cd84193b17c7564d2e9808171ad68dde14f9f 100644 (file)
@@ -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
index 7afaad1e9dc9da0b9968b200fe1d7704973966e5..0d7ccde90be3d3d94aa5ee1c3aa786f527e7daa5 100644 (file)
@@ -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(
index 535196c25329a08b68a419c985829dd56271c3d9..bae2a9707aab2f93c3139fff4f572119ca4ab477 100644 (file)
@@ -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
index 6d6d10deafdc3841d165cf71685d97ab11cb81db..42b3bd1ea5bea023f8bb054ac241b1ef9fb9b007 100644 (file)
@@ -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)
             )
         )
index a5abcb355435ba8c511564755274aaf2ca3b90ae..3e3488d52681bcf2000c61323deac71c8714f4cb 100644 (file)
@@ -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(
index 351938a512ab05b83563daf166dcbc4f59057e9b..58b09b67d781339edb3a20dd36caeb00133bc2e2 100644 (file)
@@ -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,
         )
 
index e5e819bbbfdc893e2e3206085c58859982847ca8..b491604f30ebb0b323ad2fc1765d4b276e4995bc 100644 (file)
@@ -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,
index 25d5dcc6e5a5983fe08a63e41996382a056d7e1d..7098220801de01e98808dd77be1c50e58dffbd30 100644 (file)
@@ -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()
index 4bccb5ee71594bf2217254ed1ec6d8b6749a021c..8e4c6ab17aff2f1685f6a7e9c5e86d92d6dfc295 100644 (file)
@@ -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()
index 94e74992fbe20e2bc1bbfb28865a260e14d00aa3..9ceaf4b6c8470f089f3e155074a54d065c40d8e2 100644 (file)
@@ -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,
index 7a5bb0e7edbc9921ccb659bcfcac9c02643810bb..1daf7b05384ca6bea225dd87bc9a5636a42dd224 100644 (file)
@@ -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,
index 796ad84f9868410e9d1e1cba18f83d45c475e7eb..c8f511f447a47125f120fae1dda3b2280f2e11f4 100644 (file)
@@ -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,
index 122524cc052b34f4298cd4932d45a7c45f5aa138..03c31dc0ffb2d42ac47747e2782ddffe6e1b62ea 100644 (file)
@@ -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
index 50907e984411c78462163a6174751530e703ce38..eaf08cfab419305e5f73e25e3ae5bc21540b142f 100644 (file)
@@ -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