summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy
diff options
context:
space:
mode:
Diffstat (limited to 'lib/sqlalchemy')
-rw-r--r--lib/sqlalchemy/orm/__init__.py2
-rw-r--r--lib/sqlalchemy/orm/dependency.py28
-rw-r--r--lib/sqlalchemy/orm/exc.py24
-rw-r--r--lib/sqlalchemy/orm/mapper.py21
-rw-r--r--lib/sqlalchemy/test/requires.py6
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(