From: Mike Bayer Date: Mon, 11 Apr 2011 00:22:11 +0000 (-0400) Subject: - Added checks inside the UOW to detect the unusual X-Git-Tag: rel_0_7b4~21 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=1da499a838cb3cc50c98f7b32de6d35a09f00fe6;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - Added checks inside the UOW to detect the unusual condition of being asked to UPDATE or DELETE on a primary key value that contains NULL in it. [ticket:2127] - Some refinements to attribute history. More changes are pending possibly in 0.8, but for now history has been modified such that scalar history doesn't have a "side effect" of populating None for a non-present value. This allows a slightly better ability to distinguish between a None set and no actual change, affects [ticket:2127] as well. - rewriting the history tests in test_attributes to be individual per operation/assertion. its a huge job so this is partial for the moment. --- diff --git a/CHANGES b/CHANGES index 88347f066e..0d343df81a 100644 --- a/CHANGES +++ b/CHANGES @@ -54,6 +54,20 @@ CHANGES test case + patch. [ticket:2123] (also in 0.6.7). + - Added checks inside the UOW to detect the unusual + condition of being asked to UPDATE or DELETE + on a primary key value that contains NULL + in it. [ticket:2127] + + - Some refinements to attribute history. More + changes are pending possibly in 0.8, but + for now history has been modified such that + scalar history doesn't have a "side effect" + of populating None for a non-present value. + This allows a slightly better ability to + distinguish between a None set and no actual + change, affects [ticket:2127] as well. + - sql - Restored the "catchall" constructor on the base TypeEngine class, with a deprecation warning. diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py index c084db8a88..ba262266b6 100644 --- a/lib/sqlalchemy/orm/attributes.py +++ b/lib/sqlalchemy/orm/attributes.py @@ -28,6 +28,12 @@ ATTR_EMPTY = util.symbol('ATTR_EMPTY') NO_VALUE = util.symbol('NO_VALUE') NEVER_SET = util.symbol('NEVER_SET') +PASSIVE_RETURN_NEVER_SET = util.symbol('PASSIVE_RETURN_NEVER_SET' +"""Symbol indicating that a 'default' value, i.e. None or blank +collection, should not be assigned to an attribute when a get() +is performed and no value was present. NEVER_SET is returned +instead. +""") PASSIVE_NO_INITIALIZE = util.symbol('PASSIVE_NO_INITIALIZE', """Symbol indicating that loader callables should @@ -429,8 +435,11 @@ class AttributeImpl(object): elif value is not ATTR_EMPTY: return self.set_committed_value(state, dict_, value) - # Return a new, empty value - return self.initialize(state, dict_) + if passive is PASSIVE_RETURN_NEVER_SET: + return NEVER_SET + else: + # Return a new, empty value + return self.initialize(state, dict_) def append(self, state, dict_, value, initiator, passive=PASSIVE_OFF): self.set(state, dict_, value, initiator, passive=passive) @@ -471,7 +480,7 @@ class ScalarAttributeImpl(AttributeImpl): # TODO: catch key errors, convert to attributeerror? if self.dispatch._active_history: - old = self.get(state, dict_) + old = self.get(state, dict_, PASSIVE_RETURN_NEVER_SET) else: old = dict_.get(self.key, NO_VALUE) @@ -489,7 +498,7 @@ class ScalarAttributeImpl(AttributeImpl): return if self.dispatch._active_history: - old = self.get(state, dict_) + old = self.get(state, dict_, PASSIVE_RETURN_NEVER_SET) else: old = dict_.get(self.key, NO_VALUE) @@ -599,6 +608,8 @@ class ScalarObjectAttributeImpl(ScalarAttributeImpl): if self.key in dict_: return History.from_object_attribute(self, state, dict_[self.key]) else: + if passive is PASSIVE_OFF: + passive = PASSIVE_RETURN_NEVER_SET current = self.get(state, dict_, passive=passive) if current is PASSIVE_NO_RESULT: return HISTORY_BLANK @@ -968,6 +979,11 @@ def backref_listeners(attribute, key, uselist): emit_backref_from_collection_remove_event, retval=True, raw=True) +_NO_HISTORY = util.symbol('NO_HISTORY') +_NO_STATE_SYMBOLS = frozenset([ + id(PASSIVE_NO_RESULT), + id(NO_VALUE), + id(NEVER_SET)]) class History(tuple): """A 3-tuple of added, unchanged and deleted values, representing the changes which have occurred on an instrumented @@ -1035,73 +1051,78 @@ class History(tuple): def as_state(self): return History( - [(c is not None and c is not PASSIVE_NO_RESULT) + [(c is not None) and instance_state(c) or None for c in self.added], - [(c is not None and c is not PASSIVE_NO_RESULT) + [(c is not None) and instance_state(c) or None for c in self.unchanged], - [(c is not None and c is not PASSIVE_NO_RESULT) + [(c is not None) and instance_state(c) or None for c in self.deleted], ) @classmethod def from_scalar_attribute(cls, attribute, state, current): - original = state.committed_state.get(attribute.key, NEVER_SET) - if current is NO_VALUE: - if (original is not None and - original is not NEVER_SET and - original is not NO_VALUE): - deleted = [original] + original = state.committed_state.get(attribute.key, _NO_HISTORY) + + if original is _NO_HISTORY: + if current is NO_VALUE: + return cls((), (), ()) else: - deleted = () - return cls((), (), deleted) - elif original is NO_VALUE: - return cls([current], (), ()) - elif (original is NEVER_SET or - attribute.is_equal(current, original) is True): - # dont let ClauseElement expressions here trip things up + return cls((), [current], ()) + # dont let ClauseElement expressions here trip things up + elif attribute.is_equal(current, original) is True: return cls((), [current], ()) else: - if original is not None: + # current convention on native scalars is to not + # include information + # about missing previous value in "deleted", but + # we do include None, which helps in some primary + # key situations + if id(original) in _NO_STATE_SYMBOLS: + deleted = () + else: deleted = [original] + if current is NO_VALUE: + return cls((), (), deleted) else: - deleted = () - return cls([current], (), deleted) + return cls([current], (), deleted) @classmethod def from_object_attribute(cls, attribute, state, current): - original = state.committed_state.get(attribute.key, NEVER_SET) + original = state.committed_state.get(attribute.key, _NO_HISTORY) - if current is NO_VALUE: - if (original is not None and - original is not NEVER_SET and - original is not NO_VALUE): - deleted = [original] + if original is _NO_HISTORY: + if current is NO_VALUE or current is NEVER_SET: + return cls((), (), ()) else: - deleted = () - return cls((), (), deleted) - elif original is NO_VALUE: - return cls([current], (), ()) - elif (original is NEVER_SET or - current is original): + return cls((), [current], ()) + elif current is original: return cls((), [current], ()) else: - if original is not None: + # current convention on related objects is to not + # include information + # about missing previous value in "deleted", and + # to also not include None - the dependency.py rules + # ignore the None in any case. + if id(original) in _NO_STATE_SYMBOLS or original is None: + deleted = () + else: deleted = [original] + if current is NO_VALUE or current is NEVER_SET: + return cls((), (), deleted) else: - deleted = () - return cls([current], (), deleted) + return cls([current], (), deleted) @classmethod def from_collection(cls, attribute, state, current): - original = state.committed_state.get(attribute.key, NEVER_SET) + original = state.committed_state.get(attribute.key, _NO_HISTORY) current = getattr(current, '_sa_adapter') if original is NO_VALUE: return cls(list(current), (), ()) - elif original is NEVER_SET: + elif original is _NO_HISTORY: return cls((), list(current), ()) else: current_states = [(instance_state(c), c) for c in current] diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index f5f8ea1efc..c91003fd9d 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -1955,7 +1955,8 @@ class Mapper(object): params[col.key] = value if col in pks: - if history.deleted: + if history.deleted and \ + not row_switch: # if passive_updates and sync detected # this was a pk->pk sync, use the new # value to locate the row, since the @@ -1980,11 +1981,21 @@ class Mapper(object): del params[col.key] value = history.added[0] params[col._label] = value + if value is None and hasdata: + raise sa_exc.FlushError( + "Can't update table " + "using NULL for primary key " + "value") else: hasdata = True elif col in pks: value = state.manager[prop.key].\ impl.get(state, state_dict) + if value is None: + raise sa_exc.FlushError( + "Can't update table " + "using NULL for primary " + "key value") params[col._label] = value if hasdata: update.append((state, state_dict, params, mapper, @@ -2235,8 +2246,15 @@ class Mapper(object): delete[connection].append(params) for col in mapper._pks_by_table[table]: params[col.key] = \ + value = \ mapper._get_state_attr_by_column( state, state_dict, col) + if value is None: + raise sa_exc.FlushError( + "Can't delete from table " + "using NULL for primary " + "key value") + if mapper.version_id_col is not None and \ table.c.contains_column(mapper.version_id_col): params[mapper.version_id_col.key] = \ diff --git a/lib/sqlalchemy/orm/state.py b/lib/sqlalchemy/orm/state.py index 00582fbdbd..0963a526bf 100644 --- a/lib/sqlalchemy/orm/state.py +++ b/lib/sqlalchemy/orm/state.py @@ -521,7 +521,6 @@ class MutableAttrInstanceState(InstanceState): # store strong ref'ed version of the object; will revert # to weakref when changes are persisted - obj = self.manager.new_instance(state=self) self.obj = weakref.ref(obj, self._cleanup) self._strong_obj = obj diff --git a/test/orm/test_attributes.py b/test/orm/test_attributes.py index b8002a446f..297d3218c7 100644 --- a/test/orm/test_attributes.py +++ b/test/orm/test_attributes.py @@ -456,10 +456,10 @@ class AttributesTest(fixtures.ORMTest): del f.y eq_(results, [ - ('set', f, 5, None), + ('set', f, 5, attributes.NEVER_SET), ('set', f, 17, 5), ('remove', f, 17), - ('set', f, [1,2,3], None), + ('set', f, [1,2,3], attributes.NEVER_SET), ('set', f, [4,5,6], [1,2,3]), ('remove', f, [4,5,6]) ]) @@ -1097,288 +1097,539 @@ class PendingBackrefTest(fixtures.ORMTest): attributes.instance_state(p1).commit_all(attributes.instance_dict(p1)) assert b.posts == [Post("post 1")] -class HistoryTest(fixtures.ORMTest): +class HistoryTest(fixtures.TestBase): - def test_get_committed_value(self): + def _fixture(self, uselist, useobject, active_history, **kw): class Foo(fixtures.BasicEntity): pass instrumentation.register_class(Foo) - attributes.register_attribute(Foo, 'someattr', uselist=False, - useobject=False) + attributes.register_attribute( + Foo, 'someattr', + uselist=uselist, + useobject=useobject, + active_history=active_history, + **kw) + return Foo + + def _two_obj_fixture(self, uselist): + class Foo(fixtures.BasicEntity): + pass + class Bar(fixtures.BasicEntity): + def __nonzero__(self): + assert False + + instrumentation.register_class(Foo) + instrumentation.register_class(Bar) + attributes.register_attribute(Foo, 'someattr', uselist=uselist, + useobject=True) + return Foo, Bar + + def _someattr_history(self, f): + return attributes.get_state_history( + attributes.instance_state(f), + 'someattr') + + def _commit_someattr(self, f): + attributes.instance_state(f).commit(attributes.instance_dict(f), + ['someattr']) + + def _someattr_committed_state(self, f): + Foo = f.__class__ + return Foo.someattr.impl.get_committed_value( + attributes.instance_state(f), + attributes.instance_dict(f)) + + def test_committed_value_init(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=False) + f = Foo() + eq_(self._someattr_committed_state(f), None) + + def test_committed_value_set(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=False) f = Foo() - eq_(Foo.someattr.impl.get_committed_value(attributes.instance_state(f), - attributes.instance_dict(f)), None) f.someattr = 3 - eq_(Foo.someattr.impl.get_committed_value(attributes.instance_state(f), - attributes.instance_dict(f)), None) + eq_(self._someattr_committed_state(f), None) + + def test_committed_value_set_commit(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=False) f = Foo() f.someattr = 3 - eq_(Foo.someattr.impl.get_committed_value(attributes.instance_state(f), - attributes.instance_dict(f)), None) - attributes.instance_state(f).commit(attributes.instance_dict(f), - ['someattr']) - eq_(Foo.someattr.impl.get_committed_value(attributes.instance_state(f), - attributes.instance_dict(f)), 3) + self._commit_someattr(f) + eq_(self._someattr_committed_state(f), 3) - def test_scalar(self): - class Foo(fixtures.BasicEntity): - pass + def test_scalar_init(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=False) + f = Foo() + eq_(self._someattr_history(f), ((), (), ())) - instrumentation.register_class(Foo) - attributes.register_attribute(Foo, 'someattr', uselist=False, - useobject=False) + def test_scalar_no_init_side_effect(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=False) + f = Foo() + self._someattr_history(f) + # no side effects + assert 'someattr' not in f.__dict__ + assert 'someattr' not in attributes.instance_state(f).committed_state + + def test_scalar_set(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=False) + f = Foo() + f.someattr = 'hi' + eq_(self._someattr_history(f), (['hi'], (), ())) - # case 1. new object + def test_scalar_set_commit(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=False) + f = Foo() + f.someattr = 'hi' + self._commit_someattr(f) + eq_(self._someattr_history(f), ((), ['hi'], ())) + def test_scalar_set_commit_reset(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=False) f = Foo() - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), ((), (), ())) f.someattr = 'hi' - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), (['hi'], (), ())) - attributes.instance_state(f).commit(attributes.instance_dict(f), - ['someattr']) - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), ((), ['hi'], ())) + self._commit_someattr(f) f.someattr = 'there' - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), (['there'], (), ['hi'])) - attributes.instance_state(f).commit(attributes.instance_dict(f), - ['someattr']) - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), ((), ['there'], ())) + eq_(self._someattr_history(f), (['there'], (), ['hi'])) + + def test_scalar_set_commit_reset_commit(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=False) + f = Foo() + f.someattr = 'hi' + self._commit_someattr(f) + f.someattr = 'there' + self._commit_someattr(f) + eq_(self._someattr_history(f), ((), ['there'], ())) + + def test_scalar_set_commit_reset_commit_del(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=False) + f = Foo() + f.someattr = 'there' + self._commit_someattr(f) del f.someattr - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), ((), (), ['there'])) + eq_(self._someattr_history(f), ((), (), ['there'])) - # case 2. object with direct dictionary settings (similar to a - # load operation) + def test_scalar_set_dict(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=False) + f = Foo() + f.__dict__['someattr'] = 'new' + eq_(self._someattr_history(f), ((), ['new'], ())) + def test_scalar_set_dict_set(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=False) f = Foo() f.__dict__['someattr'] = 'new' - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), ((), ['new'], ())) + self._someattr_history(f) f.someattr = 'old' - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), (['old'], (), ['new'])) - attributes.instance_state(f).commit(attributes.instance_dict(f), - ['someattr']) - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), ((), ['old'], ())) + eq_(self._someattr_history(f), (['old'], (), ['new'])) - # setting None on uninitialized is currently a change for a - # scalar attribute no lazyload occurs so this allows overwrite - # operation to proceed + def test_scalar_set_dict_set_commit(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=False) + f = Foo() + f.__dict__['someattr'] = 'new' + self._someattr_history(f) + f.someattr = 'old' + self._commit_someattr(f) + eq_(self._someattr_history(f), ((), ['old'], ())) + def test_scalar_set_None(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=False) f = Foo() - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), ((), (), ())) f.someattr = None - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), ([None], (), ())) + eq_(self._someattr_history(f), ([None], (), ())) + + def test_scalar_set_None_from_dict_set(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=False) f = Foo() f.__dict__['someattr'] = 'new' - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), ((), ['new'], ())) f.someattr = None - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), ([None], (), ['new'])) - - # set same value twice + eq_(self._someattr_history(f), ([None], (), ['new'])) + def test_scalar_set_twice_no_commit(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=False) f = Foo() - attributes.instance_state(f).commit(attributes.instance_dict(f), - ['someattr']) f.someattr = 'one' - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), (['one'], (), ())) + eq_(self._someattr_history(f), (['one'], (), ())) f.someattr = 'two' - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), (['two'], (), ())) + eq_(self._someattr_history(f), (['two'], (), ())) + def test_scalar_active_init(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=True) + f = Foo() + eq_(self._someattr_history(f), ((), (), ())) - def test_mutable_scalar(self): - class Foo(fixtures.BasicEntity): - pass + def test_scalar_active_no_init_side_effect(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=True) + f = Foo() + self._someattr_history(f) + # no side effects + assert 'someattr' not in f.__dict__ + assert 'someattr' not in attributes.instance_state(f).committed_state + + def test_scalar_active_set(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=True) + f = Foo() + f.someattr = 'hi' + eq_(self._someattr_history(f), (['hi'], (), ())) - instrumentation.register_class(Foo) - attributes.register_attribute(Foo, 'someattr', uselist=False, - useobject=False, mutable_scalars=True, - copy_function=dict) + def test_scalar_active_set_commit(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=True) + f = Foo() + f.someattr = 'hi' + self._commit_someattr(f) + eq_(self._someattr_history(f), ((), ['hi'], ())) - # case 1. new object + def test_scalar_active_set_commit_reset(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=True) + f = Foo() + f.someattr = 'hi' + self._commit_someattr(f) + f.someattr = 'there' + eq_(self._someattr_history(f), (['there'], (), ['hi'])) + def test_scalar_active_set_commit_reset_commit(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=True) + f = Foo() + f.someattr = 'hi' + self._commit_someattr(f) + f.someattr = 'there' + self._commit_someattr(f) + eq_(self._someattr_history(f), ((), ['there'], ())) + + def test_scalar_active_set_commit_reset_commit_del(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=True) + f = Foo() + f.someattr = 'there' + self._commit_someattr(f) + del f.someattr + eq_(self._someattr_history(f), ((), (), ['there'])) + + def test_scalar_active_set_dict(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=True) + f = Foo() + f.__dict__['someattr'] = 'new' + eq_(self._someattr_history(f), ((), ['new'], ())) + + def test_scalar_active_set_dict_set(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=True) + f = Foo() + f.__dict__['someattr'] = 'new' + self._someattr_history(f) + f.someattr = 'old' + eq_(self._someattr_history(f), (['old'], (), ['new'])) + + def test_scalar_active_set_dict_set_commit(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=True) + f = Foo() + f.__dict__['someattr'] = 'new' + self._someattr_history(f) + f.someattr = 'old' + self._commit_someattr(f) + eq_(self._someattr_history(f), ((), ['old'], ())) + + def test_scalar_active_set_None(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=True) + f = Foo() + f.someattr = None + eq_(self._someattr_history(f), ([None], (), ())) + + def test_scalar_active_set_None_from_dict_set(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=True) + f = Foo() + f.__dict__['someattr'] = 'new' + f.someattr = None + eq_(self._someattr_history(f), ([None], (), ['new'])) + + def test_scalar_active_set_twice_no_commit(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=True) + f = Foo() + f.someattr = 'one' + eq_(self._someattr_history(f), (['one'], (), ())) + f.someattr = 'two' + eq_(self._someattr_history(f), (['two'], (), ())) + + + def test_mutable_scalar_init(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=False, + mutable_scalars=True,copy_function=dict) + f = Foo() + eq_(self._someattr_history(f), ((), (), ())) + + def test_mutable_scalar_no_init_side_effect(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=False, + mutable_scalars=True,copy_function=dict) + f = Foo() + self._someattr_history(f) + assert 'someattr' not in f.__dict__ + assert 'someattr' not in attributes.instance_state(f).committed_state + + def test_mutable_scalar_set(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=False, + mutable_scalars=True,copy_function=dict) f = Foo() - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), ((), (), ())) f.someattr = {'foo': 'hi'} - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), ([{'foo': 'hi'}], (), ())) - attributes.instance_state(f).commit(attributes.instance_dict(f), - ['someattr']) - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), ((), [{'foo': 'hi'}], ())) + eq_(self._someattr_history(f), ([{'foo': 'hi'}], (), ())) + + def test_mutable_scalar_set_commit(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=False, + mutable_scalars=True,copy_function=dict) + f = Foo() + f.someattr = {'foo': 'hi'} + self._commit_someattr(f) + eq_(self._someattr_history(f), ((), [{'foo': 'hi'}], ())) eq_(attributes.instance_state(f).committed_state['someattr'], {'foo': 'hi'}) + + def test_mutable_scalar_set_commit_reset(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=False, + mutable_scalars=True,copy_function=dict) + f = Foo() + f.someattr = {'foo': 'hi'} + self._commit_someattr(f) f.someattr['foo'] = 'there' - eq_(attributes.instance_state(f).committed_state['someattr'], - {'foo': 'hi'}) + eq_(self._someattr_history(f), ([{'foo': 'there'}], (), [{'foo': 'hi'}])) eq_(attributes.get_state_history(attributes.instance_state(f), 'someattr'), ([{'foo': 'there'}], (), [{'foo': 'hi'}])) - attributes.instance_state(f).commit(attributes.instance_dict(f), - ['someattr']) - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), ((), [{'foo': 'there'}], ())) - # case 2. object with direct dictionary settings (similar to a - # load operation) + def test_mutable_scalar_set_commit_reset_commit(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=False, + mutable_scalars=True,copy_function=dict) + f = Foo() + f.someattr = {'foo': 'hi'} + self._commit_someattr(f) + f.someattr['foo'] = 'there' + self._commit_someattr(f) + eq_(self._someattr_history(f), ((), [{'foo': 'there'}], ())) + + def test_mutable_scalar_set_dict(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=False, + mutable_scalars=True,copy_function=dict) + f = Foo() + f.__dict__['someattr'] = {'foo': 'new'} + eq_(self._someattr_history(f), ((), [{'foo': 'new'}], ())) + def test_mutable_scalar_set_dict_set(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=False, + mutable_scalars=True,copy_function=dict) f = Foo() f.__dict__['someattr'] = {'foo': 'new'} - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), ((), [{'foo': 'new'}], ())) + eq_(self._someattr_history(f), ((), [{'foo': 'new'}], ())) f.someattr = {'foo': 'old'} - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), ([{'foo': 'old'}], (), [{'foo': 'new'}])) - attributes.instance_state(f).commit(attributes.instance_dict(f), - ['someattr']) - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), ((), [{'foo': 'old'}], ())) + eq_(self._someattr_history(f), ([{'foo': 'old'}], (), [{'foo': 'new'}])) - def test_flag_modified(self): - class Foo(fixtures.BasicEntity): - pass + def test_mutable_scalar_set_dict_set_commit(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=False, + mutable_scalars=True,copy_function=dict) + f = Foo() + f.__dict__['someattr'] = {'foo': 'new'} + f.someattr = {'foo': 'old'} + self._commit_someattr(f) + eq_(self._someattr_history(f), ((), [{'foo': 'old'}], ())) - instrumentation.register_class(Foo) - attributes.register_attribute(Foo, 'someattr', uselist=False, - useobject=False) + def test_scalar_inplace_mutation_set(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=False) f = Foo() - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), ((), (), ())) f.someattr = {'a': 'b'} - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), ([{'a': 'b'}], (), ())) - attributes.instance_state(f).commit_all(attributes.instance_dict(f)) - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), ((), [{'a': 'b'}], ())) + eq_(self._someattr_history(f), ([{'a': 'b'}], (), ())) + + def test_scalar_inplace_mutation_set_commit(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=False) + f = Foo() + f.someattr = {'a': 'b'} + self._commit_someattr(f) + eq_(self._someattr_history(f), ((), [{'a': 'b'}], ())) + + def test_scalar_inplace_mutation_set_commit_set(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=False) + f = Foo() + f.someattr = {'a': 'b'} + self._commit_someattr(f) f.someattr['a'] = 'c' - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), ((), [{'a': 'c'}], ())) + eq_(self._someattr_history(f), ((), [{'a': 'c'}], ())) + + def test_scalar_inplace_mutation_set_commit_flag_modified(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=False) + f = Foo() + f.someattr = {'a': 'b'} + self._commit_someattr(f) attributes.flag_modified(f, 'someattr') - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), ([{'a': 'c'}], (), ())) - f.someattr = ['a'] - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), ([['a']], (), ())) - attributes.instance_state(f).commit_all(attributes.instance_dict(f)) - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), ((), [['a']], ())) - f.someattr[0] = 'b' - f.someattr.append('c') - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), ((), [['b', 'c']], ())) + eq_(self._someattr_history(f), ([{'a': 'b'}], (), ())) + + def test_scalar_inplace_mutation_set_commit_set_flag_modified(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=False) + f = Foo() + f.someattr = {'a': 'b'} + self._commit_someattr(f) + f.someattr['a'] = 'c' attributes.flag_modified(f, 'someattr') - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), ([['b', 'c']], (), ())) + eq_(self._someattr_history(f), ([{'a': 'c'}], (), ())) - def test_use_object(self): - class Foo(fixtures.BasicEntity): - pass + def test_scalar_inplace_mutation_set_commit_flag_modified_set(self): + Foo = self._fixture(uselist=False, useobject=False, + active_history=False) + f = Foo() + f.someattr = {'a': 'b'} + self._commit_someattr(f) + attributes.flag_modified(f, 'someattr') + eq_(self._someattr_history(f), ([{'a': 'b'}], (), ())) + f.someattr = ['a'] + eq_(self._someattr_history(f), ([['a']], (), ())) - class Bar(fixtures.BasicEntity): - _state = None - def __nonzero__(self): - assert False + def test_use_object_init(self): + Foo, Bar = self._two_obj_fixture(uselist=False) + f = Foo() + eq_(self._someattr_history(f), ((), (), ())) + + def test_use_object_no_init_side_effect(self): + Foo, Bar = self._two_obj_fixture(uselist=False) + f = Foo() + self._someattr_history(f) + assert 'someattr' not in f.__dict__ + assert 'someattr' not in attributes.instance_state(f).committed_state + def test_use_object_set(self): + Foo, Bar = self._two_obj_fixture(uselist=False) + f = Foo() hi = Bar(name='hi') - there = Bar(name='there') - new = Bar(name='new') - old = Bar(name='old') + f.someattr = hi + eq_(self._someattr_history(f), ([hi], (), ())) - instrumentation.register_class(Foo) - attributes.register_attribute(Foo, 'someattr', uselist=False, - useobject=True) + def test_use_object_set_commit(self): + Foo, Bar = self._two_obj_fixture(uselist=False) + f = Foo() + hi = Bar(name='hi') + f.someattr = hi + self._commit_someattr(f) + eq_(self._someattr_history(f), ((), [hi], ())) - # case 1. new object + def test_use_object_set_commit_set(self): + Foo, Bar = self._two_obj_fixture(uselist=False) + f = Foo() + hi = Bar(name='hi') + f.someattr = hi + self._commit_someattr(f) + there = Bar(name='there') + f.someattr = there + eq_(self._someattr_history(f), ([there], (), [hi])) + def test_use_object_set_commit_set_commit(self): + Foo, Bar = self._two_obj_fixture(uselist=False) f = Foo() - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), ((), [None], ())) + hi = Bar(name='hi') f.someattr = hi - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), ([hi], (), ())) - attributes.instance_state(f).commit(attributes.instance_dict(f), - ['someattr']) - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), ((), [hi], ())) + self._commit_someattr(f) + there = Bar(name='there') f.someattr = there - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), ([there], (), [hi])) - attributes.instance_state(f).commit(attributes.instance_dict(f), - ['someattr']) - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), ((), [there], ())) + self._commit_someattr(f) + eq_(self._someattr_history(f), ((), [there], ())) + + def test_use_object_set_commit_del(self): + Foo, Bar = self._two_obj_fixture(uselist=False) + f = Foo() + hi = Bar(name='hi') + f.someattr = hi + self._commit_someattr(f) del f.someattr - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), ([None], (), [there])) + eq_(self._someattr_history(f), ((), (), [hi])) - # case 2. object with direct dictionary settings (similar to a - # load operation) + def test_use_object_set_dict(self): + Foo, Bar = self._two_obj_fixture(uselist=False) + f = Foo() + hi = Bar(name='hi') + f.__dict__['someattr'] = hi + eq_(self._someattr_history(f), ((), [hi], ())) + def test_use_object_set_dict_set(self): + Foo, Bar = self._two_obj_fixture(uselist=False) f = Foo() - f.__dict__['someattr'] = 'new' - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), ((), ['new'], ())) - f.someattr = old - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), ([old], (), ['new'])) - attributes.instance_state(f).commit(attributes.instance_dict(f), - ['someattr']) - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), ((), [old], ())) + hi = Bar(name='hi') + f.__dict__['someattr'] = hi - # setting None on uninitialized is currently not a change for an - # object attribute (this is different than scalar attribute). a - # lazyload has occurred so if its None, its really None + there = Bar(name='there') + f.someattr = there + eq_(self._someattr_history(f), ([there], (), [hi])) + def test_use_object_set_dict_set_commit(self): + Foo, Bar = self._two_obj_fixture(uselist=False) f = Foo() - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), ((), [None], ())) - f.someattr = None - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), ((), [None], ())) + hi = Bar(name='hi') + f.__dict__['someattr'] = hi + + there = Bar(name='there') + f.someattr = there + self._commit_someattr(f) + eq_(self._someattr_history(f), ((), [there], ())) + + def test_use_object_set_None(self): + Foo, Bar = self._two_obj_fixture(uselist=False) f = Foo() - f.__dict__['someattr'] = 'new' - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), ((), ['new'], ())) f.someattr = None - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), ([None], (), ['new'])) + eq_(self._someattr_history(f), ((), [None], ())) - # set same value twice + def test_use_object_set_dict_set_None(self): + Foo, Bar = self._two_obj_fixture(uselist=False) + f = Foo() + hi =Bar(name='hi') + f.__dict__['someattr'] = hi + f.someattr = None + eq_(self._someattr_history(f), ([None], (), [hi])) + def test_use_object_set_value_twice(self): + Foo, Bar = self._two_obj_fixture(uselist=False) f = Foo() - attributes.instance_state(f).commit(attributes.instance_dict(f), - ['someattr']) - f.someattr = 'one' - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), (['one'], (), ())) - f.someattr = 'two' - eq_(attributes.get_state_history(attributes.instance_state(f), - 'someattr'), (['two'], (), ())) + hi = Bar(name='hi') + there = Bar(name='there') + f.someattr = hi + f.someattr = there + eq_(self._someattr_history(f), ([there], (), ())) def test_object_collections_set(self): - class Foo(fixtures.BasicEntity): - pass - class Bar(fixtures.BasicEntity): - def __nonzero__(self): - assert False + # TODO: break into individual tests - instrumentation.register_class(Foo) - instrumentation.register_class(Bar) - attributes.register_attribute(Foo, 'someattr', uselist=True, - useobject=True) + Foo, Bar = self._two_obj_fixture(uselist=True) hi = Bar(name='hi') there = Bar(name='there') old = Bar(name='old') @@ -1392,15 +1643,13 @@ class HistoryTest(fixtures.ORMTest): f.someattr = [hi] eq_(attributes.get_state_history(attributes.instance_state(f), 'someattr'), ([hi], [], [])) - attributes.instance_state(f).commit(attributes.instance_dict(f), - ['someattr']) + self._commit_someattr(f) eq_(attributes.get_state_history(attributes.instance_state(f), 'someattr'), ((), [hi], ())) f.someattr = [there] eq_(attributes.get_state_history(attributes.instance_state(f), 'someattr'), ([there], [], [hi])) - attributes.instance_state(f).commit(attributes.instance_dict(f), - ['someattr']) + self._commit_someattr(f) eq_(attributes.get_state_history(attributes.instance_state(f), 'someattr'), ((), [there], ())) f.someattr = [hi] @@ -1422,12 +1671,13 @@ class HistoryTest(fixtures.ORMTest): f.someattr = [old] eq_(attributes.get_state_history(attributes.instance_state(f), 'someattr'), ([old], [], [new])) - attributes.instance_state(f).commit(attributes.instance_dict(f), - ['someattr']) + self._commit_someattr(f) eq_(attributes.get_state_history(attributes.instance_state(f), 'someattr'), ((), [old], ())) def test_dict_collections(self): + # TODO: break into individual tests + class Foo(fixtures.BasicEntity): pass class Bar(fixtures.BasicEntity): @@ -1453,13 +1703,14 @@ class HistoryTest(fixtures.ORMTest): eq_(tuple([set(x) for x in attributes.get_state_history(attributes.instance_state(f), 'someattr')]), (set([hi, there]), set(), set())) - attributes.instance_state(f).commit(attributes.instance_dict(f), - ['someattr']) + self._commit_someattr(f) eq_(tuple([set(x) for x in attributes.get_state_history(attributes.instance_state(f), 'someattr')]), (set(), set([hi, there]), set())) def test_object_collections_mutate(self): + # TODO: break into individual tests + class Foo(fixtures.BasicEntity): pass class Bar(fixtures.BasicEntity): @@ -1484,15 +1735,13 @@ class HistoryTest(fixtures.ORMTest): f.someattr.append(hi) eq_(attributes.get_state_history(attributes.instance_state(f), 'someattr'), ([hi], [], [])) - attributes.instance_state(f).commit(attributes.instance_dict(f), - ['someattr']) + self._commit_someattr(f) eq_(attributes.get_state_history(attributes.instance_state(f), 'someattr'), ((), [hi], ())) f.someattr.append(there) eq_(attributes.get_state_history(attributes.instance_state(f), 'someattr'), ([there], [hi], [])) - attributes.instance_state(f).commit(attributes.instance_dict(f), - ['someattr']) + self._commit_someattr(f) eq_(attributes.get_state_history(attributes.instance_state(f), 'someattr'), ((), [hi, there], ())) f.someattr.remove(there) @@ -1567,6 +1816,8 @@ class HistoryTest(fixtures.ORMTest): 'someattr'), ([], [], [hi, there, hi])) def test_collections_via_backref(self): + # TODO: break into individual tests + class Foo(fixtures.BasicEntity): pass class Bar(fixtures.BasicEntity): @@ -1583,7 +1834,7 @@ class HistoryTest(fixtures.ORMTest): eq_(attributes.get_state_history(attributes.instance_state(f1), 'bars'), ((), [], ())) eq_(attributes.get_state_history(attributes.instance_state(b1), - 'foo'), ((), [None], ())) + 'foo'), ((), (), ())) # b1.foo = f1 @@ -1601,7 +1852,26 @@ class HistoryTest(fixtures.ORMTest): eq_(attributes.get_state_history(attributes.instance_state(b2), 'foo'), ([f1], (), ())) + def test_deprecated_flags(self): + assert_raises_message( + sa_exc.SADeprecationWarning, + "Passing True for 'passive' is deprecated. " + "Use attributes.PASSIVE_NO_INITIALIZE", + attributes.get_history, object(), 'foo', True + ) + + assert_raises_message( + sa_exc.SADeprecationWarning, + "Passing False for 'passive' is deprecated. " + "Use attributes.PASSIVE_OFF", + attributes.get_history, object(), 'foo', False + ) + + +class LazyloadHistoryTest(fixtures.TestBase): def test_lazy_backref_collections(self): + # TODO: break into individual tests + class Foo(fixtures.BasicEntity): pass class Bar(fixtures.BasicEntity): @@ -1639,6 +1909,8 @@ class HistoryTest(fixtures.ORMTest): 'bars'), ((), [bar1, bar2, bar3], ())) def test_collections_via_lazyload(self): + # TODO: break into individual tests + class Foo(fixtures.BasicEntity): pass class Bar(fixtures.BasicEntity): @@ -1681,6 +1953,8 @@ class HistoryTest(fixtures.ORMTest): 'bars'), ([bar2], [], [])) def test_scalar_via_lazyload(self): + # TODO: break into individual tests + class Foo(fixtures.BasicEntity): pass @@ -1722,6 +1996,8 @@ class HistoryTest(fixtures.ORMTest): 'bar'), ([None], (), ['hi'])) def test_scalar_via_lazyload_with_active(self): + # TODO: break into individual tests + class Foo(fixtures.BasicEntity): pass @@ -1764,6 +2040,8 @@ class HistoryTest(fixtures.ORMTest): 'bar'), ([None], (), ['hi'])) def test_scalar_object_via_lazyload(self): + # TODO: break into individual tests + class Foo(fixtures.BasicEntity): pass class Bar(fixtures.BasicEntity): @@ -1801,26 +2079,11 @@ class HistoryTest(fixtures.ORMTest): eq_(f.bar, bar1) del f.bar eq_(attributes.get_state_history(attributes.instance_state(f), - 'bar'), ([None], (), [bar1])) + 'bar'), ((), (), [bar1])) assert f.bar is None eq_(attributes.get_state_history(attributes.instance_state(f), 'bar'), ([None], (), [bar1])) - def test_deprecated_flags(self): - assert_raises_message( - sa_exc.SADeprecationWarning, - "Passing True for 'passive' is deprecated. " - "Use attributes.PASSIVE_NO_INITIALIZE", - attributes.get_history, object(), 'foo', True - ) - - assert_raises_message( - sa_exc.SADeprecationWarning, - "Passing False for 'passive' is deprecated. " - "Use attributes.PASSIVE_OFF", - attributes.get_history, object(), 'foo', False - ) - class ListenerTest(fixtures.ORMTest): def test_receive_changes(self): """test that Listeners can mutate the given value.""" diff --git a/test/orm/test_cascade.py b/test/orm/test_cascade.py index f596bf1ec7..74246a216b 100644 --- a/test/orm/test_cascade.py +++ b/test/orm/test_cascade.py @@ -1089,7 +1089,7 @@ class M2OCascadeDeleteOrphanTestOne(fixtures.MappedTest): assert User.foo.dispatch._active_history is False eq_( attributes.get_history(u1, 'foo'), - ([None], (), [attributes.PASSIVE_NO_RESULT]) + ([None], (), ()) ) sess.add(u1) diff --git a/test/orm/test_merge.py b/test/orm/test_merge.py index f247f8f671..b58cb6e79e 100644 --- a/test/orm/test_merge.py +++ b/test/orm/test_merge.py @@ -552,7 +552,7 @@ class MergeTest(_fixtures.FixtureTest): a2 = sess2.merge(a1) eq_( attributes.get_history(a2, 'user'), - ([u2], (), [attributes.PASSIVE_NO_RESULT]) + ([u2], (), ()) ) assert a2 in sess2.dirty diff --git a/test/orm/test_unitofwork.py b/test/orm/test_unitofwork.py index d1136c6bbd..8b2787d12a 100644 --- a/test/orm/test_unitofwork.py +++ b/test/orm/test_unitofwork.py @@ -267,10 +267,6 @@ class BinaryHistTest(fixtures.MappedTest, testing.AssertsExecutionResults): s.flush() self.assert_sql_count(testing.db, go, 0) - - - - class PKTest(fixtures.MappedTest): @classmethod @@ -2137,68 +2133,6 @@ class BooleanColTest(fixtures.MappedTest): sess.flush() eq_(sess.query(T).filter(T.value==True).all(), [T(value=True, name="t1"),T(value=True, name="t3")]) -class DontAllowFlushOnLoadingObjectTest(fixtures.MappedTest): - """Test that objects with NULL identity keys aren't permitted to complete a flush. - - User-defined callables that execute during a load may modify state - on instances which results in their being autoflushed, before attributes - are populated. If the primary key identifiers are missing, an explicit assertion - is needed to check that the object doesn't go through the flush process with - no net changes and gets placed in the identity map with an incorrect - identity key. - - """ - @classmethod - def define_tables(cls, metadata): - t1 = Table('t1', metadata, - Column('id', Integer, primary_key=True), - Column('data', String(30)), - ) - - def test_flush_raises(self): - t1 = self.tables.t1 - - class T1(fixtures.ComparableEntity): - @reconstructor - def go(self): - # blow away 'id', no change event. - # this simulates a callable occurring - # before 'id' was even populated, i.e. a callable - # within an attribute_mapped_collection - self.__dict__.pop('id', None) - - # generate a change event, perhaps this occurs because - # someone wrote a broken attribute_mapped_collection that - # inappropriately fires off change events when it should not, - # now we're dirty - self.data = 'foo bar' - - # blow away that change, so an UPDATE does not occur - # (since it would break) - self.__dict__.pop('data', None) - - # flush ! any lazyloader here would trigger - # autoflush, for example. - sess.flush() - - mapper(T1, t1) - - sess = Session() - sess.add(T1(data='test', id=5)) - sess.commit() - sess.close() - - # make sure that invalid state doesn't get into the session - # with the wrong key. If the identity key is not NULL, at least - # the population process would continue after the erroneous flush - # and thing would right themselves. - assert_raises_message(sa.orm.exc.FlushError, - 'has a NULL identity key. Check if this ' - 'flush is occurring at an inappropriate ' - 'time, such as during a load operation.', - sess.query(T1).first) - - class RowSwitchTest(fixtures.MappedTest): @classmethod @@ -2409,8 +2343,6 @@ class InheritingRowSwitchTest(fixtures.MappedTest): ) ) - - class TransactionTest(fixtures.MappedTest): __requires__ = ('deferrable_constraints',) @@ -2469,3 +2401,75 @@ class TransactionTest(fixtures.MappedTest): if testing.against('postgresql'): t1.bind.engine.dispose() +class PartialNullPKTest(fixtures.MappedTest): + # sqlite totally fine with NULLs in pk columns. + # no other DB is like this. + __only_on__ = ('sqlite',) + + @classmethod + def define_tables(cls, metadata): + Table('t1', metadata, + Column('col1', String(10), primary_key=True, nullable=True), + Column('col2', String(10), primary_key=True, nullable=True), + Column('col3', String(50)) + ) + + @classmethod + def setup_classes(cls): + class T1(cls.Basic): + pass + + @classmethod + def setup_mappers(cls): + orm_mapper(cls.classes.T1, cls.tables.t1) + + def test_key_switch(self): + T1 = self.classes.T1 + s = Session() + s.add(T1(col1="1", col2=None)) + + t1 = s.query(T1).first() + t1.col2 = 5 + assert_raises_message( + sa.exc.FlushError, + "Can't update table using NULL for primary key value", + s.commit + ) + + def test_plain_update(self): + T1 = self.classes.T1 + s = Session() + s.add(T1(col1="1", col2=None)) + + t1 = s.query(T1).first() + t1.col3 = 'hi' + assert_raises_message( + sa.exc.FlushError, + "Can't update table using NULL for primary key value", + s.commit + ) + + def test_delete(self): + T1 = self.classes.T1 + s = Session() + s.add(T1(col1="1", col2=None)) + + t1 = s.query(T1).first() + s.delete(t1) + assert_raises_message( + sa.exc.FlushError, + "Can't delete from table using NULL for primary key value", + s.commit + ) + + def test_total_null(self): + T1 = self.classes.T1 + s = Session() + s.add(T1(col1=None, col2=None)) + assert_raises_message( + sa.exc.FlushError, + r"Instance \ has a NULL " + "identity key. Check if this flush is occurring " + "at an inappropriate time, such as during a load operation.", + s.commit + )