]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- added 'passive_deletes="all"' flag to relation(), disables all
authorMike Bayer <mike_mp@zzzcomputing.com>
Sun, 9 Sep 2007 17:01:38 +0000 (17:01 +0000)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sun, 9 Sep 2007 17:01:38 +0000 (17:01 +0000)
  nulling-out of foreign key attributes during a flush where the parent
  object is deleted.

- fix to FK compile fix from yesterday

CHANGES
lib/sqlalchemy/orm/__init__.py
lib/sqlalchemy/orm/dependency.py
lib/sqlalchemy/orm/properties.py
lib/sqlalchemy/schema.py
test/orm/unitofwork.py

diff --git a/CHANGES b/CHANGES
index 8a8b897a2a823abd929946cb6eb76373bdf8ccd7..9cbd3be8d8c9c3ce460e4149b1fde698aaa0bf62 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -21,6 +21,10 @@ CHANGES
   
 - Fixed reflection of the empty string for mysql enums.
 
+- added 'passive_deletes="all"' flag to relation(), disables all
+  nulling-out of foreign key attributes during a flush where the parent 
+  object is deleted.
+  
 - column defaults and onupdates, executing inline,  will add parenthesis 
   for subqueries and other parenthesis-requiring expressions
 
index 0d4dec4bc4a19fe2a7ab33b8585daff4c3a1fe62..5a2c4144979f9589f38aa75f8a42f2b76f28f0c6 100644 (file)
@@ -168,14 +168,21 @@ def relation(argument, secondary=None, **kwargs):
           indicates the ordering that should be applied when loading these items.
 
         passive_deletes=False
-          Indicates if lazy-loaders should not be executed during the ``flush()``
-          process, which normally occurs in order to locate all existing child
-          items when a parent item is to be deleted. Setting this flag to True is
-          appropriate when ``ON DELETE CASCADE`` rules have been set up on the
-          actual tables so that the database may handle cascading deletes
-          automatically. This strategy is useful particularly for handling the
-          deletion of objects that have very large (and/or deep) child-object
-          collections. 
+          Indicates the behavior of delete operations. 
+          A value of True indicates that unloaded child items should not be loaded
+          during a delete operation on the parent.  Normally, when a parent
+          item is deleted, all child items are loaded so that they can either be
+          marked as deleted, or have their foreign key to the parent set to NULL.
+          Marking this flag as True usually implies an ON DELETE <CASCADE|SET NULL>
+          rule is in place which will handle updating/deleting child rows on the
+          database side.
+          
+          Additionally, setting the flag to the string value 'all' will disable
+          the "nulling out" of the child foreign keys, when there is no delete or
+          delete-orphan cascade enabled.  This is typically used when a triggering
+          or error raise scenario is in place on the database side.  Note that
+          the foreign key attributes on in-session child objects will not be changed
+          after a flush occurs so this is a very special use-case setting.
 
         post_update
           this indicates that the relationship should be handled by a second
index 3b837d275ac033c391c99efa2779cf586487ed2f..1e461e6bf7de330173ab0f10e13ad42f01ea35c2 100644 (file)
@@ -188,7 +188,7 @@ class OneToManyDP(DependencyProcessor):
             # the child objects have to have their foreign key to the parent set to NULL
             # this phase can be called safely for any cascade but is unnecessary if delete cascade
             # is on.
-            if not self.cascade.delete or self.post_update:
+            if (not self.cascade.delete or self.post_update) and not self.passive_deletes=='all':
                 for obj in deplist:
                     childlist = self.get_object_dependencies(obj, uowcommit, passive=self.passive_deletes)
                     if childlist is not None:
@@ -217,7 +217,7 @@ class OneToManyDP(DependencyProcessor):
         if delete:
             # head object is being deleted, and we manage its list of child objects
             # the child objects have to have their foreign key to the parent set to NULL
-            if not self.post_update and not self.cascade.delete:
+            if not self.post_update and not self.cascade.delete and not self.passive_deletes=='all':
                 for obj in deplist:
                     childlist = self.get_object_dependencies(obj, uowcommit, passive=self.passive_deletes)
                     if childlist is not None:
@@ -265,7 +265,7 @@ class ManyToOneDP(DependencyProcessor):
     def process_dependencies(self, task, deplist, uowcommit, delete = False):
         #print self.mapper.mapped_table.name + " " + self.key + " " + repr(len(deplist)) + " process_dep isdelete " + repr(delete) + " direction " + repr(self.direction)
         if delete:
