]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- Added new parameter :paramref:`.mapper.confirm_deleted_rows`. Defaults
authorMike Bayer <mike_mp@zzzcomputing.com>
Fri, 28 Mar 2014 22:00:35 +0000 (18:00 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 28 Mar 2014 22:00:35 +0000 (18:00 -0400)
to True, indicates that a series of DELETE statements should confirm
that the cursor rowcount matches the number of primary keys that should
have matched;  this behavior had been taken off in most cases
(except when version_id is used) to support the unusual edge case of
self-referential ON DELETE CASCADE; to accomodate this, the message
is now just a warning, not an exception, and the flag can be used
to indicate a mapping that expects self-refererntial cascaded
deletes of this nature.  See also :ticket:`2403` for background on the
original change. re: #2403 fix #3007

doc/build/changelog/changelog_09.rst
lib/sqlalchemy/orm/mapper.py
lib/sqlalchemy/orm/persistence.py
test/orm/test_unitofwork.py
test/orm/test_unitofworkv2.py

index d0116b33369942507ef642c4591f996739d63488..35078e53bc2b757319c5d18d9987f65bd46387ff 100644 (file)
 .. changelog::
     :version: 0.9.4
 
+    .. change::
+        :tags: feature, orm
+        :tickets: 3007
+
+        Added new parameter :paramref:`.mapper.confirm_deleted_rows`.  Defaults
+        to True, indicates that a series of DELETE statements should confirm
+        that the cursor rowcount matches the number of primary keys that should
+        have matched;  this behavior had been taken off in most cases
+        (except when version_id is used) to support the unusual edge case of
+        self-referential ON DELETE CASCADE; to accomodate this, the message
+        is now just a warning, not an exception, and the flag can be used
+        to indicate a mapping that expects self-refererntial cascaded
+        deletes of this nature.  See also :ticket:`2403` for background on the
+        original change.
+
     .. change::
         :tags: bug, ext, automap
         :tickets: 3004
index 4747c075da90ed582ae25654d4b3af34216010e1..a939cb9c76d6bcee7fd21826d42a500e2b511382 100644 (file)
@@ -110,6 +110,7 @@ class Mapper(_InspectionAttr):
                  include_properties=None,
                  exclude_properties=None,
                  passive_updates=True,
+                 confirm_deleted_rows=True,
                  eager_defaults=False,
                  legacy_is_orphan=False,
                  _compiled_cache_size=100,
@@ -208,6 +209,17 @@ class Mapper(_InspectionAttr):
 
            See the section :ref:`concrete_inheritance` for an example.
 
+        :param confirm_deleted_rows: defaults to True; when a DELETE occurs
+          of one more more rows based on specific primary keys, a warning is
+          emitted when the number of rows matched does not equal the number
+          of rows expected.  This parameter may be set to False to handle the case
+          where database ON DELETE CASCADE rules may be deleting some of those
+          rows automatically.  The warning may be changed to an exception
+          in a future release.
+
+          .. versionadded:: 0.9.4 - added :paramref:`.mapper.confirm_deleted_rows`
+             as well as conditional matched row checking on delete.
+
         :param eager_defaults: if True, the ORM will immediately fetch the
           value of server-generated default values after an INSERT or UPDATE,
           rather than leaving them as expired to be fetched on next access.
@@ -545,9 +557,13 @@ class Mapper(_InspectionAttr):
         self._compiled_cache_size = _compiled_cache_size
         self._reconstructor = None
         self._deprecated_extensions = util.to_list(extension or [])
-
         self.allow_partial_pks = allow_partial_pks
 
+        if self.inherits and not self.concrete:
+            self.confirm_deleted_rows = False
+        else:
+            self.confirm_deleted_rows = confirm_deleted_rows
+
         self._set_with_polymorphic(with_polymorphic)
 
         if isinstance(self.local_table, expression.SelectBase):
index d62e803ee5fcd0e134221df4ce1aceff1bc4a052..1bd432f155a3cb24dad949bebf911e957692201c 100644 (file)
@@ -681,19 +681,14 @@ def _emit_delete_statements(base_mapper, uowtransaction, cached_connections,
 
         expected = len(del_objects)
         rows_matched = -1
+        only_warn = False
         if connection.dialect.supports_sane_multi_rowcount:
             c = connection.execute(statement, del_objects)
 
-            # only do a row check if we have versioning turned on.
-            # unfortunately, we *cannot* do a check on the number of
-            # rows matched here in general, as there is the edge case
-            # of a table that has a self-referential foreign key with
-            # ON DELETE CASCADE on it, see #2403.   I'm not sure how we can
-            # resolve this, unless we require special configuration
-            # to enable "count rows" for certain mappings, or to disable
-            # it, or to based on it relationship(), not sure.
-            if need_version_id:
-                rows_matched = c.rowcount
+            if not need_version_id:
+                only_warn = True
+
+            rows_matched = c.rowcount
 
         elif need_version_id:
             if connection.dialect.supports_sane_rowcount:
@@ -713,12 +708,24 @@ def _emit_delete_statements(base_mapper, uowtransaction, cached_connections,
         else:
             connection.execute(statement, del_objects)
 
-        if rows_matched > -1 and expected != rows_matched:
-            raise orm_exc.StaleDataError(
-                "DELETE statement on table '%s' expected to "
-                "delete %d row(s); %d were matched." %
-                (table.description, expected, rows_matched)
-            )
+        if base_mapper.confirm_deleted_rows and \
+            rows_matched > -1 and expected != rows_matched:
+            if only_warn:
+                util.warn(
+                    "DELETE statement on table '%s' expected to "
+                    "delete %d row(s); %d were matched.  Please set "
+                    "confirm_deleted_rows=False within the mapper "
+                    "configuration to prevent this warning." %
+                    (table.description, expected, rows_matched)
+                )
+            else:
+                raise orm_exc.StaleDataError(
+                    "DELETE statement on table '%s' expected to "
+                    "delete %d row(s); %d were matched.  Please set "
+                    "confirm_deleted_rows=False within the mapper "
+                    "configuration to prevent this warning." %
+                    (table.description, expected, rows_matched)
+                )
 
 def _finalize_insert_update_commands(base_mapper, uowtransaction,
                             states_to_insert, states_to_update):
index 61f736ce60ebd5d9e5d30191541bdc6065bac882..4776e29885ef707495663903e25452f4db997a97 100644 (file)
@@ -562,6 +562,7 @@ class BatchDeleteIgnoresRowcountTest(fixtures.DeclarativeMappedTest):
         class A(cls.DeclarativeBasic):
             __tablename__ = 'A'
             __table_args__ = dict(test_needs_fk=True)
+            __mapper_args__ = {"confirm_deleted_rows": False}
             id = Column(Integer, primary_key=True)
             parent_id = Column(Integer, ForeignKey('A.id', ondelete='CASCADE'))
 
index 0ac3349afa24a68b0444562262fee29c0b401db1..5512535e53b2105256f273ad429933d3ebd5285a 100644 (file)
@@ -3,6 +3,7 @@ from sqlalchemy import testing
 from sqlalchemy.testing import engines
 from sqlalchemy.testing.schema import Table, Column
 from test.orm import _fixtures
+from sqlalchemy import exc
 from sqlalchemy.testing import fixtures
 from sqlalchemy import Integer, String, ForeignKey, func
 from sqlalchemy.orm import mapper, relationship, backref, \
@@ -1284,7 +1285,7 @@ class BasicStaleChecksTest(fixtures.MappedTest):
             Column('data', Integer)
         )
 
-    def _fixture(self):
+    def _fixture(self, confirm_deleted_rows=True):
         parent, child = self.tables.parent, self.tables.child
 
         class Parent(fixtures.BasicEntity):
@@ -1295,8 +1296,8 @@ class BasicStaleChecksTest(fixtures.MappedTest):
         mapper(Parent, parent, properties={
             'child':relationship(Child, uselist=False,
                                     cascade="all, delete-orphan",
-                                    backref="parent")
-        })
+                                    backref="parent"),
+        }, confirm_deleted_rows=confirm_deleted_rows)
         mapper(Child, child)
         return Parent, Child
 
