]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
all tests pass with this version
authorMike Bayer <mike_mp@zzzcomputing.com>
Tue, 6 Apr 2010 03:21:02 +0000 (23:21 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Tue, 6 Apr 2010 03:21:02 +0000 (23:21 -0400)
lib/sqlalchemy/orm/dependency.py
lib/sqlalchemy/orm/unitofwork.py
test/orm/test_cycles.py

index 63a34cd0ab9f4590332acb201e40f7cdcf739171..ecea094fd10ece9dba8b282e1fcea5e6fbe0610c 100644 (file)
@@ -36,8 +36,10 @@ class DependencyProcessor(object):
         self.enable_typechecks = prop.enable_typechecks
         self.key = prop.key
         if not self.prop.synchronize_pairs:
-            raise sa_exc.ArgumentError("Can't build a DependencyProcessor for relationship %s.  "
-                    "No target attributes to populate between parent and child are present" %
+            raise sa_exc.ArgumentError(
+                    "Can't build a DependencyProcessor for relationship %s. "
+                    "No target attributes to populate between parent and "
+                    "child are present" %
                      self.prop)
 
     def _get_instrumented_attribute(self):
@@ -49,7 +51,8 @@ class DependencyProcessor(object):
 
     def hasparent(self, state):
         """return True if the given object instance has a parent,
-        according to the ``InstrumentedAttribute`` handled by this ``DependencyProcessor``.
+        according to the ``InstrumentedAttribute`` handled by this 
+        ``DependencyProcessor``.
         
         """
         # TODO: use correct API for this
@@ -62,6 +65,9 @@ class DependencyProcessor(object):
         the aggreagte.
         
         """
+        if self.post_update and self._check_reverse(uow):
+            return
+        
         unitofwork.GetDependentObjects(uow, self, False, True)
         unitofwork.GetDependentObjects(uow, self, True, True)
 
@@ -90,6 +96,10 @@ class DependencyProcessor(object):
         in the 'aggregated' version of events.
         
         """
+
+        if self.post_update and self._check_reverse(uow):
+            return
+
         # locate and disable the aggregate processors
         # for this dependency
         
@@ -207,65 +217,29 @@ class DependencyProcessor(object):
                     "whose mapper does not inherit from that of %s." % 
                     (state.class_, self.prop, self.mapper.class_))
             
-    def _synchronize(self, state, child, associationrow, clearkeys, uowcommit):
-        """Called during a flush to synchronize primary key identifier
-        values between a parent/child object, as well as to an
-        associationrow in the case of many-to-many.
-        
-        """
+    def _synchronize(self, state, 
+                            child, associationrow, 
+                            clearkeys, uowcommit):
         raise NotImplementedError()
 
     def _check_reverse(self, uow):
         for p in self.prop._reverse_property:
             if not p.viewonly and p._dependency_processor and \
-                (unitofwork.ProcessAll, p._dependency_processor, False, True) in \
+                (unitofwork.ProcessAll, 
+                    p._dependency_processor, False, True) in \
                     uow.postsort_actions:
                 return True
         else:
             return False
 
-    def _check_reverse_action(self, uowcommit, parent, child, action):
-        """Determine if an action has been performed by the 'reverse' property of this property.
-        
-        this is used to ensure that only one side of a bidirectional relationship
-        issues a certain operation for a parent/child pair.
-        
-        """
-        for r in self.prop._reverse_property:
-            if not r.viewonly and (r._dependency_processor, action, parent, child) in uowcommit.attributes:
-                return True
-        return False
-    
-    def _performed_action(self, uowcommit, parent, child, action):
-        """Establish that an action has been performed for a certain parent/child pair.
-        
-        Used only for actions that are sensitive to bidirectional double-action,
-        i.e. manytomany, post_update.
-        
-        """
-        uowcommit.attributes[(self, action, parent, child)] = True
-        
     def _conditional_post_update(self, state, uowcommit, related):
-        """Execute a post_update call.
-
-        For relationships that contain the post_update flag, an additional
-        ``UPDATE`` statement may be associated after an ``INSERT`` or
-        before a ``DELETE`` in order to resolve circular row
-        dependencies.
-
-        This method will check for the post_update flag being set on a
-        particular relationship, and given a target object and list of
-        one or more related objects, and execute the ``UPDATE`` if the
-        given related object list contains ``INSERT``s or ``DELETE``s.
-        
-        """
         if state is not None and self.post_update:
             for x in related:
-                if x is not None and not self._check_reverse_action(uowcommit, x, state, "postupdate"):
-                    uowcommit.issue_post_update(state, [r for l, r in self.prop.synchronize_pairs])
-                    #uowcommit.register_object(state, postupdate=True, 
-                    #                post_update_cols=[r for l, r in self.prop.synchronize_pairs])
-                    self._performed_action(uowcommit, x, state, "postupdate")
+                if x is not None:
+                    uowcommit.issue_post_update(
+                            state, 
+                            [r for l, r in self.prop.synchronize_pairs]
+                    )
                     break
 
     def _pks_changed(self, uowcommit, state):
@@ -283,7 +257,6 @@ class OneToManyDP(DependencyProcessor):
                                                 after_save, 
                                                 before_delete):
         if self.post_update:
