1# orm/loading.py
2# Copyright (C) 2005-2019 the SQLAlchemy authors and contributors
3# <see AUTHORS file>
4#
5# This module is part of SQLAlchemy and is released under
6# the MIT License: http://www.opensource.org/licenses/mit-license.php
7
8"""private module containing functions used to convert database
9rows into object instances and associated state.
10
11the functions here are called primarily by Query, Mapper,
12as well as some of the attribute loading strategies.
13
14"""
15from __future__ import absolute_import
16
17import collections
18
19from . import attributes
20from . import exc as orm_exc
21from . import path_registry
22from . import strategy_options
23from .base import _DEFER_FOR_STATE
24from .base import _SET_DEFERRED_EXPIRED
25from .util import _none_set
26from .util import state_str
27from .. import exc as sa_exc
28from .. import util
29from ..sql import util as sql_util
30
31
32_new_runid = util.counter()
33
34
35def instances(query, cursor, context):
36    """Return an ORM result as an iterator."""
37
38    context.runid = _new_runid()
39    context.post_load_paths = {}
40
41    filtered = query._has_mapper_entities
42
43    single_entity = (
44        not query._only_return_tuples
45        and len(query._entities) == 1
46        and query._entities[0].supports_single_entity
47    )
48
49    if filtered:
50        if single_entity:
51            filter_fn = id
52        else:
53
54            def filter_fn(row):
55                return tuple(
56                    id(item) if ent.use_id_for_hash else item
57                    for ent, item in zip(query._entities, row)
58                )
59
60    try:
61        (process, labels) = list(
62            zip(
63                *[
64                    query_entity.row_processor(query, context, cursor)
65                    for query_entity in query._entities
66                ]
67            )
68        )
69
70        if not single_entity:
71            keyed_tuple = util.lightweight_named_tuple("result", labels)
72
73        while True:
74            context.partials = {}
75
76            if query._yield_per:
77                fetch = cursor.fetchmany(query._yield_per)
78                if not fetch:
79                    break
80            else:
81                fetch = cursor.fetchall()
82
83            if single_entity:
84                proc = process[0]
85                rows = [proc(row) for row in fetch]
86            else:
87                rows = [
88                    keyed_tuple([proc(row) for proc in process])
89                    for row in fetch
90                ]
91
92            for path, post_load in context.post_load_paths.items():
93                post_load.invoke(context, path)
94
95            if filtered:
96                rows = util.unique_list(rows, filter_fn)
97
98            for row in rows:
99                yield row
100
101            if not query._yield_per:
102                break
103    except Exception as err:
104        cursor.close()
105        util.raise_from_cause(err)
106
107
108@util.dependencies("sqlalchemy.orm.query")
109def merge_result(querylib, query, iterator, load=True):
110    """Merge a result into this :class:`.Query` object's Session."""
111
112    session = query.session
113    if load:
114        # flush current contents if we expect to load data
115        session._autoflush()
116
117    autoflush = session.autoflush
118    try:
119        session.autoflush = False
120        single_entity = len(query._entities) == 1
121        if single_entity:
122            if isinstance(query._entities[0], querylib._MapperEntity):
123                result = [
124                    session._merge(
125                        attributes.instance_state(instance),
126                        attributes.instance_dict(instance),
127                        load=load,
128                        _recursive={},
129                        _resolve_conflict_map={},
130                    )
131                    for instance in iterator
132                ]
133            else:
134                result = list(iterator)
135        else:
136            mapped_entities = [
137                i
138                for i, e in enumerate(query._entities)
139                if isinstance(e, querylib._MapperEntity)
140            ]
141            result = []
142            keys = [ent._label_name for ent in query._entities]
143            keyed_tuple = util.lightweight_named_tuple("result", keys)
144            for row in iterator:
145                newrow = list(row)
146                for i in mapped_entities:
147                    if newrow[i] is not None:
148                        newrow[i] = session._merge(
149                            attributes.instance_state(newrow[i]),
150                            attributes.instance_dict(newrow[i]),
151                            load=load,
152                            _recursive={},
153                            _resolve_conflict_map={},
154                        )
155                result.append(keyed_tuple(newrow))
156
157        return iter(result)
158    finally:
159        session.autoflush = autoflush
160
161
162def get_from_identity(session, key, passive):
163    """Look up the given key in the given session's identity map,
164    check the object for expired state if found.
165
166    """
167    instance = session.identity_map.get(key)
168    if instance is not None:
169
170        state = attributes.instance_state(instance)
171
172        # expired - ensure it still exists
173        if state.expired:
174            if not passive & attributes.SQL_OK:
175                # TODO: no coverage here
176                return attributes.PASSIVE_NO_RESULT
177            elif not passive & attributes.RELATED_OBJECT_OK:
178                # this mode is used within a flush and the instance's
179                # expired state will be checked soon enough, if necessary
180                return instance
181            try:
182                state._load_expired(state, passive)
183            except orm_exc.ObjectDeletedError:
184                session._remove_newly_deleted([state])
185                return None
186        return instance
187    else:
188        return None
189
190
191def load_on_ident(
192    query, key, refresh_state=None, with_for_update=None, only_load_props=None
193):
194    """Load the given identity key from the database."""
195
196    if key is not None:
197        ident = key[1]
198        identity_token = key[2]
199    else:
200        ident = identity_token = None
201
202    return load_on_pk_identity(
203        query,
204        ident,
205        refresh_state=refresh_state,
206        with_for_update=with_for_update,
207        only_load_props=only_load_props,
208        identity_token=identity_token,
209    )
210
211
212def load_on_pk_identity(
213    query,
214    primary_key_identity,
215    refresh_state=None,
216    with_for_update=None,
217    only_load_props=None,
218    identity_token=None,
219):
220
221    """Load the given primary key identity from the database."""
222
223    if refresh_state is None:
224        q = query._clone()
225        q._get_condition()
226    else:
227        q = query._clone()
228
229    if primary_key_identity is not None:
230        mapper = query._mapper_zero()
231
232        (_get_clause, _get_params) = mapper._get_clause
233
234        # None present in ident - turn those comparisons
235        # into "IS NULL"
236        if None in primary_key_identity:
237            nones = set(
238                [
239                    _get_params[col].key
240                    for col, value in zip(
241                        mapper.primary_key, primary_key_identity
242                    )
243                    if value is None
244                ]
245            )
246            _get_clause = sql_util.adapt_criterion_to_null(_get_clause, nones)
247
248        _get_clause = q._adapt_clause(_get_clause, True, False)
249        q._criterion = _get_clause
250
251        params = dict(
252            [
253                (_get_params[primary_key].key, id_val)
254                for id_val, primary_key in zip(
255                    primary_key_identity, mapper.primary_key
256                )
257            ]
258        )
259
260        q._params = params
261
262    # with_for_update needs to be query.LockmodeArg()
263    if with_for_update is not None:
264        version_check = True
265        q._for_update_arg = with_for_update
266    elif query._for_update_arg is not None:
267        version_check = True
268        q._for_update_arg = query._for_update_arg
269    else:
270        version_check = False
271
272    q._get_options(
273        populate_existing=bool(refresh_state),
274        version_check=version_check,
275        only_load_props=only_load_props,
276        refresh_state=refresh_state,
277        identity_token=identity_token,
278    )
279    q._order_by = None
280
281    try:
282        return q.one()
283    except orm_exc.NoResultFound:
284        return None
285
286
287def _setup_entity_query(
288    context,
289    mapper,
290    query_entity,
291    path,
292    adapter,
293    column_collection,
294    with_polymorphic=None,
295    only_load_props=None,
296    polymorphic_discriminator=None,
297    **kw
298):
299
300    if with_polymorphic:
301        poly_properties = mapper._iterate_polymorphic_properties(
302            with_polymorphic
303        )
304    else:
305        poly_properties = mapper._polymorphic_properties
306
307    quick_populators = {}
308
309    path.set(context.attributes, "memoized_setups", quick_populators)
310
311    for value in poly_properties:
312        if only_load_props and value.key not in only_load_props:
313            continue
314        value.setup(
315            context,
316            query_entity,
317            path,
318            adapter,
319            only_load_props=only_load_props,
320            column_collection=column_collection,
321            memoized_populators=quick_populators,
322            **kw
323        )
324
325    if (
326        polymorphic_discriminator is not None
327        and polymorphic_discriminator is not mapper.polymorphic_on
328    ):
329
330        if adapter:
331            pd = adapter.columns[polymorphic_discriminator]
332        else:
333            pd = polymorphic_discriminator
334        column_collection.append(pd)
335
336
337def _instance_processor(
338    mapper,
339    context,
340    result,
341    path,
342    adapter,
343    only_load_props=None,
344    refresh_state=None,
345    polymorphic_discriminator=None,
346    _polymorphic_from=None,
347):
348    """Produce a mapper level row processor callable
349       which processes rows into mapped instances."""
350
351    # note that this method, most of which exists in a closure
352    # called _instance(), resists being broken out, as
353    # attempts to do so tend to add significant function
354    # call overhead.  _instance() is the most
355    # performance-critical section in the whole ORM.
356
357    pk_cols = mapper.primary_key
358
359    if adapter:
360        pk_cols = [adapter.columns[c] for c in pk_cols]
361
362    identity_class = mapper._identity_class
363
364    populators = collections.defaultdict(list)
365
366    props = mapper._prop_set
367    if only_load_props is not None:
368        props = props.intersection(mapper._props[k] for k in only_load_props)
369
370    quick_populators = path.get(
371        context.attributes, "memoized_setups", _none_set
372    )
373
374    for prop in props:
375        if prop in quick_populators:
376            # this is an inlined path just for column-based attributes.
377            col = quick_populators[prop]
378            if col is _DEFER_FOR_STATE:
379                populators["new"].append(
380                    (prop.key, prop._deferred_column_loader)
381                )
382            elif col is _SET_DEFERRED_EXPIRED:
383                # note that in this path, we are no longer
384                # searching in the result to see if the column might
385                # be present in some unexpected way.
386                populators["expire"].append((prop.key, False))
387            else:
388                getter = None
389                # the "adapter" can be here via different paths,
390                # e.g. via adapter present at setup_query or adapter
391                # applied to the query afterwards via eager load subquery.
392                # If the column here
393                # were already a product of this adapter, sending it through
394                # the adapter again can return a totally new expression that
395                # won't be recognized in the result, and the ColumnAdapter
396                # currently does not accommodate for this.   OTOH, if the
397                # column were never applied through this adapter, we may get
398                # None back, in which case we still won't get our "getter".
399                # so try both against result._getter().  See issue #4048
400                if adapter:
401                    adapted_col = adapter.columns[col]
402                    if adapted_col is not None:
403                        getter = result._getter(adapted_col, False)
404                if not getter:
405                    getter = result._getter(col, False)
406                if getter:
407                    populators["quick"].append((prop.key, getter))
408                else:
409                    # fall back to the ColumnProperty itself, which
410                    # will iterate through all of its columns
411                    # to see if one fits
412                    prop.create_row_processor(
413                        context, path, mapper, result, adapter, populators
414                    )
415        else:
416            prop.create_row_processor(
417                context, path, mapper, result, adapter, populators
418            )
419
420    propagate_options = context.propagate_options
421    load_path = (
422        context.query._current_path + path
423        if context.query._current_path.path
424        else path
425    )
426
427    session_identity_map = context.session.identity_map
428
429    populate_existing = context.populate_existing or mapper.always_refresh
430    load_evt = bool(mapper.class_manager.dispatch.load)
431    refresh_evt = bool(mapper.class_manager.dispatch.refresh)
432    persistent_evt = bool(context.session.dispatch.loaded_as_persistent)
433    if persistent_evt:
434        loaded_as_persistent = context.session.dispatch.loaded_as_persistent
435    instance_state = attributes.instance_state
436    instance_dict = attributes.instance_dict
437    session_id = context.session.hash_key
438    version_check = context.version_check
439    runid = context.runid
440    identity_token = context.identity_token
441
442    if not refresh_state and _polymorphic_from is not None:
443        key = ("loader", path.path)
444        if key in context.attributes and context.attributes[key].strategy == (
445            ("selectinload_polymorphic", True),
446        ):
447            selectin_load_via = mapper._should_selectin_load(
448                context.attributes[key].local_opts["entities"],
449                _polymorphic_from,
450            )
451        else:
452            selectin_load_via = mapper._should_selectin_load(
453                None, _polymorphic_from
454            )
455
456        if selectin_load_via and selectin_load_via is not _polymorphic_from:
457            # only_load_props goes w/ refresh_state only, and in a refresh
458            # we are a single row query for the exact entity; polymorphic
459            # loading does not apply
460            assert only_load_props is None
461
462            callable_ = _load_subclass_via_in(context, path, selectin_load_via)
463
464            PostLoad.callable_for_path(
465                context,
466                load_path,
467                selectin_load_via.mapper,
468                selectin_load_via,
469                callable_,
470                selectin_load_via,
471            )
472
473    post_load = PostLoad.for_context(context, load_path, only_load_props)
474
475    if refresh_state:
476        refresh_identity_key = refresh_state.key
477        if refresh_identity_key is None:
478            # super-rare condition; a refresh is being called
479            # on a non-instance-key instance; this is meant to only
480            # occur within a flush()
481            refresh_identity_key = mapper._identity_key_from_state(
482                refresh_state
483            )
484    else:
485        refresh_identity_key = None
486
487    if mapper.allow_partial_pks:
488        is_not_primary_key = _none_set.issuperset
489    else:
490        is_not_primary_key = _none_set.intersection
491
492    def _instance(row):
493
494        # determine the state that we'll be populating
495        if refresh_identity_key:
496            # fixed state that we're refreshing
497            state = refresh_state
498            instance = state.obj()
499            dict_ = instance_dict(instance)
500            isnew = state.runid != runid
501            currentload = True
502            loaded_instance = False
503        else:
504            # look at the row, see if that identity is in the
505            # session, or we have to create a new one
506            identitykey = (
507                identity_class,
508                tuple([row[column] for column in pk_cols]),
509                identity_token,
510            )
511
512            instance = session_identity_map.get(identitykey)
513
514            if instance is not None:
515                # existing instance
516                state = instance_state(instance)
517                dict_ = instance_dict(instance)
518
519                isnew = state.runid != runid
520                currentload = not isnew
521                loaded_instance = False
522
523                if version_check and not currentload:
524                    _validate_version_id(mapper, state, dict_, row, adapter)
525
526            else:
527                # create a new instance
528
529                # check for non-NULL values in the primary key columns,
530                # else no entity is returned for the row
531                if is_not_primary_key(identitykey[1]):
532                    return None
533
534                isnew = True
535                currentload = True
536                loaded_instance = True
537
538                instance = mapper.class_manager.new_instance()
539
540                dict_ = instance_dict(instance)
541                state = instance_state(instance)
542                state.key = identitykey
543                state.identity_token = identity_token
544
545                # attach instance to session.
546                state.session_id = session_id
547                session_identity_map._add_unpresent(state, identitykey)
548
549        # populate.  this looks at whether this state is new
550        # for this load or was existing, and whether or not this
551        # row is the first row with this identity.
552        if currentload or populate_existing:
553            # full population routines.  Objects here are either
554            # just created, or we are doing a populate_existing
555
556            # be conservative about setting load_path when populate_existing
557            # is in effect; want to maintain options from the original
558            # load.  see test_expire->test_refresh_maintains_deferred_options
559            if isnew and (propagate_options or not populate_existing):
560                state.load_options = propagate_options
561                state.load_path = load_path
562
563            _populate_full(
564                context,
565                row,
566                state,
567                dict_,
568                isnew,
569                load_path,
570                loaded_instance,
571                populate_existing,
572                populators,
573            )
574
575            if isnew:
576                if loaded_instance:
577                    if load_evt:
578                        state.manager.dispatch.load(state, context)
579                    if persistent_evt:
580                        loaded_as_persistent(context.session, state.obj())
581                elif refresh_evt:
582                    state.manager.dispatch.refresh(
583                        state, context, only_load_props
584                    )
585
586                if populate_existing or state.modified:
587                    if refresh_state and only_load_props:
588                        state._commit(dict_, only_load_props)
589                    else:
590                        state._commit_all(dict_, session_identity_map)
591
592            if post_load:
593                post_load.add_state(state, True)
594
595        else:
596            # partial population routines, for objects that were already
597            # in the Session, but a row matches them; apply eager loaders
598            # on existing objects, etc.
599            unloaded = state.unloaded
600            isnew = state not in context.partials
601
602            if not isnew or unloaded or populators["eager"]:
603                # state is having a partial set of its attributes
604                # refreshed.  Populate those attributes,
605                # and add to the "context.partials" collection.
606
607                to_load = _populate_partial(
608                    context,
609                    row,
610                    state,
611                    dict_,
612                    isnew,
613                    load_path,
614                    unloaded,
615                    populators,
616                )
617
618                if isnew:
619                    if refresh_evt:
620                        state.manager.dispatch.refresh(state, context, to_load)
621
622                    state._commit(dict_, to_load)
623
624            if post_load and context.invoke_all_eagers:
625                post_load.add_state(state, False)
626
627        return instance
628
629    if mapper.polymorphic_map and not _polymorphic_from and not refresh_state:
630        # if we are doing polymorphic, dispatch to a different _instance()
631        # method specific to the subclass mapper
632        _instance = _decorate_polymorphic_switch(
633            _instance,
634            context,
635            mapper,
636            result,
637            path,
638            polymorphic_discriminator,
639            adapter,
640        )
641
642    return _instance
643
644
645def _load_subclass_via_in(context, path, entity):
646    mapper = entity.mapper
647
648    zero_idx = len(mapper.base_mapper.primary_key) == 1
649
650    if entity.is_aliased_class:
651        q, enable_opt, disable_opt = mapper._subclass_load_via_in(entity)
652    else:
653        q, enable_opt, disable_opt = mapper._subclass_load_via_in_mapper
654
655    def do_load(context, path, states, load_only, effective_entity):
656        orig_query = context.query
657
658        q2 = q._with_lazyload_options(
659            (enable_opt,) + orig_query._with_options + (disable_opt,),
660            path.parent,
661            cache_path=path,
662        )
663
664        if orig_query._populate_existing:
665            q2.add_criteria(lambda q: q.populate_existing())
666
667        q2(context.session).params(
668            primary_keys=[
669                state.key[1][0] if zero_idx else state.key[1]
670                for state, load_attrs in states
671            ]
672        ).all()
673
674    return do_load
675
676
677def _populate_full(
678    context,
679    row,
680    state,
681    dict_,
682    isnew,
683    load_path,
684    loaded_instance,
685    populate_existing,
686    populators,
687):
688    if isnew:
689        # first time we are seeing a row with this identity.
690        state.runid = context.runid
691
692        for key, getter in populators["quick"]:
693            dict_[key] = getter(row)
694        if populate_existing:
695            for key, set_callable in populators["expire"]:
696                dict_.pop(key, None)
697                if set_callable:
698                    state.expired_attributes.add(key)
699        else:
700            for key, set_callable in populators["expire"]:
701                if set_callable:
702                    state.expired_attributes.add(key)
703        for key, populator in populators["new"]:
704            populator(state, dict_, row)
705        for key, populator in populators["delayed"]:
706            populator(state, dict_, row)
707    elif load_path != state.load_path:
708        # new load path, e.g. object is present in more than one
709        # column position in a series of rows
710        state.load_path = load_path
711
712        # if we have data, and the data isn't in the dict, OK, let's put
713        # it in.
714        for key, getter in populators["quick"]:
715            if key not in dict_:
716                dict_[key] = getter(row)
717
718        # otherwise treat like an "already seen" row
719        for key, populator in populators["existing"]:
720            populator(state, dict_, row)
721            # TODO:  allow "existing" populator to know this is
722            # a new path for the state:
723            # populator(state, dict_, row, new_path=True)
724
725    else:
726        # have already seen rows with this identity in this same path.
727        for key, populator in populators["existing"]:
728            populator(state, dict_, row)
729
730            # TODO: same path
731            # populator(state, dict_, row, new_path=False)
732
733
734def _populate_partial(
735    context, row, state, dict_, isnew, load_path, unloaded, populators
736):
737
738    if not isnew:
739        to_load = context.partials[state]
740        for key, populator in populators["existing"]:
741            if key in to_load:
742                populator(state, dict_, row)
743    else:
744        to_load = unloaded
745        context.partials[state] = to_load
746
747        for key, getter in populators["quick"]:
748            if key in to_load:
749                dict_[key] = getter(row)
750        for key, set_callable in populators["expire"]:
751            if key in to_load:
752                dict_.pop(key, None)
753                if set_callable:
754                    state.expired_attributes.add(key)
755        for key, populator in populators["new"]:
756            if key in to_load:
757                populator(state, dict_, row)
758        for key, populator in populators["delayed"]:
759            if key in to_load:
760                populator(state, dict_, row)
761    for key, populator in populators["eager"]:
762        if key not in unloaded:
763            populator(state, dict_, row)
764
765    return to_load
766
767
768def _validate_version_id(mapper, state, dict_, row, adapter):
769
770    version_id_col = mapper.version_id_col
771
772    if version_id_col is None:
773        return
774
775    if adapter:
776        version_id_col = adapter.columns[version_id_col]
777
778    if (
779        mapper._get_state_attr_by_column(state, dict_, mapper.version_id_col)
780        != row[version_id_col]
781    ):
782        raise orm_exc.StaleDataError(
783            "Instance '%s' has version id '%s' which "
784            "does not match database-loaded version id '%s'."
785            % (
786                state_str(state),
787                mapper._get_state_attr_by_column(
788                    state, dict_, mapper.version_id_col
789                ),
790                row[version_id_col],
791            )
792        )
793
794
795def _decorate_polymorphic_switch(
796    instance_fn,
797    context,
798    mapper,
799    result,
800    path,
801    polymorphic_discriminator,
802    adapter,
803):
804    if polymorphic_discriminator is not None:
805        polymorphic_on = polymorphic_discriminator
806    else:
807        polymorphic_on = mapper.polymorphic_on
808    if polymorphic_on is None:
809        return instance_fn
810
811    if adapter:
812        polymorphic_on = adapter.columns[polymorphic_on]
813
814    def configure_subclass_mapper(discriminator):
815        try:
816            sub_mapper = mapper.polymorphic_map[discriminator]
817        except KeyError:
818            raise AssertionError(
819                "No such polymorphic_identity %r is defined" % discriminator
820            )
821        else:
822            if sub_mapper is mapper:
823                return None
824
825            return _instance_processor(
826                sub_mapper,
827                context,
828                result,
829                path,
830                adapter,
831                _polymorphic_from=mapper,
832            )
833
834    polymorphic_instances = util.PopulateDict(configure_subclass_mapper)
835
836    def polymorphic_instance(row):
837        discriminator = row[polymorphic_on]
838        if discriminator is not None:
839            _instance = polymorphic_instances[discriminator]
840            if _instance:
841                return _instance(row)
842        return instance_fn(row)
843
844    return polymorphic_instance
845
846
847class PostLoad(object):
848    """Track loaders and states for "post load" operations.
849
850    """
851
852    __slots__ = "loaders", "states", "load_keys"
853
854    def __init__(self):
855        self.loaders = {}
856        self.states = util.OrderedDict()
857        self.load_keys = None
858
859    def add_state(self, state, overwrite):
860        # the states for a polymorphic load here are all shared
861        # within a single PostLoad object among multiple subtypes.
862        # Filtering of callables on a per-subclass basis needs to be done at
863        # the invocation level
864        self.states[state] = overwrite
865
866    def invoke(self, context, path):
867        if not self.states:
868            return
869        path = path_registry.PathRegistry.coerce(path)
870        for token, limit_to_mapper, loader, arg, kw in self.loaders.values():
871            states = [
872                (state, overwrite)
873                for state, overwrite in self.states.items()
874                if state.manager.mapper.isa(limit_to_mapper)
875            ]
876            if states:
877                loader(context, path, states, self.load_keys, *arg, **kw)
878        self.states.clear()
879
880    @classmethod
881    def for_context(cls, context, path, only_load_props):
882        pl = context.post_load_paths.get(path.path)
883        if pl is not None and only_load_props:
884            pl.load_keys = only_load_props
885        return pl
886
887    @classmethod
888    def path_exists(self, context, path, key):
889        return (
890            path.path in context.post_load_paths
891            and key in context.post_load_paths[path.path].loaders
892        )
893
894    @classmethod
895    def callable_for_path(
896        cls, context, path, limit_to_mapper, token, loader_callable, *arg, **kw
897    ):
898        if path.path in context.post_load_paths:
899            pl = context.post_load_paths[path.path]
900        else:
901            pl = context.post_load_paths[path.path] = PostLoad()
902        pl.loaders[token] = (token, limit_to_mapper, loader_callable, arg, kw)
903
904
905def load_scalar_attributes(mapper, state, attribute_names):
906    """initiate a column-based attribute refresh operation."""
907
908    # assert mapper is _state_mapper(state)
909    session = state.session
910    if not session:
911        raise orm_exc.DetachedInstanceError(
912            "Instance %s is not bound to a Session; "
913            "attribute refresh operation cannot proceed" % (state_str(state))
914        )
915
916    has_key = bool(state.key)
917
918    result = False
919
920    # in the case of inheritance, particularly concrete and abstract
921    # concrete inheritance, the class manager might have some keys
922    # of attributes on the superclass that we didn't actually map.
923    # These could be mapped as "concrete, dont load" or could be completely
924    # exluded from the mapping and we know nothing about them.  Filter them
925    # here to prevent them from coming through.
926    if attribute_names:
927        attribute_names = attribute_names.intersection(mapper.attrs.keys())
928
929    if mapper.inherits and not mapper.concrete:
930        # because we are using Core to produce a select() that we
931        # pass to the Query, we aren't calling setup() for mapped
932        # attributes; in 1.0 this means deferred attrs won't get loaded
933        # by default
934        statement = mapper._optimized_get_statement(state, attribute_names)
935        if statement is not None:
936            result = load_on_ident(
937                session.query(mapper)
938                .options(strategy_options.Load(mapper).undefer("*"))
939                .from_statement(statement),
940                None,
941                only_load_props=attribute_names,
942                refresh_state=state,
943            )
944
945    if result is False:
946        if has_key:
947            identity_key = state.key
948        else:
949            # this codepath is rare - only valid when inside a flush, and the
950            # object is becoming persistent but hasn't yet been assigned
951            # an identity_key.
952            # check here to ensure we have the attrs we need.
953            pk_attrs = [
954                mapper._columntoproperty[col].key for col in mapper.primary_key
955            ]
956            if state.expired_attributes.intersection(pk_attrs):
957                raise sa_exc.InvalidRequestError(
958                    "Instance %s cannot be refreshed - it's not "
959                    " persistent and does not "
960                    "contain a full primary key." % state_str(state)
961                )
962            identity_key = mapper._identity_key_from_state(state)
963
964        if (
965            _none_set.issubset(identity_key) and not mapper.allow_partial_pks
966        ) or _none_set.issuperset(identity_key):
967            util.warn_limited(
968                "Instance %s to be refreshed doesn't "
969                "contain a full primary key - can't be refreshed "
970                "(and shouldn't be expired, either).",
971                state_str(state),
972            )
973            return
974
975        result = load_on_ident(
976            session.query(mapper),
977            identity_key,
978            refresh_state=state,
979            only_load_props=attribute_names,
980        )
981
982    # if instance is pending, a refresh operation
983    # may not complete (even if PK attributes are assigned)
984    if has_key and result is None:
985        raise orm_exc.ObjectDeletedError(state)
986