@@ -1318,7 +1319,7 @@ class BasicStaleChecksTest(fixtures.MappedTest):
         )
 
     @testing.requires.sane_multi_rowcount
-    def test_delete_multi_missing(self):
+    def test_delete_multi_missing_warning(self):
         Parent, Child = self._fixture()
         sess = Session()
         p1 = Parent(id=1, data=2, child=None)
@@ -1330,16 +1331,27 @@ class BasicStaleChecksTest(fixtures.MappedTest):
         sess.delete(p1)
         sess.delete(p2)
 
+        assert_raises_message(
+            exc.SAWarning,
+            "DELETE statement on table 'parent' expected to "
+                "delete 2 row\(s\); 0 were matched.",
+            sess.flush
+        )
+
+    @testing.requires.sane_multi_rowcount
+    def test_delete_multi_missing_allow(self):
+        Parent, Child = self._fixture(confirm_deleted_rows=False)
+        sess = Session()
+        p1 = Parent(id=1, data=2, child=None)
+        p2 = Parent(id=2, data=3, child=None)
+        sess.add_all([p1, p2])
         sess.flush()
 
-        # see issue #2403 - we *cannot* use rowcount here, as
-        # self-referential DELETE CASCADE could have deleted rows
-        #assert_raises_message(
-        #    orm_exc.StaleDataError,
-        #    "DELETE statement on table 'parent' expected to "
-        #        "delete 2 row\(s\); 0 were matched.",
-        #    sess.flush
-        #)
+        sess.execute(self.tables.parent.delete())
+        sess.delete(p1)
+        sess.delete(p2)
+
+        sess.flush()
 
 
 class BatchInsertsTest(fixtures.MappedTest, testing.AssertsExecutionResults):