-            # TODO: childisdelete logic ?
             uow.dependencies.update([
                 (child_saves, after_save),
                 (parent_saves, after_save),
@@ -311,16 +284,30 @@ class OneToManyDP(DependencyProcessor):
                                     isdelete, childisdelete):
         
         if self.post_update:
+            # TODO: this whole block is not covered
+            # by any tests
             if not isdelete:
-                uow.dependencies.update([
-                    (save_parent, after_save),
-                    (child_action, after_save)
-                ])
+                if childisdelete:
+                    uow.dependencies.update([
+                        (save_parent, after_save),
+                        (after_save, child_action)
+                    ])
+                else:
+                    uow.dependencies.update([
+                        (save_parent, after_save),
+                        (child_action, after_save)
+                    ])
             else:
-                uow.dependencies.update([
-                    (before_delete, delete_parent),
-                    (before_delete, child_action)
-                ])
+                if childisdelete:
+                    uow.dependencies.update([
+                        (before_delete, delete_parent),
+                        (before_delete, child_action)
+                    ])
+                else:
+                    uow.dependencies.update([
+                        (before_delete, delete_parent),
+                        (child_action, before_delete)
+                    ])
         elif not isdelete:
             uow.dependencies.update([
                 (save_parent, after_save),
@@ -334,12 +321,16 @@ class OneToManyDP(DependencyProcessor):
             ])
         
     def presort_deletes(self, uowcommit, states):
-        # 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
-        should_null_fks = not self.cascade.delete and not self.passive_deletes == 'all'
+        # 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
+        should_null_fks = not self.cascade.delete and \
+                            not self.passive_deletes == 'all'
         for state in states:
             history = uowcommit.get_attribute_history(
-                                        state, self.key, passive=self.passive_deletes)
+                                            state, 
+                                            self.key, 
+                                            passive=self.passive_deletes)
             if history:
                 for child in history.deleted:
                     if child is not None and self.hasparent(child) is False:
@@ -354,7 +345,10 @@ class OneToManyDP(DependencyProcessor):
     
     def presort_saves(self, uowcommit, states):
         for state in states:
-            history = uowcommit.get_attribute_history(state, self.key, passive=True)
+            history = uowcommit.get_attribute_history(
+                                            state, 
+                                            self.key, 
+                                            passive=True)
             if history:
                 for child in history.added:
                     if child is not None:
@@ -378,13 +372,17 @@ class OneToManyDP(DependencyProcessor):
                             uowcommit.register_object(child, False, self.passive_updates)
     
     def process_deletes(self, uowcommit, states):
