From 1dac2263372df2b85db5d029a45721fa158a5c9d Mon Sep 17 00:00:00 2001 From: xiubuzhe Date: Sun, 8 Oct 2023 20:59:00 +0800 Subject: first add files --- lib/sqlalchemy/orm/strategy_options.py | 2008 ++++++++++++++++++++++++++++++++ 1 file changed, 2008 insertions(+) create mode 100644 lib/sqlalchemy/orm/strategy_options.py (limited to 'lib/sqlalchemy/orm/strategy_options.py') diff --git a/lib/sqlalchemy/orm/strategy_options.py b/lib/sqlalchemy/orm/strategy_options.py new file mode 100644 index 0000000..c3dd5df --- /dev/null +++ b/lib/sqlalchemy/orm/strategy_options.py @@ -0,0 +1,2008 @@ +# Copyright (C) 2005-2022 the SQLAlchemy authors and contributors +# +# +# This module is part of SQLAlchemy and is released under +# the MIT License: https://www.opensource.org/licenses/mit-license.php + +""" + +""" + +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 MapperProperty +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 .. import exc as sa_exc +from .. import inspect +from .. import util +from ..sql import and_ +from ..sql import coercions +from ..sql import roles +from ..sql import traversals +from ..sql import visitors +from ..sql.base import _generative +from ..sql.base import Generative + + +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. + + 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:: + + myopt = Load(MyClass).joinedload("widgets") + + The above ``myopt`` can now be used with :meth:`_query.Query.options`, + where it + will only take effect for the ``MyClass`` entity:: + + session.query(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:: + + session.query(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. + + .. seealso:: + + :ref:`deferred_options` + + :ref:`deferred_loading_w_multiple` + + :ref:`relationship_loader_options` + + """ + + _is_strategy_option = True + + _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, + ), + ] + + def __init__(self, entity): + insp = inspect(entity) + insp._post_inspect + + 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 + + @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 _generate_extra_criteria(self, context): + """Apply the current bound parameters in a QueryContext to the + immediate "extra_criteria" stored with this Load object. + + 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. + + """ + + assert ( + self._extra_criteria + ), "this should only be called if _extra_criteria is present" + + orig_query = context.compile_state.select_statement + current_query = context.query + + # 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. + + # if orig_query is current_query: + # not cached yet. just do the and_() + # return and_(*self._extra_criteria) + + k1 = orig_query._generate_cache_key() + k2 = current_query._generate_cache_key() + + return k2._apply_params_to_element(k1, and_(*self._extra_criteria)) + + 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. + + """ + orig_query = context.compile_state.select_statement + + applied = {} + + ck = [None, None] + + def process(opt): + if not opt._extra_criteria: + return + + if ck[0] is None: + ck[:] = ( + orig_query._generate_cache_key(), + context.query._generate_cache_key(), + ) + k1, k2 = ck + + opt._extra_criteria = tuple( + k2._apply_params_to_element(k1, crit) + for crit in opt._extra_criteria + ) + + return self._deep_clone(applied, process) + + def _deep_clone(self, applied, process): + if self in applied: + return applied[self] + + 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 + + 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() + ] + ) + + cloned.local_opts.update(self.local_opts) + + process(cloned) + + return cloned + + @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 + + def _generate(self): + cloned = super(Load, self)._generate() + cloned.local_opts = {} + return cloned + + is_opts_only = False + is_class_strategy = False + strategy = None + propagate_to_loaders = False + _of_type = None + _extra_criteria = () + + def process_compile_state_replaced_entities( + self, compile_state, mapper_entities + ): + if not compile_state.compile_options._enable_eagerloads: + return + + # 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), + ) + + def process_compile_state(self, compile_state): + if not compile_state.compile_options._enable_eagerloads: + return + + 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 _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 _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,) + ) + + if isinstance(attr, util.string_types): + + default_token = attr.endswith(_DEFAULT_TOKEN) + attr_str_name = attr + 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 + + if existing_of_type: + ent = inspect(existing_of_type) + else: + ent = path.entity + + util.warn_deprecated_20( + "Using strings to indicate column or " + "relationship paths in loader options is deprecated " + "and will be removed in SQLAlchemy 2.0. Please use " + "the class-bound attribute directly.", + ) + try: + # use getattr on the class to work around + # synonyms, hybrids, etc. + attr = getattr(ent.class_, attr) + except AttributeError as err: + if raiseerr: + util.raise_( + sa_exc.ArgumentError( + 'Can\'t find property named "%s" on ' + "%s in this Query." % (attr, ent) + ), + replace_context=err, + ) + else: + return None + else: + try: + attr = found_property = attr.property + except AttributeError as ae: + if not isinstance(attr, MapperProperty): + util.raise_( + sa_exc.ArgumentError( + 'Expected attribute "%s" on %s to be a ' + "mapped attribute; " + "instead got %s object." + % (attr_str_name, ent, type(attr)) + ), + replace_context=ae, + ) + else: + raise + + path = path[attr] + else: + insp = inspect(attr) + + 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 + + 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 + + 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 + + if getattr(attr, "_of_type", None): + ac = attr._of_type + ext_info = of_type_info = inspect(ac) + + if polymorphic_entity_context is None: + polymorphic_entity_context = self.context + + existing = path.entity_path[prop].get( + polymorphic_entity_context, "path_with_polymorphic" + ) + + 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, + ) + + ext_info = inspect(ac) + + path.entity_path[prop].set( + polymorphic_entity_context, "path_with_polymorphic", ac + ) + + path = path[prop][ext_info] + + self._of_type = of_type_info + + else: + path = path[prop] + + 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 + + def __str__(self): + return "Load(strategy=%r)" % (self.strategy,) + + def _coerce_strat(self, strategy): + if strategy is not None: + strategy = tuple(sorted(strategy.items())) + return strategy + + def _apply_to_parent(self, parent, applied, bound): + raise NotImplementedError( + "Only 'unbound' loader options may be used with the " + "Load.options() method" + ) + + @_generative + def options(self, *opts): + r"""Apply a series of options as sub-options to this + :class:`_orm.Load` + object. + + E.g.:: + + query = session.query(Author) + query = query.options( + joinedload(Author.book).options( + load_only(Book.summary, Book.excerpt), + joinedload(Book.citations).options( + joinedload(Citation.author) + ) + ) + ) + + :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. + + .. versionadded:: 1.3.6 + + .. seealso:: + + :func:`.defaultload` + + :ref:`relationship_loader_options` + + :ref:`deferred_loading_w_multiple` + + """ + apply_cache = {} + bound = not isinstance(self, _UnboundLoad) + if bound: + raise NotImplementedError( + "The options() method is currently only supported " + "for 'unbound' loader options" + ) + for opt in opts: + opt._apply_to_parent(self, apply_cache, bound) + + @_generative + def set_relationship_strategy( + self, attr, strategy, propagate_to_loaders=True + ): + strategy = self._coerce_strat(strategy) + self.propagate_to_loaders = propagate_to_loaders + cloned = self._clone_for_bind_strategy(attr, strategy, "relationship") + self.path = cloned.path + 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 + + @_generative + def set_column_strategy(self, attrs, strategy, opts=None, opts_only=False): + 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 + + @_generative + def set_generic_strategy(self, attrs, strategy): + strategy = self._coerce_strat(strategy) + for attr in attrs: + cloned = self._clone_for_bind_strategy(attr, strategy, None) + cloned.propagate_to_loaders = True + + @_generative + def set_class_strategy(self, strategy, opts): + 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. + + """ + cloned = self._generate() + cloned._generate_path(self.path, attr, strategy, wildcard_key) + cloned.strategy = strategy + + cloned.local_opts = self.local_opts + if opts: + cloned.local_opts.update(opts) + if opts_only: + cloned.is_opts_only = True + + if strategy or cloned.is_opts_only: + cloned._set_path_strategy() + return cloned + + 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 + + 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, + ) + + # remove cycles; _set_path_strategy is always invoked on an + # anonymous clone of the Load / UnboundLoad object since #5056 + self.context = None + + def __getstate__(self): + d = self.__dict__.copy() + + # can't pickle this right now; warning is raised by strategies + d["_extra_criteria"] = () + + if d["context"] is not None: + d["context"] = PathRegistry.serialize_context_dict( + d["context"], ("loader",) + ) + d["path"] = self.path.serialize() + return d + + 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 _chop_path(self, to_chop, path): + i = -1 + + for i, (c_token, p_token) in enumerate(zip(to_chop, path.path)): + if isinstance(c_token, util.string_types): + # TODO: this is approximated from the _UnboundLoad + # version and probably has issues, not fully covered. + + if i == 0 and c_token.endswith(":" + _DEFAULT_TOKEN): + return to_chop + elif ( + c_token != "relationship:%s" % (_WILDCARD_TOKEN,) + and c_token != p_token.key + ): + return None + + if c_token is p_token: + continue + elif ( + isinstance(c_token, InspectionAttr) + and c_token.is_mapper + and p_token.is_mapper + and c_token.isa(p_token) + ): + continue + else: + return None + return to_chop[i + 1 :] + + +class _UnboundLoad(Load): + """Represent a loader option that isn't tied to a root entity. + + The loader option will produce an entity-linked :class:`_orm.Load` + object when it is passed :meth:`_query.Query.options`. + + This provides compatibility with the traditional system + of freestanding options, e.g. ``joinedload('x.y.z')``. + + """ + + def __init__(self): + self.path = () + self._to_bind = [] + self.local_opts = {} + self._extra_criteria = () + + def _gen_cache_key(self, anon_map, bindparams, _unbound_option_seen=None): + """Inlined gen_cache_key + + Original traversal is:: + + + _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, + ), + ] + + The inlining is so that the "_to_bind" list can be flattened to not + repeat the same UnboundLoad options over and over again. + + See #6869 + + """ + + idself = id(self) + cls = self.__class__ + + if idself in anon_map: + return (anon_map[idself], cls) + else: + id_ = anon_map[idself] + + vis = traversals._cache_key_traversal_visitor + + seen = _unbound_option_seen + if seen is None: + seen = set() + + 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 () + ) + ) + + _is_chain_link = False + + def _set_path_strategy(self): + self._to_bind.append(self) + + # remove cycles; _set_path_strategy is always invoked on an + # anonymous clone of the Load / UnboundLoad object since #5056 + self._to_bind = None + + def _deep_clone(self, applied, process): + if self in applied: + return applied[self] + + 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 () + ] + + cloned.local_opts.update(self.local_opts) + + process(cloned) + + return cloned + + def _apply_to_parent(self, parent, applied, bound, to_bind=None): + if self in applied: + return applied[self] + + if to_bind is None: + to_bind = self._to_bind + + cloned = self._generate() + + applied[self] = cloned + + cloned.strategy = self.strategy + if self.path: + attr = self.path[-1] + if isinstance(attr, util.string_types) and attr.endswith( + _DEFAULT_TOKEN + ): + attr = attr.split(":")[0] + ":" + _WILDCARD_TOKEN + cloned._generate_path( + parent.path + self.path[0:-1], attr, self.strategy, None + ) + + # 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 + + uniq = set() + + cloned._to_bind = parent._to_bind + + 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) + ] + + cloned.local_opts.update(self.local_opts) + + return cloned + + def _generate_path(self, path, attr, for_strategy, wildcard_key): + if ( + wildcard_key + and isinstance(attr, util.string_types) + 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", ()) + + return path + + 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) + 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 + + 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, util.string_types): + # 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,) + + all_tokens = [token for key in keys for token in _split_key(key)] + + 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) + + opt = meth(opt, all_tokens[-1], **kw) + opt._is_chain_link = False + return opt + + 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, util.string_types): + 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 + + def _bind_loader(self, entities, current_path, context, raiseerr): + """Convert from an _UnboundLoad() object into a Load() object. + + 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. + + Example:: + + + query = session.query(User).options( + joinedload("orders").joinedload("items")) + + 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"}), + ] + ) + + After this method, we get something more like this (again this is + not exact API):: + + Load( + User, + (User, User.orders.property)) + Load( + User, + (User, User.orders.property, Order, Order.items.property)) + + """ + + start_path = self.path + + if self.is_class_strategy and current_path: + start_path += (entities[0],) + + # _current_path implies we're in a + # secondary load with an existing path + + if current_path: + start_path = self._chop_path(start_path, current_path) + + if not start_path: + return None + + # 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, util.string_types): + 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" + ) + + if not entity: + return + + path_element = entity + + # transfer our entity-less state into a Load() object + # 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) + + 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, + ) + + return loader + + 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 + + def _find_entity_basestring(self, entities, token, raiseerr): + if token.endswith(":" + _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), + ", ".join( + "Load(%s).some_option('*')" % ent + for ent in entities + ), + ) + ) + elif token.endswith(_DEFAULT_TOKEN): + raiseerr = False + + for ent in entities: + # return only the first _MapperEntity when searching + # based on string prop name. Ideally object + # attributes are used to specify more exactly. + return ent + else: + if raiseerr: + raise sa_exc.ArgumentError( + "Query has only expression-based entities - " + 'can\'t find property named "%s".' % (token,) + ) + else: + return None + + +class loader_option(object): + 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 + + 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") + ) + +""" % { + "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 + + +@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. + + The option is used in conjunction with an explicit join that loads + the desired rows, i.e.:: + + sess.query(Order).\ + join(Order.user).\ + options(contains_eager(Order.user)) + + 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. + + 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:: + + sess.query(User).\ + join(User.addresses).\ + filter(Address.email_address.like('%@aol.com')).\ + options(contains_eager(User.addresses)).\ + populate_existing() + + See the section :ref:`contains_eager` for complete usage details. + + .. seealso:: + + :ref:`loading_toplevel` + + :ref:`contains_eager` + + """ + if alias is not None: + if not isinstance(alias, str): + info = inspect(alias) + alias = info.selectable + + 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", + ) + + elif getattr(attr, "_of_type", None): + ot = inspect(attr._of_type) + alias = ot.selectable + + cloned = loadopt.set_relationship_strategy( + attr, {"lazy": "joined"}, propagate_to_loaders=False + ) + cloned.local_opts["eager_from_alias"] = alias + return cloned + + +@contains_eager._add_unbound_fn +def contains_eager(*keys, **kw): + return _UnboundLoad()._from_keys( + _UnboundLoad.contains_eager, keys, True, kw + ) + + +@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. + + 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:: + + 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 :class:`_query.Query` that has multiple entities, + the lead entity can be + specifically referred to using the :class:`_orm.Load` constructor:: + + session.query(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. + + .. versionadded:: 0.9.0 + + """ + cloned = loadopt.set_column_strategy( + attrs, {"deferred": False, "instrument": True} + ) + cloned.set_column_strategy( + "*", {"deferred": True, "instrument": True}, {"undefer_pks": True} + ) + return cloned + + +@load_only._add_unbound_fn +def load_only(*attrs): + return _UnboundLoad().load_only(*attrs) + + +@loader_option() +def joinedload(loadopt, attr, innerjoin=None): + """Indicate that the given attribute should be loaded using joined + eager loading. + + This function is part of the :class:`_orm.Load` interface and supports + both method-chained and standalone operation. + + examples:: + + # joined-load the "orders" collection on "User" + query(User).options(joinedload(User.orders)) + + # joined-load Order.items and then Item.keywords + query(Order).options( + joinedload(Order.items).joinedload(Item.keywords)) + + # lazily load Order.items, but when Items are loaded, + # joined-load the keywords collection + query(Order).options( + lazyload(Order.items).joinedload(Item.keywords)) + + :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 :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:`_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` + + """ + 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) + + +@loader_option() +def subqueryload(loadopt, attr): + """Indicate that the given attribute should be loaded using + subquery eager loading. + + This function is part of the :class:`_orm.Load` interface and supports + both method-chained and standalone operation. + + examples:: + + # subquery-load the "orders" collection on "User" + query(User).options(subqueryload(User.orders)) + + # subquery-load Order.items and then Item.keywords + query(Order).options( + subqueryload(Order.items).subqueryload(Item.keywords)) + + # lazily load Order.items, but when Items are loaded, + # subquery-load the keywords collection + query(Order).options( + lazyload(Order.items).subqueryload(Item.keywords)) + + + .. seealso:: + + :ref:`loading_toplevel` + + :ref:`subquery_eager_loading` + + """ + return loadopt.set_relationship_strategy(attr, {"lazy": "subquery"}) + + +@subqueryload._add_unbound_fn +def subqueryload(*keys): + return _UnboundLoad._from_keys(_UnboundLoad.subqueryload, keys, False, {}) + + +@loader_option() +def selectinload(loadopt, attr): + """Indicate that the given attribute should be loaded using + SELECT IN eager loading. + + This function is part of the :class:`_orm.Load` interface and supports + both method-chained and standalone operation. + + examples:: + + # selectin-load the "orders" collection on "User" + query(User).options(selectinload(User.orders)) + + # selectin-load Order.items and then Item.keywords + query(Order).options( + selectinload(Order.items).selectinload(Item.keywords)) + + # lazily load Order.items, but when Items are loaded, + # selectin-load the keywords collection + query(Order).options( + lazyload(Order.items).selectinload(Item.keywords)) + + .. versionadded:: 1.2 + + .. seealso:: + + :ref:`loading_toplevel` + + :ref:`selectin_eager_loading` + + """ + return loadopt.set_relationship_strategy(attr, {"lazy": "selectin"}) + + +@selectinload._add_unbound_fn +def selectinload(*keys): + return _UnboundLoad._from_keys(_UnboundLoad.selectinload, keys, False, {}) + + +@loader_option() +def lazyload(loadopt, 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 loadopt.set_relationship_strategy(attr, {"lazy": "select"}) + + +@lazyload._add_unbound_fn +def lazyload(*keys): + return _UnboundLoad._from_keys(_UnboundLoad.lazyload, keys, False, {}) + + +@loader_option() +def immediateload(loadopt, 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 = loadopt.set_relationship_strategy(attr, {"lazy": "immediate"}) + return loader + + +@immediateload._add_unbound_fn +def immediateload(*keys): + return _UnboundLoad._from_keys(_UnboundLoad.immediateload, keys, False, {}) + + +@loader_option() +def noload(loadopt, 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; 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. + + .. seealso:: + + :ref:`loading_toplevel` + + """ + + return loadopt.set_relationship_strategy(attr, {"lazy": "noload"}) + + +@noload._add_unbound_fn +def noload(*keys): + return _UnboundLoad._from_keys(_UnboundLoad.noload, keys, False, {}) + + +@loader_option() +def raiseload(loadopt, 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 loadopt.set_relationship_strategy( + attr, {"lazy": "raise_on_sql" if sql_only else "raise"} + ) + + +@raiseload._add_unbound_fn +def raiseload(*keys, **kw): + return _UnboundLoad._from_keys(_UnboundLoad.raiseload, keys, False, kw) + + +@loader_option() +def defaultload(loadopt, attr): + """Indicate an attribute should load using its default loader style. + + 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:: + + session.query(MyClass).options( + defaultload(MyClass.someattribute). + joinedload(MyOtherClass.someotherattribute) + ) + + :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") + ) + + .. seealso:: + + :meth:`_orm.Load.options` - allows for complex hierarchical + loader option structures with less verbosity than with individual + :func:`.defaultload` directives. + + :ref:`relationship_loader_options` + + :ref:`deferred_loading_w_multiple` + + """ + return loadopt.set_relationship_strategy(attr, None) + + +@defaultload._add_unbound_fn +def defaultload(*keys): + return _UnboundLoad._from_keys(_UnboundLoad.defaultload, keys, False, {}) + + +@loader_option() +def defer(loadopt, key, raiseload=False): + r"""Indicate that the given column-oriented attribute should be deferred, + e.g. not loaded until accessed. + + This function is part of the :class:`_orm.Load` interface and supports + both method-chained and standalone operation. + + e.g.:: + + from sqlalchemy.orm import defer + + session.query(MyClass).options( + defer("attribute_one"), + defer("attribute_two")) + + session.query(MyClass).options( + defer(MyClass.attribute_one), + defer(MyClass.attribute_two)) + + To specify a deferred load of an attribute on a related class, + the path can be specified one token at a time, specifying the loading + style for each link along the chain. To leave the loading style + for a link unchanged, use :func:`_orm.defaultload`:: + + session.query(MyClass).options(defaultload("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:: + + + session.query(MyClass).options( + defaultload("someattr"). + defer("some_column"). + defer("some_other_column"). + defer("another_column") + ) + + :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. + + .. versionadded:: 1.4 + + .. seealso:: + + :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. + + .. 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. + + + .. seealso:: + + :ref:`deferred` + + :func:`_orm.undefer` + + """ + strategy = {"deferred": True, "instrument": True} + if raiseload: + strategy["raiseload"] = True + return loadopt.set_column_strategy((key,), strategy) + + +@defer._add_unbound_fn +def defer(key, *addl_attrs, **kw): + if addl_attrs: + util.warn_deprecated( + "The *addl_attrs on orm.defer is deprecated. Please use " + "method chaining in conjunction with defaultload() to " + "indicate a path.", + version="1.3", + ) + return _UnboundLoad._from_keys( + _UnboundLoad.defer, (key,) + addl_attrs, False, kw + ) + + +@loader_option() +def undefer(loadopt, key): + r"""Indicate that the given column-oriented attribute should be undeferred, + e.g. specified within the SELECT statement of the entity as a whole. + + The column being undeferred is typically set up on the mapping as a + :func:`.deferred` attribute. + + This function is part of the :class:`_orm.Load` interface and supports + both method-chained and standalone operation. + + Examples:: + + # undefer two columns + session.query(MyClass).options(undefer("col1"), undefer("col2")) + + # 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 +def undefer(key, *addl_attrs): + if addl_attrs: + util.warn_deprecated( + "The *addl_attrs on orm.undefer is deprecated. Please use " + "method chaining in conjunction with defaultload() to " + "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 + ) + + +@undefer_group._add_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} + ) + + +@with_expression._add_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 loadopt + + +@selectin_polymorphic._add_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 -- cgit v1.2.3