"""
+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
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
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
# 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
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(
"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(
"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)
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
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
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_(
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"]),
)
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",
)
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",
)
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,
)
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):
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):
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(
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"),
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
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 = (
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):
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):
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, [])
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
opt = self._option_fixture(User.orders, Order.items).joinedload(
Item.keywords
)
+
self._assert_path_result(
opt,
q,
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):
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. ",
)
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".',
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".',
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".',
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):
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
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
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):
# 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):
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):
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(
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",
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 = [
]
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 = [
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,
)
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)),
"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):
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)
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()