1# Copyright (C) 2005-2016 the SQLAlchemy authors and contributors
2# <see AUTHORS file>
3#
4# This module is part of SQLAlchemy and is released under
5# the MIT License: http://www.opensource.org/licenses/mit-license.php
6
7"""
8
9"""
10
11from .interfaces import MapperOption, PropComparator
12from .. import util
13from ..sql.base import _generative, Generative
14from .. import exc as sa_exc, inspect
15from .base import _is_aliased_class, _class_to_mapper
16from . import util as orm_util
17from .path_registry import PathRegistry, TokenRegistry, \
18    _WILDCARD_TOKEN, _DEFAULT_TOKEN
19
20
21class Load(Generative, MapperOption):
22    """Represents loader options which modify the state of a
23    :class:`.Query` in order to affect how various mapped attributes are
24    loaded.
25
26    .. versionadded:: 0.9.0 The :meth:`.Load` system is a new foundation for
27       the existing system of loader options, including options such as
28       :func:`.orm.joinedload`, :func:`.orm.defer`, and others.   In
29       particular, it introduces a new method-chained system that replaces the
30       need for dot-separated paths as well as "_all()" options such as
31       :func:`.orm.joinedload_all`.
32
33    A :class:`.Load` object can be used directly or indirectly.  To use one
34    directly, instantiate given the parent class.  This style of usage is
35    useful when dealing with a :class:`.Query` that has multiple entities,
36    or when producing a loader option that can be applied generically to
37    any style of query::
38
39        myopt = Load(MyClass).joinedload("widgets")
40
41    The above ``myopt`` can now be used with :meth:`.Query.options`::
42
43        session.query(MyClass).options(myopt)
44
45    The :class:`.Load` construct is invoked indirectly whenever one makes use
46    of the various loader options that are present in ``sqlalchemy.orm``,
47    including options such as :func:`.orm.joinedload`, :func:`.orm.defer`,
48    :func:`.orm.subqueryload`, and all the rest.  These constructs produce an
49    "anonymous" form of the :class:`.Load` object which tracks attributes and
50    options, but is not linked to a parent class until it is associated with a
51    parent :class:`.Query`::
52
53        # produce "unbound" Load object
54        myopt = joinedload("widgets")
55
56        # when applied using options(), the option is "bound" to the
57        # class observed in the given query, e.g. MyClass
58        session.query(MyClass).options(myopt)
59
60    Whether the direct or indirect style is used, the :class:`.Load` object
61    returned now represents a specific "path" along the entities of a
62    :class:`.Query`.  This path can be traversed using a standard
63    method-chaining approach.  Supposing a class hierarchy such as ``User``,
64    ``User.addresses -> Address``, ``User.orders -> Order`` and
65    ``Order.items -> Item``, we can specify a variety of loader options along
66    each element in the "path"::
67
68        session.query(User).options(
69                    joinedload("addresses"),
70                    subqueryload("orders").joinedload("items")
71                )
72
73    Where above, the ``addresses`` collection will be joined-loaded, the
74    ``orders`` collection will be subquery-loaded, and within that subquery
75    load the ``items`` collection will be joined-loaded.
76
77
78    """
79
80    def __init__(self, entity):
81        insp = inspect(entity)
82        self.path = insp._path_registry
83        self.context = {}
84        self.local_opts = {}
85
86    def _generate(self):
87        cloned = super(Load, self)._generate()
88        cloned.local_opts = {}
89        return cloned
90
91    _merge_into_path = False
92    strategy = None
93    propagate_to_loaders = False
94
95    def process_query(self, query):
96        self._process(query, True)
97
98    def process_query_conditionally(self, query):
99        self._process(query, False)
100
101    def _process(self, query, raiseerr):
102        current_path = query._current_path
103        if current_path:
104            for (token, start_path), loader in self.context.items():
105                chopped_start_path = self._chop_path(start_path, current_path)
106                if chopped_start_path is not None:
107                    query._attributes[(token, chopped_start_path)] = loader
108        else:
109            query._attributes.update(self.context)
110
111    def _generate_path(self, path, attr, wildcard_key, raiseerr=True):
112        if raiseerr and not path.has_entity:
113            if isinstance(path, TokenRegistry):
114                raise sa_exc.ArgumentError(
115                    "Wildcard token cannot be followed by another entity")
116            else:
117                raise sa_exc.ArgumentError(
118                    "Attribute '%s' of entity '%s' does not "
119                    "refer to a mapped entity" %
120                    (path.prop.key, path.parent.entity)
121                )
122
123        if isinstance(attr, util.string_types):
124            default_token = attr.endswith(_DEFAULT_TOKEN)
125            if attr.endswith(_WILDCARD_TOKEN) or default_token:
126                if default_token:
127                    self.propagate_to_loaders = False
128                if wildcard_key:
129                    attr = "%s:%s" % (wildcard_key, attr)
130                return path.token(attr)
131
132            try:
133                # use getattr on the class to work around
134                # synonyms, hybrids, etc.
135                attr = getattr(path.entity.class_, attr)
136            except AttributeError:
137                if raiseerr:
138                    raise sa_exc.ArgumentError(
139                        "Can't find property named '%s' on the "
140                        "mapped entity %s in this Query. " % (
141                            attr, path.entity)
142                    )
143                else:
144                    return None
145            else:
146                attr = attr.property
147
148            path = path[attr]
149        else:
150            prop = attr.property
151
152            if not prop.parent.common_parent(path.mapper):
153                if raiseerr:
154                    raise sa_exc.ArgumentError(
155                        "Attribute '%s' does not "
156                        "link from element '%s'" % (attr, path.entity))
157                else:
158                    return None
159
160            if getattr(attr, '_of_type', None):
161                ac = attr._of_type
162                ext_info = inspect(ac)
163
164                path_element = ext_info.mapper
165                existing = path.entity_path[prop].get(
166                    self.context, "path_with_polymorphic")
167                if not ext_info.is_aliased_class:
168                    ac = orm_util.with_polymorphic(
169                        ext_info.mapper.base_mapper,
170                        ext_info.mapper, aliased=True,
171                        _use_mapper_path=True,
172                        _existing_alias=existing)
173                path.entity_path[prop].set(
174                    self.context, "path_with_polymorphic", inspect(ac))
175                path = path[prop][path_element]
176            else:
177                path = path[prop]
178
179        if path.has_entity:
180            path = path.entity_path
181        return path
182
183    def __str__(self):
184        return "Load(strategy=%r)" % (self.strategy, )
185
186    def _coerce_strat(self, strategy):
187        if strategy is not None:
188            strategy = tuple(sorted(strategy.items()))
189        return strategy
190
191    @_generative
192    def set_relationship_strategy(
193            self, attr, strategy, propagate_to_loaders=True):
194        strategy = self._coerce_strat(strategy)
195
196        self.propagate_to_loaders = propagate_to_loaders
197        # if the path is a wildcard, this will set propagate_to_loaders=False
198        self.path = self._generate_path(self.path, attr, "relationship")
199        self.strategy = strategy
200        if strategy is not None:
201            self._set_path_strategy()
202
203    @_generative
204    def set_column_strategy(self, attrs, strategy, opts=None):
205        strategy = self._coerce_strat(strategy)
206
207        for attr in attrs:
208            path = self._generate_path(self.path, attr, "column")
209            cloned = self._generate()
210            cloned.strategy = strategy
211            cloned.path = path
212            cloned.propagate_to_loaders = True
213            if opts:
214                cloned.local_opts.update(opts)
215            cloned._set_path_strategy()
216
217    def _set_path_strategy(self):
218        if self._merge_into_path:
219            # special helper for undefer_group
220            existing = self.path.get(self.context, "loader")
221            if existing:
222                existing.local_opts.update(self.local_opts)
223            else:
224                self.path.set(self.context, "loader", self)
225
226        elif self.path.has_entity:
227            self.path.parent.set(self.context, "loader", self)
228        else:
229            self.path.set(self.context, "loader", self)
230
231    def __getstate__(self):
232        d = self.__dict__.copy()
233        d["path"] = self.path.serialize()
234        return d
235
236    def __setstate__(self, state):
237        self.__dict__.update(state)
238        self.path = PathRegistry.deserialize(self.path)
239
240    def _chop_path(self, to_chop, path):
241        i = -1
242
243        for i, (c_token, p_token) in enumerate(zip(to_chop, path.path)):
244            if isinstance(c_token, util.string_types):
245                # TODO: this is approximated from the _UnboundLoad
246                # version and probably has issues, not fully covered.
247
248                if i == 0 and c_token.endswith(':' + _DEFAULT_TOKEN):
249                    return to_chop
250                elif c_token != 'relationship:%s' % (_WILDCARD_TOKEN,) and \
251                        c_token != p_token.key:
252                    return None
253
254            if c_token is p_token:
255                continue
256            else:
257                return None
258        return to_chop[i + 1:]
259
260
261class _UnboundLoad(Load):
262    """Represent a loader option that isn't tied to a root entity.
263
264    The loader option will produce an entity-linked :class:`.Load`
265    object when it is passed :meth:`.Query.options`.
266
267    This provides compatibility with the traditional system
268    of freestanding options, e.g. ``joinedload('x.y.z')``.
269
270    """
271
272    def __init__(self):
273        self.path = ()
274        self._to_bind = set()
275        self.local_opts = {}
276
277    _is_chain_link = False
278
279    def _set_path_strategy(self):
280        self._to_bind.add(self)
281
282    def _generate_path(self, path, attr, wildcard_key):
283        if wildcard_key and isinstance(attr, util.string_types) and \
284                attr in (_WILDCARD_TOKEN, _DEFAULT_TOKEN):
285            if attr == _DEFAULT_TOKEN:
286                self.propagate_to_loaders = False
287            attr = "%s:%s" % (wildcard_key, attr)
288
289        return path + (attr, )
290
291    def __getstate__(self):
292        d = self.__dict__.copy()
293        d['path'] = ret = []
294        for token in util.to_list(self.path):
295            if isinstance(token, PropComparator):
296                ret.append((token._parentmapper.class_, token.key))
297            else:
298                ret.append(token)
299        return d
300
301    def __setstate__(self, state):
302        ret = []
303        for key in state['path']:
304            if isinstance(key, tuple):
305                cls, propkey = key
306                ret.append(getattr(cls, propkey))
307            else:
308                ret.append(key)
309        state['path'] = tuple(ret)
310        self.__dict__ = state
311
312    def _process(self, query, raiseerr):
313        for val in self._to_bind:
314            val._bind_loader(query, query._attributes, raiseerr)
315
316    @classmethod
317    def _from_keys(self, meth, keys, chained, kw):
318        opt = _UnboundLoad()
319
320        def _split_key(key):
321            if isinstance(key, util.string_types):
322                # coerce fooload('*') into "default loader strategy"
323                if key == _WILDCARD_TOKEN:
324                    return (_DEFAULT_TOKEN, )
325                # coerce fooload(".*") into "wildcard on default entity"
326                elif key.startswith("." + _WILDCARD_TOKEN):
327                    key = key[1:]
328                return key.split(".")
329            else:
330                return (key,)
331        all_tokens = [token for key in keys for token in _split_key(key)]
332
333        for token in all_tokens[0:-1]:
334            if chained:
335                opt = meth(opt, token, **kw)
336            else:
337                opt = opt.defaultload(token)
338            opt._is_chain_link = True
339
340        opt = meth(opt, all_tokens[-1], **kw)
341        opt._is_chain_link = False
342
343        return opt
344
345    def _chop_path(self, to_chop, path):
346        i = -1
347        for i, (c_token, (p_mapper, p_prop)) in enumerate(
348                zip(to_chop, path.pairs())):
349            if isinstance(c_token, util.string_types):
350                if i == 0 and c_token.endswith(':' + _DEFAULT_TOKEN):
351                    return to_chop
352                elif c_token != 'relationship:%s' % (
353                        _WILDCARD_TOKEN,) and c_token != p_prop.key:
354                    return None
355            elif isinstance(c_token, PropComparator):
356                if c_token.property is not p_prop:
357                    return None
358        else:
359            i += 1
360
361        return to_chop[i:]
362
363    def _bind_loader(self, query, context, raiseerr):
364        start_path = self.path
365        # _current_path implies we're in a
366        # secondary load with an existing path
367
368        current_path = query._current_path
369        if current_path:
370            start_path = self._chop_path(start_path, current_path)
371
372        if not start_path:
373            return None
374
375        token = start_path[0]
376
377        if isinstance(token, util.string_types):
378            entity = self._find_entity_basestring(query, token, raiseerr)
379        elif isinstance(token, PropComparator):
380            prop = token.property
381            entity = self._find_entity_prop_comparator(
382                query,
383                prop.key,
384                token._parententity,
385                raiseerr)
386
387        else:
388            raise sa_exc.ArgumentError(
389                "mapper option expects "
390                "string key or list of attributes")
391
392        if not entity:
393            return
394
395        path_element = entity.entity_zero
396
397        # transfer our entity-less state into a Load() object
398        # with a real entity path.
399        loader = Load(path_element)
400        loader.context = context
401        loader.strategy = self.strategy
402
403        path = loader.path
404        for token in start_path:
405            loader.path = path = loader._generate_path(
406                loader.path, token, None, raiseerr)
407            if path is None:
408                return
409
410        loader.local_opts.update(self.local_opts)
411
412        if loader.path.has_entity:
413            effective_path = loader.path.parent
414        else:
415            effective_path = loader.path
416
417        # prioritize "first class" options over those
418        # that were "links in the chain", e.g. "x" and "y" in
419        # someload("x.y.z") versus someload("x") / someload("x.y")
420
421        if effective_path.is_token:
422            for path in effective_path.generate_for_superclasses():
423                if self._merge_into_path:
424                    # special helper for undefer_group
425                    existing = path.get(context, "loader")
426                    if existing:
427                        existing.local_opts.update(self.local_opts)
428                    else:
429                        path.set(context, "loader", loader)
430                elif self._is_chain_link:
431                    path.setdefault(context, "loader", loader)
432                else:
433                    path.set(context, "loader", loader)
434        else:
435            # only supported for the undefer_group() wildcard opt
436            assert not self._merge_into_path
437            if self._is_chain_link:
438                effective_path.setdefault(context, "loader", loader)
439            else:
440                effective_path.set(context, "loader", loader)
441
442    def _find_entity_prop_comparator(self, query, token, mapper, raiseerr):
443        if _is_aliased_class(mapper):
444            searchfor = mapper
445        else:
446            searchfor = _class_to_mapper(mapper)
447        for ent in query._mapper_entities:
448            if ent.corresponds_to(searchfor):
449                return ent
450        else:
451            if raiseerr:
452                if not list(query._mapper_entities):
453                    raise sa_exc.ArgumentError(
454                        "Query has only expression-based entities - "
455                        "can't find property named '%s'."
456                        % (token, )
457                    )
458                else:
459                    raise sa_exc.ArgumentError(
460                        "Can't find property '%s' on any entity "
461                        "specified in this Query.  Note the full path "
462                        "from root (%s) to target entity must be specified."
463                        % (token, ",".join(str(x) for
464                                           x in query._mapper_entities))
465                    )
466            else:
467                return None
468
469    def _find_entity_basestring(self, query, token, raiseerr):
470        if token.endswith(':' + _WILDCARD_TOKEN):
471            if len(list(query._mapper_entities)) != 1:
472                if raiseerr:
473                    raise sa_exc.ArgumentError(
474                        "Wildcard loader can only be used with exactly "
475                        "one entity.  Use Load(ent) to specify "
476                        "specific entities.")
477        elif token.endswith(_DEFAULT_TOKEN):
478            raiseerr = False
479
480        for ent in query._mapper_entities:
481            # return only the first _MapperEntity when searching
482            # based on string prop name.   Ideally object
483            # attributes are used to specify more exactly.
484            return ent
485        else:
486            if raiseerr:
487                raise sa_exc.ArgumentError(
488                    "Query has only expression-based entities - "
489                    "can't find property named '%s'."
490                    % (token, )
491                )
492            else:
493                return None
494
495
496class loader_option(object):
497    def __init__(self):
498        pass
499
500    def __call__(self, fn):
501        self.name = name = fn.__name__
502        self.fn = fn
503        if hasattr(Load, name):
504            raise TypeError("Load class already has a %s method." % (name))
505        setattr(Load, name, fn)
506
507        return self
508
509    def _add_unbound_fn(self, fn):
510        self._unbound_fn = fn
511        fn_doc = self.fn.__doc__
512        self.fn.__doc__ = """Produce a new :class:`.Load` object with the
513:func:`.orm.%(name)s` option applied.
514
515See :func:`.orm.%(name)s` for usage examples.
516
517""" % {"name": self.name}
518
519        fn.__doc__ = fn_doc
520        return self
521
522    def _add_unbound_all_fn(self, fn):
523        self._unbound_all_fn = fn
524        fn.__doc__ = """Produce a standalone "all" option for :func:`.orm.%(name)s`.
525
526.. deprecated:: 0.9.0
527
528    The "_all()" style is replaced by method chaining, e.g.::
529
530        session.query(MyClass).options(
531            %(name)s("someattribute").%(name)s("anotherattribute")
532        )
533
534""" % {"name": self.name}
535        return self
536
537
538@loader_option()
539def contains_eager(loadopt, attr, alias=None):
540    """Indicate that the given attribute should be eagerly loaded from
541    columns stated manually in the query.
542
543    This function is part of the :class:`.Load` interface and supports
544    both method-chained and standalone operation.
545
546    The option is used in conjunction with an explicit join that loads
547    the desired rows, i.e.::
548
549        sess.query(Order).\\
550                join(Order.user).\\
551                options(contains_eager(Order.user))
552
553    The above query would join from the ``Order`` entity to its related
554    ``User`` entity, and the returned ``Order`` objects would have the
555    ``Order.user`` attribute pre-populated.
556
557    :func:`contains_eager` also accepts an `alias` argument, which is the
558    string name of an alias, an :func:`~sqlalchemy.sql.expression.alias`
559    construct, or an :func:`~sqlalchemy.orm.aliased` construct. Use this when
560    the eagerly-loaded rows are to come from an aliased table::
561
562        user_alias = aliased(User)
563        sess.query(Order).\\
564                join((user_alias, Order.user)).\\
565                options(contains_eager(Order.user, alias=user_alias))
566
567    .. seealso::
568
569        :ref:`contains_eager`
570
571    """
572    if alias is not None:
573        if not isinstance(alias, str):
574            info = inspect(alias)
575            alias = info.selectable
576
577    cloned = loadopt.set_relationship_strategy(
578        attr,
579        {"lazy": "joined"},
580        propagate_to_loaders=False
581    )
582    cloned.local_opts['eager_from_alias'] = alias
583    return cloned
584
585
586@contains_eager._add_unbound_fn
587def contains_eager(*keys, **kw):
588    return _UnboundLoad()._from_keys(
589        _UnboundLoad.contains_eager, keys, True, kw)
590
591
592@loader_option()
593def load_only(loadopt, *attrs):
594    """Indicate that for a particular entity, only the given list
595    of column-based attribute names should be loaded; all others will be
596    deferred.
597
598    This function is part of the :class:`.Load` interface and supports
599    both method-chained and standalone operation.
600
601    Example - given a class ``User``, load only the ``name`` and ``fullname``
602    attributes::
603
604        session.query(User).options(load_only("name", "fullname"))
605
606    Example - given a relationship ``User.addresses -> Address``, specify
607    subquery loading for the ``User.addresses`` collection, but on each
608    ``Address`` object load only the ``email_address`` attribute::
609
610        session.query(User).options(
611                subqueryload("addresses").load_only("email_address")
612        )
613
614    For a :class:`.Query` that has multiple entities, the lead entity can be
615    specifically referred to using the :class:`.Load` constructor::
616
617        session.query(User, Address).join(User.addresses).options(
618                    Load(User).load_only("name", "fullname"),
619                    Load(Address).load_only("email_addres")
620                )
621
622
623    .. versionadded:: 0.9.0
624
625    """
626    cloned = loadopt.set_column_strategy(
627        attrs,
628        {"deferred": False, "instrument": True}
629    )
630    cloned.set_column_strategy("*",
631                               {"deferred": True, "instrument": True},
632                               {"undefer_pks": True})
633    return cloned
634
635
636@load_only._add_unbound_fn
637def load_only(*attrs):
638    return _UnboundLoad().load_only(*attrs)
639
640
641@loader_option()
642def joinedload(loadopt, attr, innerjoin=None):
643    """Indicate that the given attribute should be loaded using joined
644    eager loading.
645
646    This function is part of the :class:`.Load` interface and supports
647    both method-chained and standalone operation.
648
649    examples::
650
651        # joined-load the "orders" collection on "User"
652        query(User).options(joinedload(User.orders))
653
654        # joined-load Order.items and then Item.keywords
655        query(Order).options(joinedload(Order.items).joinedload(Item.keywords))
656
657        # lazily load Order.items, but when Items are loaded,
658        # joined-load the keywords collection
659        query(Order).options(lazyload(Order.items).joinedload(Item.keywords))
660
661    :param innerjoin: if ``True``, indicates that the joined eager load should
662     use an inner join instead of the default of left outer join::
663
664        query(Order).options(joinedload(Order.user, innerjoin=True))
665
666     In order to chain multiple eager joins together where some may be
667     OUTER and others INNER, right-nested joins are used to link them::
668
669        query(A).options(
670            joinedload(A.bs, innerjoin=False).
671                joinedload(B.cs, innerjoin=True)
672        )
673
674     The above query, linking A.bs via "outer" join and B.cs via "inner" join
675     would render the joins as "a LEFT OUTER JOIN (b JOIN c)".   When using
676     SQLite, this form of JOIN is translated to use full subqueries as this
677     syntax is otherwise not directly supported.
678
679     The ``innerjoin`` flag can also be stated with the term ``"unnested"``.
680     This will prevent joins from being right-nested, and will instead
681     link an "innerjoin" eagerload to an "outerjoin" eagerload by bypassing
682     the "inner" join.   Using this form as follows::
683
684        query(A).options(
685            joinedload(A.bs, innerjoin=False).
686                joinedload(B.cs, innerjoin="unnested")
687        )
688
689     Joins will be rendered as "a LEFT OUTER JOIN b LEFT OUTER JOIN c", so that
690     all of "a" is matched rather than being incorrectly limited by a "b" that
691     does not contain a "c".
692
693     .. note:: The "unnested" flag does **not** affect the JOIN rendered
694        from a many-to-many association table, e.g. a table configured
695        as :paramref:`.relationship.secondary`, to the target table; for
696        correctness of results, these joins are always INNER and are
697        therefore right-nested if linked to an OUTER join.
698
699     .. versionadded:: 0.9.4 Added support for "nesting" of eager "inner"
700        joins.  See :ref:`feature_2976`.
701
702     .. versionchanged:: 1.0.0 ``innerjoin=True`` now implies
703        ``innerjoin="nested"``, whereas in 0.9 it implied
704        ``innerjoin="unnested"``.  In order to achieve the pre-1.0 "unnested"
705        inner join behavior, use the value ``innerjoin="unnested"``.
706        See :ref:`migration_3008`.
707
708    .. note::
709
710        The joins produced by :func:`.orm.joinedload` are **anonymously
711        aliased**.  The criteria by which the join proceeds cannot be
712        modified, nor can the :class:`.Query` refer to these joins in any way,
713        including ordering.
714
715        To produce a specific SQL JOIN which is explicitly available, use
716        :meth:`.Query.join`.   To combine explicit JOINs with eager loading
717        of collections, use :func:`.orm.contains_eager`; see
718        :ref:`contains_eager`.
719
720    .. seealso::
721
722        :ref:`loading_toplevel`
723
724        :ref:`contains_eager`
725
726        :func:`.orm.subqueryload`
727
728        :func:`.orm.lazyload`
729
730        :paramref:`.relationship.lazy`
731
732        :paramref:`.relationship.innerjoin` - :func:`.relationship`-level
733        version of the :paramref:`.joinedload.innerjoin` option.
734
735    """
736    loader = loadopt.set_relationship_strategy(attr, {"lazy": "joined"})
737    if innerjoin is not None:
738        loader.local_opts['innerjoin'] = innerjoin
739    return loader
740
741
742@joinedload._add_unbound_fn
743def joinedload(*keys, **kw):
744    return _UnboundLoad._from_keys(
745        _UnboundLoad.joinedload, keys, False, kw)
746
747
748@joinedload._add_unbound_all_fn
749def joinedload_all(*keys, **kw):
750    return _UnboundLoad._from_keys(
751        _UnboundLoad.joinedload, keys, True, kw)
752
753
754@loader_option()
755def subqueryload(loadopt, attr):
756    """Indicate that the given attribute should be loaded using
757    subquery eager loading.
758
759    This function is part of the :class:`.Load` interface and supports
760    both method-chained and standalone operation.
761
762    examples::
763
764        # subquery-load the "orders" collection on "User"
765        query(User).options(subqueryload(User.orders))
766
767        # subquery-load Order.items and then Item.keywords
768        query(Order).options(subqueryload(Order.items).subqueryload(Item.keywords))
769
770        # lazily load Order.items, but when Items are loaded,
771        # subquery-load the keywords collection
772        query(Order).options(lazyload(Order.items).subqueryload(Item.keywords))
773
774
775    .. seealso::
776
777        :ref:`loading_toplevel`
778
779        :func:`.orm.joinedload`
780
781        :func:`.orm.lazyload`
782
783        :paramref:`.relationship.lazy`
784
785    """
786    return loadopt.set_relationship_strategy(attr, {"lazy": "subquery"})
787
788
789@subqueryload._add_unbound_fn
790def subqueryload(*keys):
791    return _UnboundLoad._from_keys(_UnboundLoad.subqueryload, keys, False, {})
792
793
794@subqueryload._add_unbound_all_fn
795def subqueryload_all(*keys):
796    return _UnboundLoad._from_keys(_UnboundLoad.subqueryload, keys, True, {})
797
798
799@loader_option()
800def lazyload(loadopt, attr):
801    """Indicate that the given attribute should be loaded using "lazy"
802    loading.
803
804    This function is part of the :class:`.Load` interface and supports
805    both method-chained and standalone operation.
806
807    .. seealso::
808
809        :paramref:`.relationship.lazy`
810
811    """
812    return loadopt.set_relationship_strategy(attr, {"lazy": "select"})
813
814
815@lazyload._add_unbound_fn
816def lazyload(*keys):
817    return _UnboundLoad._from_keys(_UnboundLoad.lazyload, keys, False, {})
818
819
820@lazyload._add_unbound_all_fn
821def lazyload_all(*keys):
822    return _UnboundLoad._from_keys(_UnboundLoad.lazyload, keys, True, {})
823
824
825@loader_option()
826def immediateload(loadopt, attr):
827    """Indicate that the given attribute should be loaded using
828    an immediate load with a per-attribute SELECT statement.
829
830    This function is part of the :class:`.Load` interface and supports
831    both method-chained and standalone operation.
832
833    .. seealso::
834
835        :ref:`loading_toplevel`
836
837        :func:`.orm.joinedload`
838
839        :func:`.orm.lazyload`
840
841        :paramref:`.relationship.lazy`
842
843    """
844    loader = loadopt.set_relationship_strategy(attr, {"lazy": "immediate"})
845    return loader
846
847
848@immediateload._add_unbound_fn
849def immediateload(*keys):
850    return _UnboundLoad._from_keys(
851        _UnboundLoad.immediateload, keys, False, {})
852
853
854@loader_option()
855def noload(loadopt, attr):
856    """Indicate that the given relationship attribute should remain unloaded.
857
858    This function is part of the :class:`.Load` interface and supports
859    both method-chained and standalone operation.
860
861    :func:`.orm.noload` applies to :func:`.relationship` attributes; for
862    column-based attributes, see :func:`.orm.defer`.
863
864    """
865
866    return loadopt.set_relationship_strategy(attr, {"lazy": "noload"})
867
868
869@noload._add_unbound_fn
870def noload(*keys):
871    return _UnboundLoad._from_keys(_UnboundLoad.noload, keys, False, {})
872
873
874@loader_option()
875def defaultload(loadopt, attr):
876    """Indicate an attribute should load using its default loader style.
877
878    This method is used to link to other loader options, such as
879    to set the :func:`.orm.defer` option on a class that is linked to
880    a relationship of the parent class being loaded, :func:`.orm.defaultload`
881    can be used to navigate this path without changing the loading style
882    of the relationship::
883
884        session.query(MyClass).options(defaultload("someattr").defer("some_column"))
885
886    .. seealso::
887
888        :func:`.orm.defer`
889
890        :func:`.orm.undefer`
891
892    """
893    return loadopt.set_relationship_strategy(
894        attr,
895        None
896    )
897
898
899@defaultload._add_unbound_fn
900def defaultload(*keys):
901    return _UnboundLoad._from_keys(_UnboundLoad.defaultload, keys, False, {})
902
903
904@loader_option()
905def defer(loadopt, key):
906    """Indicate that the given column-oriented attribute should be deferred, e.g.
907    not loaded until accessed.
908
909    This function is part of the :class:`.Load` interface and supports
910    both method-chained and standalone operation.
911
912    e.g.::
913
914        from sqlalchemy.orm import defer
915
916        session.query(MyClass).options(
917                            defer("attribute_one"),
918                            defer("attribute_two"))
919
920        session.query(MyClass).options(
921                            defer(MyClass.attribute_one),
922                            defer(MyClass.attribute_two))
923
924    To specify a deferred load of an attribute on a related class,
925    the path can be specified one token at a time, specifying the loading
926    style for each link along the chain.  To leave the loading style
927    for a link unchanged, use :func:`.orm.defaultload`::
928
929        session.query(MyClass).options(defaultload("someattr").defer("some_column"))
930
931    A :class:`.Load` object that is present on a certain path can have
932    :meth:`.Load.defer` called multiple times, each will operate on the same
933    parent entity::
934
935
936        session.query(MyClass).options(
937                        defaultload("someattr").
938                            defer("some_column").
939                            defer("some_other_column").
940                            defer("another_column")
941            )
942
943    :param key: Attribute to be deferred.
944
945    :param \*addl_attrs: Deprecated; this option supports the old 0.8 style
946     of specifying a path as a series of attributes, which is now superseded
947     by the method-chained style.
948
949    .. seealso::
950
951        :ref:`deferred`
952
953        :func:`.orm.undefer`
954
955    """
956    return loadopt.set_column_strategy(
957        (key, ),
958        {"deferred": True, "instrument": True}
959    )
960
961
962@defer._add_unbound_fn
963def defer(key, *addl_attrs):
964    return _UnboundLoad._from_keys(
965        _UnboundLoad.defer, (key, ) + addl_attrs, False, {})
966
967
968@loader_option()
969def undefer(loadopt, key):
970    """Indicate that the given column-oriented attribute should be undeferred,
971    e.g. specified within the SELECT statement of the entity as a whole.
972
973    The column being undeferred is typically set up on the mapping as a
974    :func:`.deferred` attribute.
975
976    This function is part of the :class:`.Load` interface and supports
977    both method-chained and standalone operation.
978
979    Examples::
980
981        # undefer two columns
982        session.query(MyClass).options(undefer("col1"), undefer("col2"))
983
984        # undefer all columns specific to a single class using Load + *
985        session.query(MyClass, MyOtherClass).options(
986            Load(MyClass).undefer("*"))
987
988    :param key: Attribute to be undeferred.
989
990    :param \*addl_attrs: Deprecated; this option supports the old 0.8 style
991     of specifying a path as a series of attributes, which is now superseded
992     by the method-chained style.
993
994    .. seealso::
995
996        :ref:`deferred`
997
998        :func:`.orm.defer`
999
1000        :func:`.orm.undefer_group`
1001
1002    """
1003    return loadopt.set_column_strategy(
1004        (key, ),
1005        {"deferred": False, "instrument": True}
1006    )
1007
1008
1009@undefer._add_unbound_fn
1010def undefer(key, *addl_attrs):
1011    return _UnboundLoad._from_keys(
1012        _UnboundLoad.undefer, (key, ) + addl_attrs, False, {})
1013
1014
1015@loader_option()
1016def undefer_group(loadopt, name):
1017    """Indicate that columns within the given deferred group name should be
1018    undeferred.
1019
1020    The columns being undeferred are set up on the mapping as
1021    :func:`.deferred` attributes and include a "group" name.
1022
1023    E.g::
1024
1025        session.query(MyClass).options(undefer_group("large_attrs"))
1026
1027    To undefer a group of attributes on a related entity, the path can be
1028    spelled out using relationship loader options, such as
1029    :func:`.orm.defaultload`::
1030
1031        session.query(MyClass).options(
1032            defaultload("someattr").undefer_group("large_attrs"))
1033
1034    .. versionchanged:: 0.9.0 :func:`.orm.undefer_group` is now specific to a
1035       particiular entity load path.
1036
1037    .. seealso::
1038
1039        :ref:`deferred`
1040
1041        :func:`.orm.defer`
1042
1043        :func:`.orm.undefer`
1044
1045    """
1046    loadopt._merge_into_path = True
1047    return loadopt.set_column_strategy(
1048        "*",
1049        None,
1050        {"undefer_group_%s" % name: True}
1051    )
1052
1053
1054@undefer_group._add_unbound_fn
1055def undefer_group(name):
1056    return _UnboundLoad().undefer_group(name)
1057