0.6.4
=====
- orm
+ - The name ConcurrentModificationError has been
+ changed to StaleDataError, and descriptive
+ error messages have been revised to reflect
+ exactly what the issue is. Both names will
+ remain available for the forseeable future
+ for schemes that may be specifying
+ ConcurrentModificationError in an "except:"
+ clause.
+
- Moving an o2m object from one collection to
another, or vice versa changing the referenced
object by an m2o, where the foreign key is also a
a running *version id* of mapped entities in the database. this is
used during save operations to ensure that no other thread or process
has updated the instance during the lifetime of the entity, else a
- ``ConcurrentModificationError`` exception is thrown.
+ :class:`StaleDataError` exception is thrown.
:param version_id_generator: A callable which defines the algorithm used to generate new version
ids. Defaults to an integer generator. Can be replaced with one that
if result.supports_sane_multi_rowcount() and \
result.rowcount != len(secondary_delete):
- raise exc.ConcurrentModificationError(
- "Deleted rowcount %d does not match number of "
- "secondary table rows deleted from table '%s': %d" %
- (
- result.rowcount,
- self.secondary.description,
- len(secondary_delete))
- )
+ raise exc.StaleDataError(
+ "DELETE statement on table '%s' expected to delete %d row(s); "
+ "Only %d were matched." %
+ (self.secondary.description, len(secondary_delete),
+ result.rowcount)
+ )
if secondary_update:
associationrow = secondary_update[0]
result = connection.execute(statement, secondary_update)
if result.supports_sane_multi_rowcount() and \
result.rowcount != len(secondary_update):
- raise exc.ConcurrentModificationError(
- "Updated rowcount %d does not match number of "
- "secondary table rows updated from table '%s': %d" %
- (
- result.rowcount,
- self.secondary.description,
- len(secondary_update))
- )
+ raise exc.StaleDataError(
+ "UPDATE statement on table '%s' expected to update %d row(s); "
+ "Only %d were matched." %
+ (self.secondary.description, len(secondary_update),
+ result.rowcount)
+ )
if secondary_insert:
statement = self.secondary.insert()
NO_STATE = (AttributeError, KeyError)
"""Exception types that may be raised by instrumentation implementations."""
-class ConcurrentModificationError(sa.exc.SQLAlchemyError):
- """Rows have been modified outside of the unit of work."""
+class StaleDataError(sa.exc.SQLAlchemyError):
+ """An operation encountered database state that is unaccounted for.
+
+ Two conditions cause this to happen:
+
+ * A flush may have attempted to update or delete rows
+ and an unexpected number of rows were matched during
+ the UPDATE or DELETE statement. Note that when
+ version_id_col is used, rows in UPDATE or DELETE statements
+ are also matched against the current known version
+ identifier.
+
+ * A mapped object with version_id_col was refreshed,
+ and the version number coming back from the database does
+ not match that of the object itself.
+
+ """
+
+ConcurrentModificationError = StaleDataError
class FlushError(sa.exc.SQLAlchemyError):
"""TODO"""
class DetachedInstanceError(sa.exc.SQLAlchemyError):
- """An attempt to access unloaded attributes on a mapped instance that is detached."""
+ """An attempt to access unloaded attributes on a
+ mapped instance that is detached."""
class UnmappedInstanceError(UnmappedError):
"""An mapping operation was requested for an unknown instance."""
if connection.dialect.supports_sane_rowcount:
if rows != len(update):
- raise orm_exc.ConcurrentModificationError(
- "Updated rowcount %d does not match number "
- "of objects updated %d" %
- (rows, len(update)))
+ raise orm_exc.StaleDataError(
+ "UPDATE statement on table '%s' expected to update %d row(s); "
+ "%d were matched." %
+ (table.description, len(update), rows))
elif needs_version_id:
util.warn("Dialect %s does not support updated rowcount "
rows = c.rowcount
if rows != -1 and rows != len(del_objects):
- raise orm_exc.ConcurrentModificationError(
- "Deleted rowcount %d does not match "
- "number of objects deleted %d" %
- (c.rowcount, len(del_objects))
+ raise orm_exc.StaleDataError(
+ "DELETE statement on table '%s' expected to delete %d row(s); "
+ "%d were matched." %
+ (table.description, len(del_objects), c.rowcount)
)
for state, state_dict, mapper, has_identity, connection in tups:
self.version_id_col) != \
row[version_id_col]:
- raise orm_exc.ConcurrentModificationError(
- "Instance '%s' version of %s does not match %s"
+ raise orm_exc.StaleDataError(
+ "Instance '%s' has version id '%s' which "
+ "does not match database-loaded version id '%s'."
% (state_str(state),
self._get_state_attr_by_column(
state, dict_,
skip_if(lambda: not testing.db.dialect.supports_sane_rowcount)
)
+def sane_multi_rowcount(fn):
+ return _chain_decorators_on(
+ fn,
+ skip_if(lambda: not testing.db.dialect.supports_sane_multi_rowcount)
+ )
+
def reflects_pk_names(fn):
"""Target driver reflects the name of primary key constraints."""
return _chain_decorators_on(
sess.flush()
- assert_raises(orm_exc.ConcurrentModificationError,
+ assert_raises(orm_exc.StaleDataError,
sess2.query(Base).with_lockmode('read').get,
s1.id)
if not testing.db.dialect.supports_sane_rowcount:
sess2.flush()
else:
- assert_raises(orm_exc.ConcurrentModificationError, sess2.flush)
+ assert_raises(orm_exc.StaleDataError, sess2.flush)
sess2.refresh(s2)
if testing.db.dialect.supports_sane_rowcount:
s2.subdata = 'some new subdata'
sess.flush()
- try:
- s1.subdata = 'some new subdata'
+ s1.subdata = 'some new subdata'
+ if testing.db.dialect.supports_sane_rowcount:
+ assert_raises(
+ orm_exc.StaleDataError,
+ sess.flush
+ )
+ else:
sess.flush()
- assert not testing.db.dialect.supports_sane_rowcount
- except orm_exc.ConcurrentModificationError, e:
- assert True
class DistinctPKTest(_base.MappedTest):
"""test the construction of mapper.primary_key when an inheriting relationship
-from sqlalchemy.test.testing import assert_raises, assert_raises_message, eq_
+from sqlalchemy.test.testing import assert_raises, \
+ assert_raises_message, eq_
import sqlalchemy as sa
from sqlalchemy.test import testing
from sqlalchemy import Integer, String, ForeignKey
from sqlalchemy.test.schema import Table
from sqlalchemy.test.schema import Column
-from sqlalchemy.orm import mapper, relationship, create_session
+from sqlalchemy.orm import mapper, relationship, create_session, \
+ exc as orm_exc, sessionmaker
from test.orm import _base
self.assert_result([t1], Transition, {'outputs': (Place, [{'name':'place3'}, {'name':'place1'}])})
self.assert_result([p2], Place, {'inputs': (Transition, [{'name':'transition1'},{'name':'transition2'}])})
-
+ @testing.requires.sane_multi_rowcount
+ @testing.resolve_artifact_names
+ def test_stale_conditions(self):
+ mapper(Place, place, properties={
+ 'transitions':relationship(Transition, secondary=place_input,
+ passive_updates=False)
+ })
+ mapper(Transition, transition)
+
+ p1 = Place('place1')
+ t1 = Transition('t1')
+ p1.transitions.append(t1)
+ sess = sessionmaker()()
+ sess.add_all([p1, t1])
+ sess.commit()
+
+ p1.place_id
+ p1.transitions
+
+ sess.execute("delete from place_input", mapper=Place)
+ p1.place_id = 7
+
+ assert_raises_message(
+ orm_exc.StaleDataError,
+ r"UPDATE statement on table 'place_input' expected to "
+ r"update 1 row\(s\); Only 0 were matched.",
+ sess.commit
+ )
+ sess.rollback()
+
+ p1.place_id
+ p1.transitions
+ sess.execute("delete from place_input", mapper=Place)
+ p1.transitions.remove(t1)
+ assert_raises_message(
+ orm_exc.StaleDataError,
+ r"DELETE statement on table 'place_input' expected to "
+ r"delete 1 row\(s\); Only 0 were matched.",
+ sess.commit
+ )
+
class M2MTest2(_base.MappedTest):
@classmethod
def define_tables(cls, metadata):
from test.engine import _base as engine_base
-_uuids =['1fc614acbb904742a2990f86af6ded95', '23e253786f4d491b9f9d6189dc33de9b', 'fc44910db37e43fd93e9ec8165b885cf',
- '0187a1832b4249e6b48911821d86de58', '778af6ea2fb74a009d8d2f5abe5dc29a', '51a6ce031aff47e4b5f2895c4161f120',
- '7434097cd319401fb9f15fa443ccbbbb', '9bc548a8128e4a85ac18060bc3f4b7d3', '59548715e3c440b7bcb96417d06f7930',
- 'd7647c7004734de196885ca2bd73adf8', '70cef121d3ff48d39906b6d1ac77f41a', 'ee37a8a6430c466aa322b8a215a0dd70',
- '782a5f04b4364a53a6fce762f48921c1', 'bef510f2420f4476a7629013ead237f5']
+_uuids = [
+ '1fc614acbb904742a2990f86af6ded95',
+ '23e253786f4d491b9f9d6189dc33de9b',
+ 'fc44910db37e43fd93e9ec8165b885cf',
+ '0187a1832b4249e6b48911821d86de58',
+ '778af6ea2fb74a009d8d2f5abe5dc29a',
+ '51a6ce031aff47e4b5f2895c4161f120',
+ '7434097cd319401fb9f15fa443ccbbbb',
+ '9bc548a8128e4a85ac18060bc3f4b7d3',
+ '59548715e3c440b7bcb96417d06f7930',
+ 'd7647c7004734de196885ca2bd73adf8',
+ '70cef121d3ff48d39906b6d1ac77f41a',
+ 'ee37a8a6430c466aa322b8a215a0dd70',
+ '782a5f04b4364a53a6fce762f48921c1',
+ 'bef510f2420f4476a7629013ead237f5',
+ ]
def make_uuid():
"""generate uuids even on Python 2.4 which has no 'uuid'"""
f1.value='f1rev3mine'
# Only dialects with a sane rowcount can detect the
- # ConcurrentModificationError
+ # StaleDataError
if testing.db.dialect.supports_sane_rowcount:
- assert_raises(sa.orm.exc.ConcurrentModificationError, s1.commit)
+ assert_raises_message(sa.orm.exc.StaleDataError,
+ r"UPDATE statement on table 'version_table' expected "
+ r"to update 1 row\(s\); 0 were matched.",
+ s1.commit),
s1.rollback()
else:
s1.commit()
s1.delete(f2)
if testing.db.dialect.supports_sane_rowcount:
- assert_raises(sa.orm.exc.ConcurrentModificationError, s1.commit)
+ assert_raises_message(
+ sa.orm.exc.StaleDataError,
+ r"DELETE statement on table 'version_table' expected "
+ r"to delete 2 row\(s\); 1 were matched.",
+ s1.commit)
else:
s1.commit()
s2.commit()
# load, version is wrong
- assert_raises(
- sa.orm.exc.ConcurrentModificationError,
+ assert_raises_message(
+ sa.orm.exc.StaleDataError,
+ r"Instance .* has version id '\d+' which does not "
+ r"match database-loaded version id '\d+'",
s1.query(Foo).with_lockmode('read').get, f1s1.id
)
sess1.commit()
p2.data = 'P overwritten by concurrent tx'
- assert_raises(
- orm.exc.ConcurrentModificationError,
+ assert_raises_message(
+ orm.exc.StaleDataError,
+ r"UPDATE statement on table 'p' expected to update "
+ r"1 row\(s\); 0 were matched.",
sess2.commit
)