-        # 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
-        # this phase can be called safely for any cascade but is unnecessary if delete cascade
+        # 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 this phase can be called 
+        # safely for any cascade but is unnecessary if delete cascade
         # is on.
         if self.post_update or not self.passive_deletes == 'all':
             for state in states:
-                history = uowcommit.get_attribute_history(state, self.key, passive=self.passive_deletes)
+                history = uowcommit.get_attribute_history(
+                                            state, 
+                                            self.key, 
+                                            passive=self.passive_deletes)
                 if history:
                     for child in history.deleted:
                         if child is not None and self.hasparent(child) is False:
@@ -427,7 +425,11 @@ class OneToManyDP(DependencyProcessor):
                                     self.passive_updates)
 
     def _pks_changed(self, uowcommit, state):
-        return sync.source_modified(uowcommit, state, self.parent, self.prop.synchronize_pairs)
+        return sync.source_modified(
+                            uowcommit, 
+                            state, 
+                            self.parent, 
+                            self.prop.synchronize_pairs)
 
 class ManyToOneDP(DependencyProcessor):
     def __init__(self, prop):
@@ -477,7 +479,6 @@ class ManyToOneDP(DependencyProcessor):
                         (child_action, after_save)
                     ])
             else:
-                # TODO: childisdelete logic here?
                 uow.dependencies.update([
                     (before_delete, delete_parent),
                     (before_delete, child_action)
@@ -668,7 +669,8 @@ class ManyToManyDP(DependencyProcessor):
         if self._check_reverse(uow):
             return
         else:
-            DependencyProcessor.per_state_flush_actions(self, uow, states, isdelete)
+            DependencyProcessor.\
+                    per_state_flush_actions(self, uow, states, isdelete)
             
     def per_property_dependencies(self, uow, parent_saves, 
                                                 child_saves, 
@@ -734,7 +736,8 @@ class ManyToManyDP(DependencyProcessor):
                     self._synchronize(state, child, associationrow, False, uowcommit)
                     secondary_delete.append(associationrow)
 
-        self._run_crud(uowcommit, secondary_insert, secondary_update, secondary_delete)
+        self._run_crud(uowcommit, secondary_insert, 
+                        secondary_update, secondary_delete)
 
     def process_saves(self, uowcommit, states):
         secondary_delete = []
@@ -763,13 +766,23 @@ class ManyToManyDP(DependencyProcessor):
                 
                 for child in history.unchanged:
                     associationrow = {}
-                    sync.update(state, self.parent, associationrow, "old_", self.prop.synchronize_pairs)
-                    sync.update(child, self.mapper, associationrow, "old_", self.prop.secondary_synchronize_pairs)
+                    sync.update(
+                                state, 
+                                self.parent, 
+                                associationrow, 
+                                "old_", 
+                                self.prop.synchronize_pairs)
+                    sync.update(
+                                child, 
+                                self.mapper, 
+                                associationrow, 
+                                "old_", 
+                                self.prop.secondary_synchronize_pairs)
 
-                    #self.syncrules.update(associationrow, state, child, "old_")
                     secondary_update.append(associationrow)
                     
-        self._run_crud(uowcommit, secondary_insert, secondary_update, secondary_delete)
+        self._run_crud(uowcommit, secondary_insert, 
+                        secondary_update, secondary_delete)
         
     def _run_crud(self, uowcommit, secondary_insert, secondary_update, secondary_delete):
         connection = uowcommit.transaction.connection(self.mapper)
@@ -777,22 +790,27 @@ class ManyToManyDP(DependencyProcessor):
         if secondary_delete:
             associationrow = secondary_delete[0]
             statement = self.secondary.delete(sql.and_(*[
-                                c == sql.bindparam(c.key, type_=c.type) for c in self.secondary.c if c.key in associationrow
+                                c == sql.bindparam(c.key, type_=c.type) 
+                                for c in self.secondary.c if c.key in associationrow
                             ]))
             result = connection.execute(statement, secondary_delete)
-            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)))
+            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)))
 
         if secondary_update:
             associationrow = secondary_update[0]
             statement = self.secondary.update(sql.and_(*[
-                                c == sql.bindparam("old_" + c.key, type_=c.type) for c in self.secondary.c if c.key in associationrow
+                                c == sql.bindparam("old_" + c.key, type_=c.type) 
+                                for c in self.secondary.c if c.key in associationrow
                             ]))
             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 "
