summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES14
-rw-r--r--lib/sqlalchemy/orm/attributes.py101
-rw-r--r--lib/sqlalchemy/orm/mapper.py20
-rw-r--r--lib/sqlalchemy/orm/state.py1
-rw-r--r--test/orm/test_attributes.py725
-rw-r--r--test/orm/test_cascade.py2
-rw-r--r--test/orm/test_merge.py2
-rw-r--r--test/orm/test_unitofwork.py140
8 files changed, 662 insertions, 343 deletions
diff --git a/CHANGES b/CHANGES
index 88347f066..0d343df81 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 c084db8a8..ba262266b 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 f5f8ea1ef..c91003fd9 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 00582fbdb..0963a526b 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 b8002a446..297d3218c 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 f596bf1ec..74246a216 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 f247f8f67..b58cb6e79 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 d1136c6bb..8b2787d12 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 \<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
+ )