summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authormike bayer <mike_mp@zzzcomputing.com>2017-12-26 13:04:52 -0500
committerGerrit Code Review <gerrit@ci.zzzcomputing.com>2017-12-26 13:04:52 -0500
commitfb964539b84152f30f46837713495562a833d64d (patch)
tree9f156554215122a96e19efa273b90f8619922989 /lib
parentc7f698e9670d8353bdb6be7d12900528b99c95c2 (diff)
parent50d9f1687a6e0c3ce9b62fe98b76b25af7b20c11 (diff)
downloadsqlalchemy-fb964539b84152f30f46837713495562a833d64d.tar.gz
Merge "Add an identity_token to the identity key"
Diffstat (limited to 'lib')
-rw-r--r--lib/sqlalchemy/ext/horizontal_shard.py34
-rw-r--r--lib/sqlalchemy/orm/loading.py5
-rw-r--r--lib/sqlalchemy/orm/mapper.py37
-rw-r--r--lib/sqlalchemy/orm/persistence.py2
-rw-r--r--lib/sqlalchemy/orm/query.py11
-rw-r--r--lib/sqlalchemy/orm/state.py34
-rw-r--r--lib/sqlalchemy/orm/util.py50
7 files changed, 112 insertions, 61 deletions
diff --git a/lib/sqlalchemy/ext/horizontal_shard.py b/lib/sqlalchemy/ext/horizontal_shard.py
index 8902ae606..c5cf98b40 100644
--- a/lib/sqlalchemy/ext/horizontal_shard.py
+++ b/lib/sqlalchemy/ext/horizontal_shard.py
@@ -15,6 +15,7 @@ the source distribution.
"""
+from .. import inspect
from .. import util
from ..orm.session import Session
from ..orm.query import Query
@@ -42,7 +43,7 @@ class ShardedQuery(Query):
def _execute_and_instances(self, context):
def iter_for_shard(shard_id):
- context.attributes['shard_id'] = shard_id
+ context.attributes['shard_id'] = context.identity_token = shard_id
result = self._connection_from_session(
mapper=self._mapper_zero(),
shard_id=shard_id).execute(
@@ -62,6 +63,9 @@ class ShardedQuery(Query):
return iter(partial)
def _get_impl(self, ident, fallback_fn):
+ # TODO: the "ident" here should be getting the identity token
+ # which indicates that this area can likely be simplified, as the
+ # token will fall through into _execute_and_instances
def _fallback(query, ident):
if self._shard_id is not None:
return fallback_fn(self, ident)
@@ -75,7 +79,13 @@ class ShardedQuery(Query):
else:
return None
- return super(ShardedQuery, self)._get_impl(ident, _fallback)
+ if self._shard_id is not None:
+ identity_token = self._shard_id
+ else:
+ identity_token = None
+
+ return super(ShardedQuery, self)._get_impl(
+ ident, _fallback, identity_token=identity_token)
class ShardedSession(Session):
@@ -112,9 +122,24 @@ class ShardedSession(Session):
for k in shards:
self.bind_shard(k, shards[k])
+ def _choose_shard_and_assign(self, mapper, instance, **kw):
+ if instance is not None:
+ state = inspect(instance)
+ if state.key:
+ token = state.key[2]
+ assert token is not None
+ return token
+ elif state.identity_token:
+ return state.identity_token
+
+ shard_id = self.shard_chooser(mapper, instance, **kw)
+ if instance is not None:
+ state.identity_token = shard_id
+ return shard_id
+
def connection(self, mapper=None, instance=None, shard_id=None, **kwargs):
if shard_id is None:
- shard_id = self.shard_chooser(mapper, instance)
+ shard_id = self._choose_shard_and_assign(mapper, instance)
if self.transaction is not None:
return self.transaction.connection(mapper, shard_id=shard_id)
@@ -128,7 +153,8 @@ class ShardedSession(Session):
def get_bind(self, mapper, shard_id=None,
instance=None, clause=None, **kw):
if shard_id is None:
- shard_id = self.shard_chooser(mapper, instance, clause=clause)
+ shard_id = self._choose_shard_and_assign(
+ mapper, instance, clause=clause)
return self.__binds[shard_id]
def bind_shard(self, shard_id, bind):
diff --git a/lib/sqlalchemy/orm/loading.py b/lib/sqlalchemy/orm/loading.py
index a25a1422d..a23cafac2 100644
--- a/lib/sqlalchemy/orm/loading.py
+++ b/lib/sqlalchemy/orm/loading.py
@@ -369,6 +369,7 @@ def _instance_processor(
session_id = context.session.hash_key
version_check = context.version_check
runid = context.runid
+ identity_token = context.identity_token
if not refresh_state and _polymorphic_from is not None:
key = ('loader', path.path)
@@ -430,7 +431,8 @@ def _instance_processor(
# session, or we have to create a new one
identitykey = (
identity_class,
- tuple([row[column] for column in pk_cols])
+ tuple([row[column] for column in pk_cols]),
+ identity_token
)
instance = session_identity_map.get(identitykey)
@@ -464,6 +466,7 @@ def _instance_processor(
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
diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py
index 31a93f42e..8317c914b 100644
--- a/lib/sqlalchemy/orm/mapper.py
+++ b/lib/sqlalchemy/orm/mapper.py
@@ -2506,7 +2506,7 @@ class Mapper(InspectionAttr):
else:
return True
- def identity_key_from_row(self, row, adapter=None):
+ def identity_key_from_row(self, row, identity_token=None, adapter=None):
"""Return an identity-map key for use in storing/retrieving an
item from the identity map.
@@ -2522,16 +2522,16 @@ class Mapper(InspectionAttr):
pk_cols = [adapter.columns[c] for c in pk_cols]
return self._identity_class, \
- tuple(row[column] for column in pk_cols)
+ tuple(row[column] for column in pk_cols), identity_token
- def identity_key_from_primary_key(self, primary_key):
+ def identity_key_from_primary_key(self, primary_key, identity_token=None):
"""Return an identity-map key for use in storing/retrieving an
item from an identity map.
:param primary_key: A list of values indicating the identifier.
"""
- return self._identity_class, tuple(primary_key)
+ return self._identity_class, tuple(primary_key), identity_token
def identity_key_from_instance(self, instance):
"""Return the identity key for the given instance, based on
@@ -2546,17 +2546,18 @@ class Mapper(InspectionAttr):
attribute name `key`.
"""
- return self.identity_key_from_primary_key(
- self.primary_key_from_instance(instance))
+ state = attributes.instance_state(instance)
+ return self._identity_key_from_state(state, attributes.PASSIVE_OFF)
- def _identity_key_from_state(self, state):
+ def _identity_key_from_state(
+ self, state, passive=attributes.PASSIVE_RETURN_NEVER_SET):
dict_ = state.dict
manager = state.manager
return self._identity_class, tuple([
- manager[self._columntoproperty[col].key].
- impl.get(state, dict_, attributes.PASSIVE_RETURN_NEVER_SET)
- for col in self.primary_key
- ])
+ manager[prop.key].
+ impl.get(state, dict_, passive)
+ for prop in self._identity_key_props
+ ]), state.identity_token
def primary_key_from_instance(self, instance):
"""Return the list of primary key values for the given
@@ -2569,17 +2570,9 @@ class Mapper(InspectionAttr):
"""
state = attributes.instance_state(instance)
- return self._primary_key_from_state(state, attributes.PASSIVE_OFF)
-
- def _primary_key_from_state(
- self, state, passive=attributes.PASSIVE_RETURN_NEVER_SET):
- dict_ = state.dict
- manager = state.manager
- return [
- manager[prop.key].
- impl.get(state, dict_, passive)
- for prop in self._identity_key_props
- ]
+ identity_key = self._identity_key_from_state(
+ state, attributes.PASSIVE_OFF)
+ return identity_key[1]
@_memoized_configured_property
def _identity_key_props(self):
diff --git a/lib/sqlalchemy/orm/persistence.py b/lib/sqlalchemy/orm/persistence.py
index dfa05a85e..942ac2b24 100644
--- a/lib/sqlalchemy/orm/persistence.py
+++ b/lib/sqlalchemy/orm/persistence.py
@@ -1402,7 +1402,7 @@ class BulkEvaluate(BulkUD):
# TODO: detect when the where clause is a trivial primary key match
self.matched_objects = [
- obj for (cls, pk), obj in
+ obj for (cls, pk, identity_token), obj in
query.session.identity_map.items()
if issubclass(cls, target_cls) and
eval_condition(obj)]
diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py
index 209bb6d6a..8668b312b 100644
--- a/lib/sqlalchemy/orm/query.py
+++ b/lib/sqlalchemy/orm/query.py
@@ -867,9 +867,10 @@ class Query(object):
:return: The object instance, or ``None``.
"""
- return self._get_impl(ident, loading.load_on_ident)
+ return self._get_impl(
+ ident, loading.load_on_ident)
- def _get_impl(self, ident, fallback_fn):
+ def _get_impl(self, ident, fallback_fn, identity_token=None):
# convert composite types to individual args
if hasattr(ident, '__composite_values__'):
ident = ident.__composite_values__()
@@ -884,7 +885,8 @@ class Query(object):
"primary key for query.get(); primary key columns are %s" %
','.join("'%s'" % c for c in mapper.primary_key))
- key = mapper.identity_key_from_primary_key(ident)
+ key = mapper.identity_key_from_primary_key(
+ ident, identity_token=identity_token)
if not self._populate_existing and \
not mapper.always_refresh and \
@@ -4127,7 +4129,7 @@ class QueryContext(object):
'eager_joins', 'create_eager_joins', 'propagate_options',
'attributes', 'statement', 'from_clause', 'whereclause',
'order_by', 'labels', '_for_update_arg', 'runid', 'partials',
- 'post_load_paths'
+ 'post_load_paths', 'identity_token'
)
def __init__(self, query):
@@ -4164,6 +4166,7 @@ class QueryContext(object):
self.propagate_options = set(o for o in query._with_options if
o.propagate_to_loaders)
self.attributes = query._attributes.copy()
+ self.identity_token = None
class AliasOption(interfaces.MapperOption):
diff --git a/lib/sqlalchemy/orm/state.py b/lib/sqlalchemy/orm/state.py
index 4964c22e6..04ccf7e3c 100644
--- a/lib/sqlalchemy/orm/state.py
+++ b/lib/sqlalchemy/orm/state.py
@@ -63,6 +63,7 @@ class InstanceState(interfaces.InspectionAttr):
_load_pending = False
_orphaned_outside_of_session = False
is_instance = True
+ identity_token = None
callables = ()
"""A namespace where a per-state loader callable can be associated.
@@ -462,21 +463,34 @@ class InstanceState(interfaces.InspectionAttr):
if 'callables' in state_dict:
self.callables = state_dict['callables']
- try:
- self.expired_attributes = state_dict['expired_attributes']
- except KeyError:
- self.expired_attributes = set()
- # 0.9 and earlier compat
- for k in list(self.callables):
- if self.callables[k] is self:
- self.expired_attributes.add(k)
- del self.callables[k]
+ try:
+ self.expired_attributes = state_dict['expired_attributes']
+ except KeyError:
+ self.expired_attributes = set()
+ # 0.9 and earlier compat
+ for k in list(self.callables):
+ if self.callables[k] is self:
+ self.expired_attributes.add(k)
+ del self.callables[k]
+ else:
+ if 'expired_attributes' in state_dict:
+ self.expired_attributes = state_dict['expired_attributes']
+ else:
+ self.expired_attributes = set()
self.__dict__.update([
(k, state_dict[k]) for k in (
- 'key', 'load_options',
+ 'key', 'load_options'
) if k in state_dict
])
+ if self.key:
+ try:
+ self.identity_token = self.key[2]
+ except IndexError:
+ # 1.1 and earlier compat before identity_token
+ assert len(self.key) == 2
+ self.key = self.key + (None, )
+ self.identity_token = None
if 'load_path' in state_dict:
self.load_path = PathRegistry.\
diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py
index 4267b79fb..ad2a74a34 100644
--- a/lib/sqlalchemy/orm/util.py
+++ b/lib/sqlalchemy/orm/util.py
@@ -214,7 +214,7 @@ def identity_key(*args, **kwargs):
This function has several call styles:
- * ``identity_key(class, ident)``
+ * ``identity_key(class, ident, identity_token=token)``
This form receives a mapped class and a primary key scalar or
tuple as an argument.
@@ -222,10 +222,13 @@ def identity_key(*args, **kwargs):
E.g.::
>>> identity_key(MyClass, (1, 2))
- (<class '__main__.MyClass'>, (1, 2))
+ (<class '__main__.MyClass'>, (1, 2), None)
:param class: mapped class (must be a positional argument)
:param ident: primary key, may be a scalar or tuple argument.
+ ;param identity_token: optional identity token
+
+ .. versionadded:: 1.2 added identity_token
* ``identity_key(instance=instance)``
@@ -239,7 +242,7 @@ def identity_key(*args, **kwargs):
>>> instance = MyClass(1, 2)
>>> identity_key(instance=instance)
- (<class '__main__.MyClass'>, (1, 2))
+ (<class '__main__.MyClass'>, (1, 2), None)
In this form, the given instance is ultimately run though
:meth:`.Mapper.identity_key_from_instance`, which will have the
@@ -248,7 +251,7 @@ def identity_key(*args, **kwargs):
:param instance: object instance (must be given as a keyword arg)
- * ``identity_key(class, row=row)``
+ * ``identity_key(class, row=row, identity_token=token)``
This form is similar to the class/tuple form, except is passed a
database result row as a :class:`.RowProxy` object.
@@ -258,41 +261,50 @@ def identity_key(*args, **kwargs):
>>> row = engine.execute("select * from table where a=1 and b=2").\
first()
>>> identity_key(MyClass, row=row)
- (<class '__main__.MyClass'>, (1, 2))
+ (<class '__main__.MyClass'>, (1, 2), None)
:param class: mapped class (must be a positional argument)
:param row: :class:`.RowProxy` row returned by a :class:`.ResultProxy`
(must be given as a keyword arg)
+ ;param identity_token: optional identity token
+
+ .. versionadded:: 1.2 added identity_token
"""
if args:
- if len(args) == 1:
+ row = None
+ largs = len(args)
+ if largs == 1:
class_ = args[0]
try:
row = kwargs.pop("row")
except KeyError:
ident = kwargs.pop("ident")
- elif len(args) == 2:
- class_, ident = args
- elif len(args) == 3:
+ elif largs in (2, 3):
class_, ident = args
else:
raise sa_exc.ArgumentError(
"expected up to three positional arguments, "
- "got %s" % len(args))
+ "got %s" % largs)
+
+ identity_token = kwargs.pop("identity_token", None)
if kwargs:
raise sa_exc.ArgumentError("unknown keyword arguments: %s"
% ", ".join(kwargs))
mapper = class_mapper(class_)
- if "ident" in locals():
- return mapper.identity_key_from_primary_key(util.to_list(ident))
- return mapper.identity_key_from_row(row)
- instance = kwargs.pop("instance")
- if kwargs:
- raise sa_exc.ArgumentError("unknown keyword arguments: %s"
- % ", ".join(kwargs.keys))
- mapper = object_mapper(instance)
- return mapper.identity_key_from_instance(instance)
+ if row is None:
+ return mapper.identity_key_from_primary_key(
+ util.to_list(ident), identity_token=identity_token)
+ else:
+ return mapper.identity_key_from_row(
+ row, identity_token=identity_token)
+ else:
+ instance = kwargs.pop("instance")
+ if kwargs:
+ raise sa_exc.ArgumentError("unknown keyword arguments: %s"
+ % ", ".join(kwargs.keys))
+ mapper = object_mapper(instance)
+ return mapper.identity_key_from_instance(instance)
class ORMAdapter(sql_util.ColumnAdapter):