+                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)))
 
@@ -811,5 +829,9 @@ class ManyToManyDP(DependencyProcessor):
                                         self.prop.secondary_synchronize_pairs)
 
     def _pks_changed(self, uowcommit, state):
-        return sync.source_modified(uowcommit, state, self.parent, self.prop.synchronize_pairs)
+        return sync.source_modified(
+                            uowcommit, 
+                            state, 
+                            self.parent, 
+                            self.prop.synchronize_pairs)
 
index 0e613a95feae8c348e89383bbdf6a45ec8a9cf79..0028202c57bd3b6409c2fdfe4001e56ba7ad4011 100644 (file)
@@ -129,9 +129,7 @@ class UOWTransaction(object):
         else:
             return history.as_state()
 
-    def register_object(self, state, isdelete=False, 
-                            listonly=False, postupdate=False, 
-                            post_update_cols=None):
+    def register_object(self, state, isdelete=False, listonly=False):
         
         # if object is not in the overall session, do nothing
         if not self.session._contains_state(state):
@@ -147,8 +145,10 @@ class UOWTransaction(object):
             self.states[state] = (isdelete, listonly)
         else:
             existing_isdelete, existing_listonly = self.states[state]
-            if isdelete and not existing_isdelete:
-                raise Exception("Can't upgrade from a save to a delete")
+            self.states[state] = (
+                existing_isdelete or isdelete,
+                existing_listonly and listonly
+            )
     
     def issue_post_update(self, state, post_update_cols):
         mapper = state.manager.mapper.base_mapper
index 002095b8c725d948ef0bc9adf0c8dd0f3cd33be1..aca5fa9dbdcccbc71faa1001c194eb9d1b2c7808 100644 (file)
@@ -801,7 +801,6 @@ class SelfReferentialPostUpdateTest(_base.MappedTest):
         session.add(root)
         session.flush()
 
-        print "-------------------"
         remove_child(root, cats)
         # pre-trigger lazy loader on 'cats' to make the test easier
         cats.children
@@ -826,6 +825,42 @@ class SelfReferentialPostUpdateTest(_base.MappedTest):
              lambda ctx:[{'id':cats.id}])
         )
 
+        session.delete(root)
+        self.assert_sql_execution(
+            testing.db, 
+            session.flush,
+            AllOf(
+                CompiledSQL("UPDATE node SET next_sibling_id=:next_sibling_id "
+                            "WHERE node.id = :node_id", 
+                            lambda ctx:{'next_sibling_id':None, 'node_id':about.id}),
+                CompiledSQL("UPDATE node SET next_sibling_id=:next_sibling_id "
+                            "WHERE node.id = :node_id",
+                            lambda ctx:{'node_id':stories.id, 'next_sibling_id':None})
+            ),
+            AllOf(
+                CompiledSQL("DELETE FROM node WHERE node.id = :id",
+                    lambda ctx:{'id':about.id}
+                ),
+                CompiledSQL("DELETE FROM node WHERE node.id = :id",
+                    lambda ctx:{'id':stories.id}
+                ),
+                CompiledSQL("DELETE FROM node WHERE node.id = :id",
+                    lambda ctx:{'id':bruce.id}
+                ),
+            ),
+            CompiledSQL("DELETE FROM node WHERE node.id = :id",
+                lambda ctx:{'id':root.id}
+            ),
+        )
+        about = Node('about')
+        cats = Node('cats')
+        about.next_sibling = cats
+        cats.prev_sibling = about
+        session.add(about)
+        session.flush()
+        session.delete(about)
+        cats.prev_sibling = None
+        session.flush()
 
 class SelfReferentialPostUpdateTest2(_base.MappedTest):