-            if self.post_update and not self.cascade.delete_orphan:
+            if self.post_update and not self.cascade.delete_orphan and not self.passive_deletes=='all':
                 # post_update means we have to update our row to not reference the child object
                 # before we can DELETE the row
                 for obj in deplist:
index 20cbcb2351332b1c5d6e28b49a57358fe2146376..90a7ba3a4272d9c84fa3bf47b9ff8cfcfcab3c8b 100644 (file)
@@ -161,6 +161,9 @@ class PropertyLoader(StrategizedProperty):
                 self.cascade = mapperutil.CascadeOptions("all, delete-orphan")
             else:
                 self.cascade = mapperutil.CascadeOptions("save-update, merge")
+        
+        if self.passive_deletes == 'all' and ("delete" in self.cascade or "delete-orphan" in self.cascade):
+            raise exceptions.ArgumentError("Can't set passive_deletes='all' in conjunction with 'delete' or 'delete-orphan' cascade")
 
         self.association = association
         if association:
index 212fdac82acf9334b5ef431d531877a26466c2d7..3b7f08f79cdeaf340953163df4cad5dcaa5dc26d 100644 (file)
@@ -509,6 +509,8 @@ class Column(SchemaItem, expression._ColumnClause):
         if not self._is_oid:
             self._pre_existing_column = table._columns.get(self.key)
             table._columns.add(self)
+        else:
+            self._pre_existing_column = None
         if self.primary_key:
             table.primary_key.add(self)
         elif self.key in table.primary_key:
@@ -555,6 +557,7 @@ class Column(SchemaItem, expression._ColumnClause):
         c.orig_set = self.orig_set
         c.__originating_column = self.__originating_column
         c._distance = self._distance + 1
+        c._pre_existing_column = self._pre_existing_column
         if not c._is_oid:
             selectable.columns.add(c)
             if self.primary_key:
index 63456b638573effad8d8fcff49f0e1dc183b73f6..c202abf99b8300de7a8091add5f893c1a73c6e88 100644 (file)
@@ -579,6 +579,72 @@ class PassiveDeletesTest(ORMTest):
         sess.commit()
         assert mytable.count().scalar() == 0
         assert myothertable.count().scalar() == 0
+
+class ExtraPassiveDeletesTest(ORMTest):
+    def define_tables(self, metadata):
+        global mytable,myothertable
+
+        mytable = Table('mytable', metadata,
+            Column('id', Integer, primary_key=True),
+            Column('data', String(30)),
+            test_needs_fk=True,
+            )
+
+        myothertable = Table('myothertable', metadata,
+            Column('id', Integer, primary_key=True),
+            Column('parent_id', Integer),
+            Column('data', String(30)),
+            ForeignKeyConstraint(['parent_id'],['mytable.id']),  # no CASCADE, the same as ON DELETE RESTRICT
+            test_needs_fk=True,
+            )
+    
+    def test_assertions(self):
+        class MyClass(object):
+            pass
+        class MyOtherClass(object):
+            pass
+        
+        mapper(MyOtherClass, myothertable)
+        
+        try:
+            mapper(MyClass, mytable, properties={
+                'children':relation(MyOtherClass, passive_deletes='all', cascade="all")
+            })
+            assert False
+        except exceptions.ArgumentError, e:
+            assert str(e) == "Can't set passive_deletes='all' in conjunction with 'delete' or 'delete-orphan' cascade"
+        
+    @testing.unsupported('sqlite')
+    def test_extra_passive(self):
+        class MyClass(object):
+            pass
+        class MyOtherClass(object):
+            pass
+        
+        mapper(MyOtherClass, myothertable)
+
+        mapper(MyClass, mytable, properties={
+            'children':relation(MyOtherClass, passive_deletes='all', cascade="save-update")
+        })
+
+        sess = Session
+        mc = MyClass()
+        mc.children.append(MyOtherClass())
+        mc.children.append(MyOtherClass())
+        mc.children.append(MyOtherClass())
+        mc.children.append(MyOtherClass())
+        sess.save(mc)
+        sess.commit()
+
+        assert myothertable.count().scalar() == 4
+        mc = sess.query(MyClass).get(mc.id)
+        sess.delete(mc)
+        try:
+            sess.commit()
+            assert False
+        except (exceptions.IntegrityError, exceptions.OperationalError):
+            assert True
+
         
 class DefaultTest(ORMTest):
     """tests that when saving objects whose table contains DefaultGenerators, either python-side, preexec or database-side,