You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1466 lines
48 KiB
Python
1466 lines
48 KiB
Python
# orm/loading.py
|
|
# Copyright (C) 2005-2022 the SQLAlchemy authors and contributors
|
|
# <see AUTHORS file>
|
|
#
|
|
# This module is part of SQLAlchemy and is released under
|
|
# the MIT License: https://www.opensource.org/licenses/mit-license.php
|
|
|
|
"""private module containing functions used to convert database
|
|
rows into object instances and associated state.
|
|
|
|
the functions here are called primarily by Query, Mapper,
|
|
as well as some of the attribute loading strategies.
|
|
|
|
"""
|
|
from __future__ import absolute_import
|
|
|
|
from . import attributes
|
|
from . import exc as orm_exc
|
|
from . import path_registry
|
|
from . import strategy_options
|
|
from .base import _DEFER_FOR_STATE
|
|
from .base import _RAISE_FOR_STATE
|
|
from .base import _SET_DEFERRED_EXPIRED
|
|
from .util import _none_set
|
|
from .util import state_str
|
|
from .. import exc as sa_exc
|
|
from .. import future
|
|
from .. import util
|
|
from ..engine import result_tuple
|
|
from ..engine.result import ChunkedIteratorResult
|
|
from ..engine.result import FrozenResult
|
|
from ..engine.result import SimpleResultMetaData
|
|
from ..sql import util as sql_util
|
|
from ..sql.selectable import LABEL_STYLE_TABLENAME_PLUS_COL
|
|
from ..sql.selectable import SelectState
|
|
|
|
_new_runid = util.counter()
|
|
|
|
|
|
def instances(cursor, context):
|
|
"""Return a :class:`.Result` given an ORM query context.
|
|
|
|
:param cursor: a :class:`.CursorResult`, generated by a statement
|
|
which came from :class:`.ORMCompileState`
|
|
|
|
:param context: a :class:`.QueryContext` object
|
|
|
|
:return: a :class:`.Result` object representing ORM results
|
|
|
|
.. versionchanged:: 1.4 The instances() function now uses
|
|
:class:`.Result` objects and has an all new interface.
|
|
|
|
"""
|
|
|
|
context.runid = _new_runid()
|
|
context.post_load_paths = {}
|
|
|
|
compile_state = context.compile_state
|
|
filtered = compile_state._has_mapper_entities
|
|
single_entity = (
|
|
not context.load_options._only_return_tuples
|
|
and len(compile_state._entities) == 1
|
|
and compile_state._entities[0].supports_single_entity
|
|
)
|
|
|
|
try:
|
|
(process, labels, extra) = list(
|
|
zip(
|
|
*[
|
|
query_entity.row_processor(context, cursor)
|
|
for query_entity in context.compile_state._entities
|
|
]
|
|
)
|
|
)
|
|
|
|
if context.yield_per and (
|
|
context.loaders_require_buffering
|
|
or context.loaders_require_uniquing
|
|
):
|
|
raise sa_exc.InvalidRequestError(
|
|
"Can't use yield_per with eager loaders that require uniquing "
|
|
"or row buffering, e.g. joinedload() against collections "
|
|
"or subqueryload(). Consider the selectinload() strategy "
|
|
"for better flexibility in loading objects."
|
|
)
|
|
|
|
except Exception:
|
|
with util.safe_reraise():
|
|
cursor.close()
|
|
|
|
def _no_unique(entry):
|
|
raise sa_exc.InvalidRequestError(
|
|
"Can't use the ORM yield_per feature in conjunction with unique()"
|
|
)
|
|
|
|
def _not_hashable(datatype):
|
|
def go(obj):
|
|
raise sa_exc.InvalidRequestError(
|
|
"Can't apply uniqueness to row tuple containing value of "
|
|
"type %r; this datatype produces non-hashable values"
|
|
% datatype
|
|
)
|
|
|
|
return go
|
|
|
|
if context.load_options._legacy_uniquing:
|
|
unique_filters = [
|
|
_no_unique
|
|
if context.yield_per
|
|
else id
|
|
if (
|
|
ent.use_id_for_hash
|
|
or ent._non_hashable_value
|
|
or ent._null_column_type
|
|
)
|
|
else None
|
|
for ent in context.compile_state._entities
|
|
]
|
|
else:
|
|
unique_filters = [
|
|
_no_unique
|
|
if context.yield_per
|
|
else _not_hashable(ent.column.type)
|
|
if (not ent.use_id_for_hash and ent._non_hashable_value)
|
|
else id
|
|
if ent.use_id_for_hash
|
|
else None
|
|
for ent in context.compile_state._entities
|
|
]
|
|
|
|
row_metadata = SimpleResultMetaData(
|
|
labels, extra, _unique_filters=unique_filters
|
|
)
|
|
|
|
def chunks(size):
|
|
while True:
|
|
yield_per = size
|
|
|
|
context.partials = {}
|
|
|
|
if yield_per:
|
|
fetch = cursor.fetchmany(yield_per)
|
|
|
|
if not fetch:
|
|
break
|
|
else:
|
|
fetch = cursor._raw_all_rows()
|
|
|
|
if single_entity:
|
|
proc = process[0]
|
|
rows = [proc(row) for row in fetch]
|
|
else:
|
|
rows = [
|
|
tuple([proc(row) for proc in process]) for row in fetch
|
|
]
|
|
|
|
for path, post_load in context.post_load_paths.items():
|
|
post_load.invoke(context, path)
|
|
|
|
yield rows
|
|
|
|
if not yield_per:
|
|
break
|
|
|
|
if context.execution_options.get("prebuffer_rows", False):
|
|
# this is a bit of a hack at the moment.
|
|
# I would rather have some option in the result to pre-buffer
|
|
# internally.
|
|
_prebuffered = list(chunks(None))
|
|
|
|
def chunks(size):
|
|
return iter(_prebuffered)
|
|
|
|
result = ChunkedIteratorResult(
|
|
row_metadata,
|
|
chunks,
|
|
source_supports_scalars=single_entity,
|
|
raw=cursor,
|
|
dynamic_yield_per=cursor.context._is_server_side,
|
|
)
|
|
|
|
# filtered and single_entity are used to indicate to legacy Query that the
|
|
# query has ORM entities, so legacy deduping and scalars should be called
|
|
# on the result.
|
|
result._attributes = result._attributes.union(
|
|
dict(filtered=filtered, is_single_entity=single_entity)
|
|
)
|
|
|
|
# multi_row_eager_loaders OTOH is specific to joinedload.
|
|
if context.compile_state.multi_row_eager_loaders:
|
|
|
|
def require_unique(obj):
|
|
raise sa_exc.InvalidRequestError(
|
|
"The unique() method must be invoked on this Result, "
|
|
"as it contains results that include joined eager loads "
|
|
"against collections"
|
|
)
|
|
|
|
result._unique_filter_state = (None, require_unique)
|
|
|
|
if context.yield_per:
|
|
result.yield_per(context.yield_per)
|
|
|
|
return result
|
|
|
|
|
|
@util.preload_module("sqlalchemy.orm.context")
|
|
def merge_frozen_result(session, statement, frozen_result, load=True):
|
|
"""Merge a :class:`_engine.FrozenResult` back into a :class:`_orm.Session`,
|
|
returning a new :class:`_engine.Result` object with :term:`persistent`
|
|
objects.
|
|
|
|
See the section :ref:`do_orm_execute_re_executing` for an example.
|
|
|
|
.. seealso::
|
|
|
|
:ref:`do_orm_execute_re_executing`
|
|
|
|
:meth:`_engine.Result.freeze`
|
|
|
|
:class:`_engine.FrozenResult`
|
|
|
|
"""
|
|
querycontext = util.preloaded.orm_context
|
|
|
|
if load:
|
|
# flush current contents if we expect to load data
|
|
session._autoflush()
|
|
|
|
ctx = querycontext.ORMSelectCompileState._create_entities_collection(
|
|
statement, legacy=False
|
|
)
|
|
|
|
autoflush = session.autoflush
|
|
try:
|
|
session.autoflush = False
|
|
mapped_entities = [
|
|
i
|
|
for i, e in enumerate(ctx._entities)
|
|
if isinstance(e, querycontext._MapperEntity)
|
|
]
|
|
keys = [ent._label_name for ent in ctx._entities]
|
|
|
|
keyed_tuple = result_tuple(
|
|
keys, [ent._extra_entities for ent in ctx._entities]
|
|
)
|
|
|
|
result = []
|
|
for newrow in frozen_result.rewrite_rows():
|
|
for i in mapped_entities:
|
|
if newrow[i] is not None:
|
|
newrow[i] = session._merge(
|
|
attributes.instance_state(newrow[i]),
|
|
attributes.instance_dict(newrow[i]),
|
|
load=load,
|
|
_recursive={},
|
|
_resolve_conflict_map={},
|
|
)
|
|
|
|
result.append(keyed_tuple(newrow))
|
|
|
|
return frozen_result.with_new_rows(result)
|
|
finally:
|
|
session.autoflush = autoflush
|
|
|
|
|
|
@util.deprecated_20(
|
|
":func:`_orm.merge_result`",
|
|
alternative="The function as well as the method on :class:`_orm.Query` "
|
|
"is superseded by the :func:`_orm.merge_frozen_result` function.",
|
|
becomes_legacy=True,
|
|
)
|
|
@util.preload_module("sqlalchemy.orm.context")
|
|
def merge_result(query, iterator, load=True):
|
|
"""Merge a result into the given :class:`.Query` object's Session.
|
|
|
|
See :meth:`_orm.Query.merge_result` for top-level documentation on this
|
|
function.
|
|
|
|
"""
|
|
|
|
querycontext = util.preloaded.orm_context
|
|
|
|
session = query.session
|
|
if load:
|
|
# flush current contents if we expect to load data
|
|
session._autoflush()
|
|
|
|
# TODO: need test coverage and documentation for the FrozenResult
|
|
# use case.
|
|
if isinstance(iterator, FrozenResult):
|
|
frozen_result = iterator
|
|
iterator = iter(frozen_result.data)
|
|
else:
|
|
frozen_result = None
|
|
|
|
ctx = querycontext.ORMSelectCompileState._create_entities_collection(
|
|
query, legacy=True
|
|
)
|
|
|
|
autoflush = session.autoflush
|
|
try:
|
|
session.autoflush = False
|
|
single_entity = not frozen_result and len(ctx._entities) == 1
|
|
|
|
if single_entity:
|
|
if isinstance(ctx._entities[0], querycontext._MapperEntity):
|
|
result = [
|
|
session._merge(
|
|
attributes.instance_state(instance),
|
|
attributes.instance_dict(instance),
|
|
load=load,
|
|
_recursive={},
|
|
_resolve_conflict_map={},
|
|
)
|
|
for instance in iterator
|
|
]
|
|
else:
|
|
result = list(iterator)
|
|
else:
|
|
mapped_entities = [
|
|
i
|
|
for i, e in enumerate(ctx._entities)
|
|
if isinstance(e, querycontext._MapperEntity)
|
|
]
|
|
result = []
|
|
keys = [ent._label_name for ent in ctx._entities]
|
|
|
|
keyed_tuple = result_tuple(
|
|
keys, [ent._extra_entities for ent in ctx._entities]
|
|
)
|
|
|
|
for row in iterator:
|
|
newrow = list(row)
|
|
for i in mapped_entities:
|
|
if newrow[i] is not None:
|
|
newrow[i] = session._merge(
|
|
attributes.instance_state(newrow[i]),
|
|
attributes.instance_dict(newrow[i]),
|
|
load=load,
|
|
_recursive={},
|
|
_resolve_conflict_map={},
|
|
)
|
|
result.append(keyed_tuple(newrow))
|
|
|
|
if frozen_result:
|
|
return frozen_result.with_data(result)
|
|
else:
|
|
return iter(result)
|
|
finally:
|
|
session.autoflush = autoflush
|
|
|
|
|
|
def get_from_identity(session, mapper, key, passive):
|
|
"""Look up the given key in the given session's identity map,
|
|
check the object for expired state if found.
|
|
|
|
"""
|
|
instance = session.identity_map.get(key)
|
|
if instance is not None:
|
|
|
|
state = attributes.instance_state(instance)
|
|
|
|
if mapper.inherits and not state.mapper.isa(mapper):
|
|
return attributes.PASSIVE_CLASS_MISMATCH
|
|
|
|
# expired - ensure it still exists
|
|
if state.expired:
|
|
if not passive & attributes.SQL_OK:
|
|
# TODO: no coverage here
|
|
return attributes.PASSIVE_NO_RESULT
|
|
elif not passive & attributes.RELATED_OBJECT_OK:
|
|
# this mode is used within a flush and the instance's
|
|
# expired state will be checked soon enough, if necessary.
|
|
# also used by immediateloader for a mutually-dependent
|
|
# o2m->m2m load, :ticket:`6301`
|
|
return instance
|
|
try:
|
|
state._load_expired(state, passive)
|
|
except orm_exc.ObjectDeletedError:
|
|
session._remove_newly_deleted([state])
|
|
return None
|
|
return instance
|
|
else:
|
|
return None
|
|
|
|
|
|
def load_on_ident(
|
|
session,
|
|
statement,
|
|
key,
|
|
load_options=None,
|
|
refresh_state=None,
|
|
with_for_update=None,
|
|
only_load_props=None,
|
|
no_autoflush=False,
|
|
bind_arguments=util.EMPTY_DICT,
|
|
execution_options=util.EMPTY_DICT,
|
|
):
|
|
"""Load the given identity key from the database."""
|
|
if key is not None:
|
|
ident = key[1]
|
|
identity_token = key[2]
|
|
else:
|
|
ident = identity_token = None
|
|
|
|
return load_on_pk_identity(
|
|
session,
|
|
statement,
|
|
ident,
|
|
load_options=load_options,
|
|
refresh_state=refresh_state,
|
|
with_for_update=with_for_update,
|
|
only_load_props=only_load_props,
|
|
identity_token=identity_token,
|
|
no_autoflush=no_autoflush,
|
|
bind_arguments=bind_arguments,
|
|
execution_options=execution_options,
|
|
)
|
|
|
|
|
|
def load_on_pk_identity(
|
|
session,
|
|
statement,
|
|
primary_key_identity,
|
|
load_options=None,
|
|
refresh_state=None,
|
|
with_for_update=None,
|
|
only_load_props=None,
|
|
identity_token=None,
|
|
no_autoflush=False,
|
|
bind_arguments=util.EMPTY_DICT,
|
|
execution_options=util.EMPTY_DICT,
|
|
):
|
|
|
|
"""Load the given primary key identity from the database."""
|
|
|
|
query = statement
|
|
q = query._clone()
|
|
|
|
assert not q._is_lambda_element
|
|
|
|
# TODO: fix these imports ....
|
|
from .context import QueryContext, ORMCompileState
|
|
|
|
if load_options is None:
|
|
load_options = QueryContext.default_load_options
|
|
|
|
if (
|
|
statement._compile_options
|
|
is SelectState.default_select_compile_options
|
|
):
|
|
compile_options = ORMCompileState.default_compile_options
|
|
else:
|
|
compile_options = statement._compile_options
|
|
|
|
if primary_key_identity is not None:
|
|
mapper = query._propagate_attrs["plugin_subject"]
|
|
|
|
(_get_clause, _get_params) = mapper._get_clause
|
|
|
|
# None present in ident - turn those comparisons
|
|
# into "IS NULL"
|
|
if None in primary_key_identity:
|
|
nones = set(
|
|
[
|
|
_get_params[col].key
|
|
for col, value in zip(
|
|
mapper.primary_key, primary_key_identity
|
|
)
|
|
if value is None
|
|
]
|
|
)
|
|
|
|
_get_clause = sql_util.adapt_criterion_to_null(_get_clause, nones)
|
|
|
|
if len(nones) == len(primary_key_identity):
|
|
util.warn(
|
|
"fully NULL primary key identity cannot load any "
|
|
"object. This condition may raise an error in a future "
|
|
"release."
|
|
)
|
|
|
|
q._where_criteria = (
|
|
sql_util._deep_annotate(_get_clause, {"_orm_adapt": True}),
|
|
)
|
|
|
|
params = dict(
|
|
[
|
|
(_get_params[primary_key].key, id_val)
|
|
for id_val, primary_key in zip(
|
|
primary_key_identity, mapper.primary_key
|
|
)
|
|
]
|
|
)
|
|
else:
|
|
params = None
|
|
|
|
if with_for_update is not None:
|
|
version_check = True
|
|
q._for_update_arg = with_for_update
|
|
elif query._for_update_arg is not None:
|
|
version_check = True
|
|
q._for_update_arg = query._for_update_arg
|
|
else:
|
|
version_check = False
|
|
|
|
if refresh_state and refresh_state.load_options:
|
|
compile_options += {"_current_path": refresh_state.load_path.parent}
|
|
q = q.options(*refresh_state.load_options)
|
|
|
|
new_compile_options, load_options = _set_get_options(
|
|
compile_options,
|
|
load_options,
|
|
version_check=version_check,
|
|
only_load_props=only_load_props,
|
|
refresh_state=refresh_state,
|
|
identity_token=identity_token,
|
|
)
|
|
q._compile_options = new_compile_options
|
|
q._order_by = None
|
|
|
|
if no_autoflush:
|
|
load_options += {"_autoflush": False}
|
|
|
|
execution_options = util.EMPTY_DICT.merge_with(
|
|
execution_options, {"_sa_orm_load_options": load_options}
|
|
)
|
|
result = (
|
|
session.execute(
|
|
q,
|
|
params=params,
|
|
execution_options=execution_options,
|
|
bind_arguments=bind_arguments,
|
|
)
|
|
.unique()
|
|
.scalars()
|
|
)
|
|
|
|
try:
|
|
return result.one()
|
|
except orm_exc.NoResultFound:
|
|
return None
|
|
|
|
|
|
def _set_get_options(
|
|
compile_opt,
|
|
load_opt,
|
|
populate_existing=None,
|
|
version_check=None,
|
|
only_load_props=None,
|
|
refresh_state=None,
|
|
identity_token=None,
|
|
):
|
|
|
|
compile_options = {}
|
|
load_options = {}
|
|
if version_check:
|
|
load_options["_version_check"] = version_check
|
|
if populate_existing:
|
|
load_options["_populate_existing"] = populate_existing
|
|
if refresh_state:
|
|
load_options["_refresh_state"] = refresh_state
|
|
compile_options["_for_refresh_state"] = True
|
|
if only_load_props:
|
|
compile_options["_only_load_props"] = frozenset(only_load_props)
|
|
if identity_token:
|
|
load_options["_refresh_identity_token"] = identity_token
|
|
|
|
if load_options:
|
|
load_opt += load_options
|
|
if compile_options:
|
|
compile_opt += compile_options
|
|
|
|
return compile_opt, load_opt
|
|
|
|
|
|
def _setup_entity_query(
|
|
compile_state,
|
|
mapper,
|
|
query_entity,
|
|
path,
|
|
adapter,
|
|
column_collection,
|
|
with_polymorphic=None,
|
|
only_load_props=None,
|
|
polymorphic_discriminator=None,
|
|
**kw
|
|
):
|
|
|
|
if with_polymorphic:
|
|
poly_properties = mapper._iterate_polymorphic_properties(
|
|
with_polymorphic
|
|
)
|
|
else:
|
|
poly_properties = mapper._polymorphic_properties
|
|
|
|
quick_populators = {}
|
|
|
|
path.set(compile_state.attributes, "memoized_setups", quick_populators)
|
|
|
|
# for the lead entities in the path, e.g. not eager loads, and
|
|
# assuming a user-passed aliased class, e.g. not a from_self() or any
|
|
# implicit aliasing, don't add columns to the SELECT that aren't
|
|
# in the thing that's aliased.
|
|
check_for_adapt = adapter and len(path) == 1 and path[-1].is_aliased_class
|
|
|
|
for value in poly_properties:
|
|
if only_load_props and value.key not in only_load_props:
|
|
continue
|
|
|
|
value.setup(
|
|
compile_state,
|
|
query_entity,
|
|
path,
|
|
adapter,
|
|
only_load_props=only_load_props,
|
|
column_collection=column_collection,
|
|
memoized_populators=quick_populators,
|
|
check_for_adapt=check_for_adapt,
|
|
**kw
|
|
)
|
|
|
|
if (
|
|
polymorphic_discriminator is not None
|
|
and polymorphic_discriminator is not mapper.polymorphic_on
|
|
):
|
|
|
|
if adapter:
|
|
pd = adapter.columns[polymorphic_discriminator]
|
|
else:
|
|
pd = polymorphic_discriminator
|
|
column_collection.append(pd)
|
|
|
|
|
|
def _warn_for_runid_changed(state):
|
|
util.warn(
|
|
"Loading context for %s has changed within a load/refresh "
|
|
"handler, suggesting a row refresh operation took place. If this "
|
|
"event handler is expected to be "
|
|
"emitting row refresh operations within an existing load or refresh "
|
|
"operation, set restore_load_context=True when establishing the "
|
|
"listener to ensure the context remains unchanged when the event "
|
|
"handler completes." % (state_str(state),)
|
|
)
|
|
|
|
|
|
def _instance_processor(
|
|
query_entity,
|
|
mapper,
|
|
context,
|
|
result,
|
|
path,
|
|
adapter,
|
|
only_load_props=None,
|
|
refresh_state=None,
|
|
polymorphic_discriminator=None,
|
|
_polymorphic_from=None,
|
|
):
|
|
"""Produce a mapper level row processor callable
|
|
which processes rows into mapped instances."""
|
|
|
|
# note that this method, most of which exists in a closure
|
|
# called _instance(), resists being broken out, as
|
|
# attempts to do so tend to add significant function
|
|
# call overhead. _instance() is the most
|
|
# performance-critical section in the whole ORM.
|
|
|
|
identity_class = mapper._identity_class
|
|
compile_state = context.compile_state
|
|
|
|
# look for "row getter" functions that have been assigned along
|
|
# with the compile state that were cached from a previous load.
|
|
# these are operator.itemgetter() objects that each will extract a
|
|
# particular column from each row.
|
|
|
|
getter_key = ("getters", mapper)
|
|
getters = path.get(compile_state.attributes, getter_key, None)
|
|
|
|
if getters is None:
|
|
# no getters, so go through a list of attributes we are loading for,
|
|
# and the ones that are column based will have already put information
|
|
# for us in another collection "memoized_setups", which represents the
|
|
# output of the LoaderStrategy.setup_query() method. We can just as
|
|
# easily call LoaderStrategy.create_row_processor for each, but by
|
|
# getting it all at once from setup_query we save another method call
|
|
# per attribute.
|
|
props = mapper._prop_set
|
|
if only_load_props is not None:
|
|
props = props.intersection(
|
|
mapper._props[k] for k in only_load_props
|
|
)
|
|
|
|
quick_populators = path.get(
|
|
context.attributes, "memoized_setups", _none_set
|
|
)
|
|
|
|
todo = []
|
|
cached_populators = {
|
|
"new": [],
|
|
"quick": [],
|
|
"deferred": [],
|
|
"expire": [],
|
|
"delayed": [],
|
|
"existing": [],
|
|
"eager": [],
|
|
}
|
|
|
|
if refresh_state is None:
|
|
# we can also get the "primary key" tuple getter function
|
|
pk_cols = mapper.primary_key
|
|
|
|
if adapter:
|
|
pk_cols = [adapter.columns[c] for c in pk_cols]
|
|
primary_key_getter = result._tuple_getter(pk_cols)
|
|
else:
|
|
primary_key_getter = None
|
|
|
|
getters = {
|
|
"cached_populators": cached_populators,
|
|
"todo": todo,
|
|
"primary_key_getter": primary_key_getter,
|
|
}
|
|
for prop in props:
|
|
if prop in quick_populators:
|
|
# this is an inlined path just for column-based attributes.
|
|
col = quick_populators[prop]
|
|
if col is _DEFER_FOR_STATE:
|
|
cached_populators["new"].append(
|
|
(prop.key, prop._deferred_column_loader)
|
|
)
|
|
elif col is _SET_DEFERRED_EXPIRED:
|
|
# note that in this path, we are no longer
|
|
# searching in the result to see if the column might
|
|
# be present in some unexpected way.
|
|
cached_populators["expire"].append((prop.key, False))
|
|
elif col is _RAISE_FOR_STATE:
|
|
cached_populators["new"].append(
|
|
(prop.key, prop._raise_column_loader)
|
|
)
|
|
else:
|
|
getter = None
|
|
if adapter:
|
|
# this logic had been removed for all 1.4 releases
|
|
# up until 1.4.18; the adapter here is particularly
|
|
# the compound eager adapter which isn't accommodated
|
|
# in the quick_populators right now. The "fallback"
|
|
# logic below instead took over in many more cases
|
|
# until issue #6596 was identified.
|
|
|
|
# note there is still an issue where this codepath
|
|
# produces no "getter" for cases where a joined-inh
|
|
# mapping includes a labeled column property, meaning
|
|
# KeyError is caught internally and we fall back to
|
|
# _getter(col), which works anyway. The adapter
|
|
# here for joined inh without any aliasing might not
|
|
# be useful. Tests which see this include
|
|
# test.orm.inheritance.test_basic ->
|
|
# EagerTargetingTest.test_adapt_stringency
|
|
# OptimizedLoadTest.test_column_expression_joined
|
|
# PolymorphicOnNotLocalTest.test_polymorphic_on_column_prop # noqa: E501
|
|
#
|
|
|
|
adapted_col = adapter.columns[col]
|
|
if adapted_col is not None:
|
|
getter = result._getter(adapted_col, False)
|
|
if not getter:
|
|
getter = result._getter(col, False)
|
|
if getter:
|
|
cached_populators["quick"].append((prop.key, getter))
|
|
else:
|
|
# fall back to the ColumnProperty itself, which
|
|
# will iterate through all of its columns
|
|
# to see if one fits
|
|
prop.create_row_processor(
|
|
context,
|
|
query_entity,
|
|
path,
|
|
mapper,
|
|
result,
|
|
adapter,
|
|
cached_populators,
|
|
)
|
|
else:
|
|
# loader strategies like subqueryload, selectinload,
|
|
# joinedload, basically relationships, these need to interact
|
|
# with the context each time to work correctly.
|
|
todo.append(prop)
|
|
|
|
path.set(compile_state.attributes, getter_key, getters)
|
|
|
|
cached_populators = getters["cached_populators"]
|
|
|
|
populators = {key: list(value) for key, value in cached_populators.items()}
|
|
for prop in getters["todo"]:
|
|
prop.create_row_processor(
|
|
context, query_entity, path, mapper, result, adapter, populators
|
|
)
|
|
|
|
propagated_loader_options = context.propagated_loader_options
|
|
load_path = (
|
|
context.compile_state.current_path + path
|
|
if context.compile_state.current_path.path
|
|
else path
|
|
)
|
|
|
|
session_identity_map = context.session.identity_map
|
|
|
|
populate_existing = context.populate_existing or mapper.always_refresh
|
|
load_evt = bool(mapper.class_manager.dispatch.load)
|
|
refresh_evt = bool(mapper.class_manager.dispatch.refresh)
|
|
persistent_evt = bool(context.session.dispatch.loaded_as_persistent)
|
|
if persistent_evt:
|
|
loaded_as_persistent = context.session.dispatch.loaded_as_persistent
|
|
instance_state = attributes.instance_state
|
|
instance_dict = attributes.instance_dict
|
|
session_id = context.session.hash_key
|
|
runid = context.runid
|
|
identity_token = context.identity_token
|
|
|
|
version_check = context.version_check
|
|
if version_check:
|
|
version_id_col = mapper.version_id_col
|
|
if version_id_col is not None:
|
|
if adapter:
|
|
version_id_col = adapter.columns[version_id_col]
|
|
version_id_getter = result._getter(version_id_col)
|
|
else:
|
|
version_id_getter = None
|
|
|
|
if not refresh_state and _polymorphic_from is not None:
|
|
key = ("loader", path.path)
|
|
if key in context.attributes and context.attributes[key].strategy == (
|
|
("selectinload_polymorphic", True),
|
|
):
|
|
selectin_load_via = mapper._should_selectin_load(
|
|
context.attributes[key].local_opts["entities"],
|
|
_polymorphic_from,
|
|
)
|
|
else:
|
|
selectin_load_via = mapper._should_selectin_load(
|
|
None, _polymorphic_from
|
|
)
|
|
|
|
if selectin_load_via and selectin_load_via is not _polymorphic_from:
|
|
# only_load_props goes w/ refresh_state only, and in a refresh
|
|
# we are a single row query for the exact entity; polymorphic
|
|
# loading does not apply
|
|
assert only_load_props is None
|
|
|
|
callable_ = _load_subclass_via_in(context, path, selectin_load_via)
|
|
|
|
PostLoad.callable_for_path(
|
|
context,
|
|
load_path,
|
|
selectin_load_via.mapper,
|
|
selectin_load_via,
|
|
callable_,
|
|
selectin_load_via,
|
|
)
|
|
|
|
post_load = PostLoad.for_context(context, load_path, only_load_props)
|
|
|
|
if refresh_state:
|
|
refresh_identity_key = refresh_state.key
|
|
if refresh_identity_key is None:
|
|
# super-rare condition; a refresh is being called
|
|
# on a non-instance-key instance; this is meant to only
|
|
# occur within a flush()
|
|
refresh_identity_key = mapper._identity_key_from_state(
|
|
refresh_state
|
|
)
|
|
else:
|
|
refresh_identity_key = None
|
|
|
|
primary_key_getter = getters["primary_key_getter"]
|
|
|
|
if mapper.allow_partial_pks:
|
|
is_not_primary_key = _none_set.issuperset
|
|
else:
|
|
is_not_primary_key = _none_set.intersection
|
|
|
|
def _instance(row):
|
|
|
|
# determine the state that we'll be populating
|
|
if refresh_identity_key:
|
|
# fixed state that we're refreshing
|
|
state = refresh_state
|
|
instance = state.obj()
|
|
dict_ = instance_dict(instance)
|
|
isnew = state.runid != runid
|
|
currentload = True
|
|
loaded_instance = False
|
|
else:
|
|
# look at the row, see if that identity is in the
|
|
# session, or we have to create a new one
|
|
identitykey = (
|
|
identity_class,
|
|
primary_key_getter(row),
|
|
identity_token,
|
|
)
|
|
|
|
instance = session_identity_map.get(identitykey)
|
|
|
|
if instance is not None:
|
|
# existing instance
|
|
state = instance_state(instance)
|
|
dict_ = instance_dict(instance)
|
|
|
|
isnew = state.runid != runid
|
|
currentload = not isnew
|
|
loaded_instance = False
|
|
|
|
if version_check and version_id_getter and not currentload:
|
|
_validate_version_id(
|
|
mapper, state, dict_, row, version_id_getter
|
|
)
|
|
|
|
else:
|
|
# create a new instance
|
|
|
|
# check for non-NULL values in the primary key columns,
|
|
# else no entity is returned for the row
|
|
if is_not_primary_key(identitykey[1]):
|
|
return None
|
|
|
|
isnew = True
|
|
currentload = True
|
|
loaded_instance = True
|
|
|
|
instance = mapper.class_manager.new_instance()
|
|
|
|
dict_ = instance_dict(instance)
|
|
state = instance_state(instance)
|
|
state.key = identitykey
|
|
state.identity_token = identity_token
|
|
|
|
# attach instance to session.
|
|
state.session_id = session_id
|
|
session_identity_map._add_unpresent(state, identitykey)
|
|
|
|
effective_populate_existing = populate_existing
|
|
if refresh_state is state:
|
|
effective_populate_existing = True
|
|
|
|
# populate. this looks at whether this state is new
|
|
# for this load or was existing, and whether or not this
|
|
# row is the first row with this identity.
|
|
if currentload or effective_populate_existing:
|
|
# full population routines. Objects here are either
|
|
# just created, or we are doing a populate_existing
|
|
|
|
# be conservative about setting load_path when populate_existing
|
|
# is in effect; want to maintain options from the original
|
|
# load. see test_expire->test_refresh_maintains_deferred_options
|
|
if isnew and (
|
|
propagated_loader_options or not effective_populate_existing
|
|
):
|
|
state.load_options = propagated_loader_options
|
|
state.load_path = load_path
|
|
|
|
_populate_full(
|
|
context,
|
|
row,
|
|
state,
|
|
dict_,
|
|
isnew,
|
|
load_path,
|
|
loaded_instance,
|
|
effective_populate_existing,
|
|
populators,
|
|
)
|
|
|
|
if isnew:
|
|
# state.runid should be equal to context.runid / runid
|
|
# here, however for event checks we are being more conservative
|
|
# and checking against existing run id
|
|
# assert state.runid == runid
|
|
|
|
existing_runid = state.runid
|
|
|
|
if loaded_instance:
|
|
if load_evt:
|
|
state.manager.dispatch.load(state, context)
|
|
if state.runid != existing_runid:
|
|
_warn_for_runid_changed(state)
|
|
if persistent_evt:
|
|
loaded_as_persistent(context.session, state)
|
|
if state.runid != existing_runid:
|
|
_warn_for_runid_changed(state)
|
|
elif refresh_evt:
|
|
state.manager.dispatch.refresh(
|
|
state, context, only_load_props
|
|
)
|
|
if state.runid != runid:
|
|
_warn_for_runid_changed(state)
|
|
|
|
if effective_populate_existing or state.modified:
|
|
if refresh_state and only_load_props:
|
|
state._commit(dict_, only_load_props)
|
|
else:
|
|
state._commit_all(dict_, session_identity_map)
|
|
|
|
if post_load:
|
|
post_load.add_state(state, True)
|
|
|
|
else:
|
|
# partial population routines, for objects that were already
|
|
# in the Session, but a row matches them; apply eager loaders
|
|
# on existing objects, etc.
|
|
unloaded = state.unloaded
|
|
isnew = state not in context.partials
|
|
|
|
if not isnew or unloaded or populators["eager"]:
|
|
# state is having a partial set of its attributes
|
|
# refreshed. Populate those attributes,
|
|
# and add to the "context.partials" collection.
|
|
|
|
to_load = _populate_partial(
|
|
context,
|
|
row,
|
|
state,
|
|
dict_,
|
|
isnew,
|
|
load_path,
|
|
unloaded,
|
|
populators,
|
|
)
|
|
|
|
if isnew:
|
|
if refresh_evt:
|
|
existing_runid = state.runid
|
|
state.manager.dispatch.refresh(state, context, to_load)
|
|
if state.runid != existing_runid:
|
|
_warn_for_runid_changed(state)
|
|
|
|
state._commit(dict_, to_load)
|
|
|
|
if post_load and context.invoke_all_eagers:
|
|
post_load.add_state(state, False)
|
|
|
|
return instance
|
|
|
|
if mapper.polymorphic_map and not _polymorphic_from and not refresh_state:
|
|
# if we are doing polymorphic, dispatch to a different _instance()
|
|
# method specific to the subclass mapper
|
|
def ensure_no_pk(row):
|
|
identitykey = (
|
|
identity_class,
|
|
primary_key_getter(row),
|
|
identity_token,
|
|
)
|
|
if not is_not_primary_key(identitykey[1]):
|
|
return identitykey
|
|
else:
|
|
return None
|
|
|
|
_instance = _decorate_polymorphic_switch(
|
|
_instance,
|
|
context,
|
|
query_entity,
|
|
mapper,
|
|
result,
|
|
path,
|
|
polymorphic_discriminator,
|
|
adapter,
|
|
ensure_no_pk,
|
|
)
|
|
|
|
return _instance
|
|
|
|
|
|
def _load_subclass_via_in(context, path, entity):
|
|
mapper = entity.mapper
|
|
|
|
zero_idx = len(mapper.base_mapper.primary_key) == 1
|
|
|
|
if entity.is_aliased_class:
|
|
q, enable_opt, disable_opt = mapper._subclass_load_via_in(entity)
|
|
else:
|
|
q, enable_opt, disable_opt = mapper._subclass_load_via_in_mapper
|
|
|
|
def do_load(context, path, states, load_only, effective_entity):
|
|
orig_query = context.query
|
|
|
|
options = (enable_opt,) + orig_query._with_options + (disable_opt,)
|
|
q2 = q.options(*options)
|
|
|
|
q2._compile_options = context.compile_state.default_compile_options
|
|
q2._compile_options += {"_current_path": path.parent}
|
|
|
|
if context.populate_existing:
|
|
q2 = q2.execution_options(populate_existing=True)
|
|
|
|
context.session.execute(
|
|
q2,
|
|
dict(
|
|
primary_keys=[
|
|
state.key[1][0] if zero_idx else state.key[1]
|
|
for state, load_attrs in states
|
|
]
|
|
),
|
|
).unique().scalars().all()
|
|
|
|
return do_load
|
|
|
|
|
|
def _populate_full(
|
|
context,
|
|
row,
|
|
state,
|
|
dict_,
|
|
isnew,
|
|
load_path,
|
|
loaded_instance,
|
|
populate_existing,
|
|
populators,
|
|
):
|
|
if isnew:
|
|
# first time we are seeing a row with this identity.
|
|
state.runid = context.runid
|
|
|
|
for key, getter in populators["quick"]:
|
|
dict_[key] = getter(row)
|
|
if populate_existing:
|
|
for key, set_callable in populators["expire"]:
|
|
dict_.pop(key, None)
|
|
if set_callable:
|
|
state.expired_attributes.add(key)
|
|
else:
|
|
for key, set_callable in populators["expire"]:
|
|
if set_callable:
|
|
state.expired_attributes.add(key)
|
|
|
|
for key, populator in populators["new"]:
|
|
populator(state, dict_, row)
|
|
for key, populator in populators["delayed"]:
|
|
populator(state, dict_, row)
|
|
elif load_path != state.load_path:
|
|
# new load path, e.g. object is present in more than one
|
|
# column position in a series of rows
|
|
state.load_path = load_path
|
|
|
|
# if we have data, and the data isn't in the dict, OK, let's put
|
|
# it in.
|
|
for key, getter in populators["quick"]:
|
|
if key not in dict_:
|
|
dict_[key] = getter(row)
|
|
|
|
# otherwise treat like an "already seen" row
|
|
for key, populator in populators["existing"]:
|
|
populator(state, dict_, row)
|
|
# TODO: allow "existing" populator to know this is
|
|
# a new path for the state:
|
|
# populator(state, dict_, row, new_path=True)
|
|
|
|
else:
|
|
# have already seen rows with this identity in this same path.
|
|
for key, populator in populators["existing"]:
|
|
populator(state, dict_, row)
|
|
|
|
# TODO: same path
|
|
# populator(state, dict_, row, new_path=False)
|
|
|
|
|
|
def _populate_partial(
|
|
context, row, state, dict_, isnew, load_path, unloaded, populators
|
|
):
|
|
|
|
if not isnew:
|
|
to_load = context.partials[state]
|
|
for key, populator in populators["existing"]:
|
|
if key in to_load:
|
|
populator(state, dict_, row)
|
|
else:
|
|
to_load = unloaded
|
|
context.partials[state] = to_load
|
|
|
|
for key, getter in populators["quick"]:
|
|
if key in to_load:
|
|
dict_[key] = getter(row)
|
|
for key, set_callable in populators["expire"]:
|
|
if key in to_load:
|
|
dict_.pop(key, None)
|
|
if set_callable:
|
|
state.expired_attributes.add(key)
|
|
for key, populator in populators["new"]:
|
|
if key in to_load:
|
|
populator(state, dict_, row)
|
|
for key, populator in populators["delayed"]:
|
|
if key in to_load:
|
|
populator(state, dict_, row)
|
|
for key, populator in populators["eager"]:
|
|
if key not in unloaded:
|
|
populator(state, dict_, row)
|
|
|
|
return to_load
|
|
|
|
|
|
def _validate_version_id(mapper, state, dict_, row, getter):
|
|
|
|
if mapper._get_state_attr_by_column(
|
|
state, dict_, mapper.version_id_col
|
|
) != getter(row):
|
|
raise orm_exc.StaleDataError(
|
|
"Instance '%s' has version id '%s' which "
|
|
"does not match database-loaded version id '%s'."
|
|
% (
|
|
state_str(state),
|
|
mapper._get_state_attr_by_column(
|
|
state, dict_, mapper.version_id_col
|
|
),
|
|
getter(row),
|
|
)
|
|
)
|
|
|
|
|
|
def _decorate_polymorphic_switch(
|
|
instance_fn,
|
|
context,
|
|
query_entity,
|
|
mapper,
|
|
result,
|
|
path,
|
|
polymorphic_discriminator,
|
|
adapter,
|
|
ensure_no_pk,
|
|
):
|
|
if polymorphic_discriminator is not None:
|
|
polymorphic_on = polymorphic_discriminator
|
|
else:
|
|
polymorphic_on = mapper.polymorphic_on
|
|
if polymorphic_on is None:
|
|
return instance_fn
|
|
|
|
if adapter:
|
|
polymorphic_on = adapter.columns[polymorphic_on]
|
|
|
|
def configure_subclass_mapper(discriminator):
|
|
try:
|
|
sub_mapper = mapper.polymorphic_map[discriminator]
|
|
except KeyError:
|
|
raise AssertionError(
|
|
"No such polymorphic_identity %r is defined" % discriminator
|
|
)
|
|
else:
|
|
if sub_mapper is mapper:
|
|
return None
|
|
elif not sub_mapper.isa(mapper):
|
|
return False
|
|
|
|
return _instance_processor(
|
|
query_entity,
|
|
sub_mapper,
|
|
context,
|
|
result,
|
|
path,
|
|
adapter,
|
|
_polymorphic_from=mapper,
|
|
)
|
|
|
|
polymorphic_instances = util.PopulateDict(configure_subclass_mapper)
|
|
|
|
getter = result._getter(polymorphic_on)
|
|
|
|
def polymorphic_instance(row):
|
|
discriminator = getter(row)
|
|
if discriminator is not None:
|
|
_instance = polymorphic_instances[discriminator]
|
|
if _instance:
|
|
return _instance(row)
|
|
elif _instance is False:
|
|
identitykey = ensure_no_pk(row)
|
|
|
|
if identitykey:
|
|
raise sa_exc.InvalidRequestError(
|
|
"Row with identity key %s can't be loaded into an "
|
|
"object; the polymorphic discriminator column '%s' "
|
|
"refers to %s, which is not a sub-mapper of "
|
|
"the requested %s"
|
|
% (
|
|
identitykey,
|
|
polymorphic_on,
|
|
mapper.polymorphic_map[discriminator],
|
|
mapper,
|
|
)
|
|
)
|
|
else:
|
|
return None
|
|
else:
|
|
return instance_fn(row)
|
|
else:
|
|
identitykey = ensure_no_pk(row)
|
|
|
|
if identitykey:
|
|
raise sa_exc.InvalidRequestError(
|
|
"Row with identity key %s can't be loaded into an "
|
|
"object; the polymorphic discriminator column '%s' is "
|
|
"NULL" % (identitykey, polymorphic_on)
|
|
)
|
|
else:
|
|
return None
|
|
|
|
return polymorphic_instance
|
|
|
|
|
|
class PostLoad(object):
|
|
"""Track loaders and states for "post load" operations."""
|
|
|
|
__slots__ = "loaders", "states", "load_keys"
|
|
|
|
def __init__(self):
|
|
self.loaders = {}
|
|
self.states = util.OrderedDict()
|
|
self.load_keys = None
|
|
|
|
def add_state(self, state, overwrite):
|
|
# the states for a polymorphic load here are all shared
|
|
# within a single PostLoad object among multiple subtypes.
|
|
# Filtering of callables on a per-subclass basis needs to be done at
|
|
# the invocation level
|
|
self.states[state] = overwrite
|
|
|
|
def invoke(self, context, path):
|
|
if not self.states:
|
|
return
|
|
path = path_registry.PathRegistry.coerce(path)
|
|
for token, limit_to_mapper, loader, arg, kw in self.loaders.values():
|
|
states = [
|
|
(state, overwrite)
|
|
for state, overwrite in self.states.items()
|
|
if state.manager.mapper.isa(limit_to_mapper)
|
|
]
|
|
if states:
|
|
loader(context, path, states, self.load_keys, *arg, **kw)
|
|
self.states.clear()
|
|
|
|
@classmethod
|
|
def for_context(cls, context, path, only_load_props):
|
|
pl = context.post_load_paths.get(path.path)
|
|
if pl is not None and only_load_props:
|
|
pl.load_keys = only_load_props
|
|
return pl
|
|
|
|
@classmethod
|
|
def path_exists(self, context, path, key):
|
|
return (
|
|
path.path in context.post_load_paths
|
|
and key in context.post_load_paths[path.path].loaders
|
|
)
|
|
|
|
@classmethod
|
|
def callable_for_path(
|
|
cls, context, path, limit_to_mapper, token, loader_callable, *arg, **kw
|
|
):
|
|
if path.path in context.post_load_paths:
|
|
pl = context.post_load_paths[path.path]
|
|
else:
|
|
pl = context.post_load_paths[path.path] = PostLoad()
|
|
pl.loaders[token] = (token, limit_to_mapper, loader_callable, arg, kw)
|
|
|
|
|
|
def load_scalar_attributes(mapper, state, attribute_names, passive):
|
|
"""initiate a column-based attribute refresh operation."""
|
|
|
|
# assert mapper is _state_mapper(state)
|
|
session = state.session
|
|
if not session:
|
|
raise orm_exc.DetachedInstanceError(
|
|
"Instance %s is not bound to a Session; "
|
|
"attribute refresh operation cannot proceed" % (state_str(state))
|
|
)
|
|
|
|
has_key = bool(state.key)
|
|
|
|
result = False
|
|
|
|
no_autoflush = (
|
|
bool(passive & attributes.NO_AUTOFLUSH) or state.session.autocommit
|
|
)
|
|
|
|
# in the case of inheritance, particularly concrete and abstract
|
|
# concrete inheritance, the class manager might have some keys
|
|
# of attributes on the superclass that we didn't actually map.
|
|
# These could be mapped as "concrete, don't load" or could be completely
|
|
# excluded from the mapping and we know nothing about them. Filter them
|
|
# here to prevent them from coming through.
|
|
if attribute_names:
|
|
attribute_names = attribute_names.intersection(mapper.attrs.keys())
|
|
|
|
if mapper.inherits and not mapper.concrete:
|
|
# because we are using Core to produce a select() that we
|
|
# pass to the Query, we aren't calling setup() for mapped
|
|
# attributes; in 1.0 this means deferred attrs won't get loaded
|
|
# by default
|
|
statement = mapper._optimized_get_statement(state, attribute_names)
|
|
if statement is not None:
|
|
# this was previously aliased(mapper, statement), however,
|
|
# statement is a select() and Query's coercion now raises for this
|
|
# since you can't "select" from a "SELECT" statement. only
|
|
# from_statement() allows this.
|
|
# note: using from_statement() here means there is an adaption
|
|
# with adapt_on_names set up. the other option is to make the
|
|
# aliased() against a subquery which affects the SQL.
|
|
|
|
from .query import FromStatement
|
|
|
|
stmt = FromStatement(mapper, statement).options(
|
|
strategy_options.Load(mapper).undefer("*")
|
|
)
|
|
|
|
result = load_on_ident(
|
|
session,
|
|
stmt,
|
|
None,
|
|
only_load_props=attribute_names,
|
|
refresh_state=state,
|
|
no_autoflush=no_autoflush,
|
|
)
|
|
|
|
if result is False:
|
|
if has_key:
|
|
identity_key = state.key
|
|
else:
|
|
# this codepath is rare - only valid when inside a flush, and the
|
|
# object is becoming persistent but hasn't yet been assigned
|
|
# an identity_key.
|
|
# check here to ensure we have the attrs we need.
|
|
pk_attrs = [
|
|
mapper._columntoproperty[col].key for col in mapper.primary_key
|
|
]
|
|
if state.expired_attributes.intersection(pk_attrs):
|
|
raise sa_exc.InvalidRequestError(
|
|
"Instance %s cannot be refreshed - it's not "
|
|
" persistent and does not "
|
|
"contain a full primary key." % state_str(state)
|
|
)
|
|
identity_key = mapper._identity_key_from_state(state)
|
|
|
|
if (
|
|
_none_set.issubset(identity_key) and not mapper.allow_partial_pks
|
|
) or _none_set.issuperset(identity_key):
|
|
util.warn_limited(
|
|
"Instance %s to be refreshed doesn't "
|
|
"contain a full primary key - can't be refreshed "
|
|
"(and shouldn't be expired, either).",
|
|
state_str(state),
|
|
)
|
|
return
|
|
|
|
result = load_on_ident(
|
|
session,
|
|
future.select(mapper).set_label_style(
|
|
LABEL_STYLE_TABLENAME_PLUS_COL
|
|
),
|
|
identity_key,
|
|
refresh_state=state,
|
|
only_load_props=attribute_names,
|
|
no_autoflush=no_autoflush,
|
|
)
|
|
|
|
# if instance is pending, a refresh operation
|
|
# may not complete (even if PK attributes are assigned)
|
|
if has_key and result is None:
|
|
raise orm_exc.ObjectDeletedError(state)
|