diff options
Diffstat (limited to 'lib/sqlalchemy')
-rw-r--r-- | lib/sqlalchemy/orm/__init__.py | 2 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/dependency.py | 28 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/exc.py | 24 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/mapper.py | 21 | ||||
-rw-r--r-- | lib/sqlalchemy/test/requires.py | 6 |
5 files changed, 51 insertions, 30 deletions
diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py index 1252baa07..4c9efb714 100644 --- a/lib/sqlalchemy/orm/__init__.py +++ b/lib/sqlalchemy/orm/__init__.py @@ -782,7 +782,7 @@ def mapper(class_, local_table=None, *args, **params): 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 diff --git a/lib/sqlalchemy/orm/dependency.py b/lib/sqlalchemy/orm/dependency.py index e96eed28e..376afd88d 100644 --- a/lib/sqlalchemy/orm/dependency.py +++ b/lib/sqlalchemy/orm/dependency.py @@ -1032,14 +1032,12 @@ class ManyToManyDP(DependencyProcessor): 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] @@ -1051,14 +1049,12 @@ class ManyToManyDP(DependencyProcessor): 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() diff --git a/lib/sqlalchemy/orm/exc.py b/lib/sqlalchemy/orm/exc.py index 431acc15c..3f28a3dd3 100644 --- a/lib/sqlalchemy/orm/exc.py +++ b/lib/sqlalchemy/orm/exc.py @@ -12,8 +12,25 @@ import sqlalchemy as sa 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): @@ -24,7 +41,8 @@ class UnmappedError(sa.exc.InvalidRequestError): """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.""" diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index 6c25b89ca..9cb358151 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -1818,10 +1818,10 @@ class Mapper(object): 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 " @@ -2050,10 +2050,10 @@ class Mapper(object): 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: @@ -2180,8 +2180,9 @@ class Mapper(object): 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_, diff --git a/lib/sqlalchemy/test/requires.py b/lib/sqlalchemy/test/requires.py index 1b9052fd8..fefb00330 100644 --- a/lib/sqlalchemy/test/requires.py +++ b/lib/sqlalchemy/test/requires.py @@ -247,6 +247,12 @@ def sane_rowcount(fn): 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( |