Mike Bayer avatar Mike Bayer committed 718c952

- 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.

Comments (0)

Files changed (8)

     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.

lib/sqlalchemy/orm/attributes.py

 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
                 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)
 
         # 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)
 
             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)
 
         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
                     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
 
     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]

lib/sqlalchemy/orm/mapper.py

                                     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
                                         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, 
                 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] = \

lib/sqlalchemy/orm/state.py

 
         # 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

test/orm/test_attributes.py

         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])
         ])
         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)
-        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)
-        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)
+        attributes.register_attribute(
+                    Foo, 'someattr', 
+                    uselist=uselist,
+                    useobject=useobject,
+                    active_history=active_history,
+                    **kw)
+        return Foo
 
-    def test_scalar(self):
-        class Foo(fixtures.BasicEntity):
-            pass
-
-        instrumentation.register_class(Foo)
-        attributes.register_attribute(Foo, 'someattr', uselist=False,
-                useobject=False)
-
-        # case 1.  new object
-
-        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'], ()))
-        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'], ()))
-        del f.someattr
-        eq_(attributes.get_state_history(attributes.instance_state(f),
-            'someattr'), ((), (), ['there']))
-
-        # case 2.  object with direct dictionary settings (similar to a
-        # load operation)
-
-        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'], ()))
-
-        # setting None on uninitialized is currently a change for a
-        # scalar attribute no lazyload occurs so this allows overwrite
-        # operation to proceed
-
-        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], (), ()))
-        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
-
-        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'], (), ()))
-
-
-    def test_mutable_scalar(self):
-        class Foo(fixtures.BasicEntity):
-            pass
-
-        instrumentation.register_class(Foo)
-        attributes.register_attribute(Foo, 'someattr', uselist=False,
-                useobject=False, mutable_scalars=True,
-                copy_function=dict)
-
-        # case 1.  new object
-
-        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_(attributes.instance_state(f).committed_state['someattr'],
-            {'foo': 'hi'})
-        f.someattr['foo'] = 'there'
-        eq_(attributes.instance_state(f).committed_state['someattr'],
-            {'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)
-
-        f = Foo()
-        f.__dict__['someattr'] = {'foo': 'new'}
-        eq_(attributes.get_state_history(attributes.instance_state(f),
-            'someattr'), ((), [{'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'}], ()))
-
-    def test_flag_modified(self):
-        class Foo(fixtures.BasicEntity):
-            pass
-
-        instrumentation.register_class(Foo)
-        attributes.register_attribute(Foo, 'someattr', uselist=False,
-                useobject=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'}], ()))
-        f.someattr['a'] = 'c'
-        eq_(attributes.get_state_history(attributes.instance_state(f),
-            'someattr'), ((), [{'a': 'c'}], ()))
-        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']], ()))
-        attributes.flag_modified(f, 'someattr')
-        eq_(attributes.get_state_history(attributes.instance_state(f),
-            'someattr'), ([['b', 'c']], (), ()))
-
-    def test_use_object(self):
-        class Foo(fixtures.BasicEntity):
-            pass
-
-        class Bar(fixtures.BasicEntity):
-            _state = None
-            def __nonzero__(self):
-                assert False
-
-        hi = Bar(name='hi')
-        there = Bar(name='there')
-        new = Bar(name='new')
-        old = Bar(name='old')
-
-        instrumentation.register_class(Foo)
-        attributes.register_attribute(Foo, 'someattr', uselist=False,
-                useobject=True)
-
-        # case 1.  new object
-
-        f = Foo()
-        eq_(attributes.get_state_history(attributes.instance_state(f),
-            'someattr'), ((), [None], ()))
-        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], ()))
-        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], ()))
-        del f.someattr
-        eq_(attributes.get_state_history(attributes.instance_state(f),
-            'someattr'), ([None], (), [there]))
-
-        # case 2.  object with direct dictionary settings (similar to a
-        # load operation)
-
-        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], ()))
-
-        # 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
-
-        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], ()))
-        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
-
-        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'], (), ()))
-
-    def test_object_collections_set(self):
+    def _two_obj_fixture(self, uselist):
         class Foo(fixtures.BasicEntity):
             pass
         class Bar(fixtures.BasicEntity):
 
         instrumentation.register_class(Foo)
         instrumentation.register_class(Bar)
-        attributes.register_attribute(Foo, 'someattr', uselist=True,
+        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()
+        f.someattr = 3
+        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
+        self._commit_someattr(f)
+        eq_(self._someattr_committed_state(f), 3)
+
+    def test_scalar_init(self):
+        Foo = self._fixture(uselist=False, useobject=False, 
+                                active_history=False)
+        f = Foo()
+        eq_(self._someattr_history(f), ((), (), ()))
+
+    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'], (), ()))
+
+    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()
+        f.someattr = 'hi'
+        self._commit_someattr(f)
+        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_(self._someattr_history(f), ((), (), ['there']))
+
+    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'
+        self._someattr_history(f)
+        f.someattr = 'old'
+        eq_(self._someattr_history(f), (['old'], (), ['new']))
+
+    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()
+        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'
+        f.someattr = None
+        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()
+        f.someattr = 'one'
+        eq_(self._someattr_history(f), (['one'], (), ()))
+        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_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'], (), ()))
+
+    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'], ()))
+
+    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()
+        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_(self._someattr_history(f), ([{'foo': 'there'}], (), [{'foo': 'hi'}]))
+        eq_(attributes.get_state_history(attributes.instance_state(f),
+            'someattr'), ([{'foo': 'there'}], (), [{'foo': 'hi'}]))
+
+    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_(self._someattr_history(f), ((), [{'foo': 'new'}], ()))
+        f.someattr = {'foo': 'old'}
+        eq_(self._someattr_history(f), ([{'foo': 'old'}], (), [{'foo': 'new'}]))
+
+    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'}], ()))
+
+    def test_scalar_inplace_mutation_set(self):
+        Foo = self._fixture(uselist=False, useobject=False, 
+                                active_history=False)
+        f = Foo()
+        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_(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_(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_(self._someattr_history(f), ([{'a': 'c'}], (), ()))
+
+    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']], (), ()))
+
+    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')
+        f.someattr = hi
+        eq_(self._someattr_history(f), ([hi], (), ()))
+
+    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], ()))
+
+    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()
+        hi = Bar(name='hi')
+        f.someattr = hi
+        self._commit_someattr(f)
+        there = Bar(name='there')
+        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_(self._someattr_history(f), ((), (), [hi]))
+
+    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()
+        hi = Bar(name='hi')
+        f.__dict__['someattr'] = hi
+
+        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()
+        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.someattr = None
+        eq_(self._someattr_history(f), ((), [None], ()))
+
+    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()
+        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):
+        # TODO: break into individual tests
+
+        Foo, Bar = self._two_obj_fixture(uselist=True)
         hi = Bar(name='hi')
         there = Bar(name='there')
         old = Bar(name='old')
         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]
         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):
         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):
         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)
             'someattr'), ([], [], [hi, there, hi]))
 
     def test_collections_via_backref(self):
+        # TODO: break into individual tests
+
         class Foo(fixtures.BasicEntity):
             pass
         class Bar(fixtures.BasicEntity):
         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
 
         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):
             'bars'), ((), [bar1, bar2, bar3], ()))
 
     def test_collections_via_lazyload(self):
+        # TODO: break into individual tests
+
         class Foo(fixtures.BasicEntity):
             pass
         class Bar(fixtures.BasicEntity):
             'bars'), ([bar2], [], []))
 
     def test_scalar_via_lazyload(self):
+        # TODO: break into individual tests
+
         class Foo(fixtures.BasicEntity):
             pass
 
             'bar'), ([None], (), ['hi']))
 
     def test_scalar_via_lazyload_with_active(self):
+        # TODO: break into individual tests
+
         class Foo(fixtures.BasicEntity):
             pass
 
             'bar'), ([None], (), ['hi']))
 
     def test_scalar_object_via_lazyload(self):
+        # TODO: break into individual tests
+
         class Foo(fixtures.BasicEntity):
             pass
         class Bar(fixtures.BasicEntity):
         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."""

test/orm/test_cascade.py

         assert User.foo.dispatch._active_history is False
         eq_(
             attributes.get_history(u1, 'foo'),
-            ([None], (), [attributes.PASSIVE_NO_RESULT])
+            ([None], (), ())
         )
 
         sess.add(u1)

test/orm/test_merge.py

         a2 = sess2.merge(a1)
         eq_(
             attributes.get_history(a2, 'user'), 
-            ([u2], (), [attributes.PASSIVE_NO_RESULT])
+            ([u2], (), ())
         )
         assert a2 in sess2.dirty
 

test/orm/test_unitofwork.py

             s.flush()
         self.assert_sql_count(testing.db, go, 0)
 
-
-
-
-
 class PKTest(fixtures.MappedTest):
 
     @classmethod
         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
             )
         )
 
-
-
 class TransactionTest(fixtures.MappedTest):
     __requires__ = ('deferrable_constraints',)
 
         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 \<T1 at .+?\> has a NULL "
+            "identity key.  Check if this flush is occurring "
+            "at an inappropriate time, such as during a load operation.",
+            s.commit
+        )
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.