Commits

Mike Bayer committed 69dbcdd

- Related to ticket`3060`, an adjustment has been made to the unit
of work such that loading for related many-to-one objects is slightly
more aggressive, in the case of a graph of self-referential objects
that are to be deleted; the load of related objects is to help
determine the correct order for deletion if passive_deletes is
not set.
- revert the changes to test_delete_unloaded_m2o, these deletes do in fact
need to occur in the order of the two child objects first.

  • Participants
  • Parent commits 2c8689f

Comments (0)

Files changed (8)

File doc/build/changelog/changelog_09.rst

 
     .. change::
         :tags: bug, orm
+        :versions: 1.0.0
+
+        Related to :ticket:`3060`, an adjustment has been made to the unit
+        of work such that loading for related many-to-one objects is slightly
+        more aggressive, in the case of a graph of self-referential objects
+        that are to be deleted; the load of related objects is to help
+        determine the correct order for deletion if passive_deletes is
+        not set.
+
+    .. change::
+        :tags: bug, orm
         :tickets: 3057
         :versions: 1.0.0
 

File lib/sqlalchemy/orm/attributes.py

             NEVER_SET, NO_CHANGE, CALLABLES_OK, SQL_OK, RELATED_OBJECT_OK,\
             INIT_OK, NON_PERSISTENT_OK, LOAD_AGAINST_COMMITTED, PASSIVE_OFF,\
             PASSIVE_RETURN_NEVER_SET, PASSIVE_NO_INITIALIZE, PASSIVE_NO_FETCH,\
-            PASSIVE_NO_FETCH_RELATED, PASSIVE_ONLY_PERSISTENT, NO_AUTOFLUSH,\
-            _none_tuple
+            PASSIVE_NO_FETCH_RELATED, PASSIVE_ONLY_PERSISTENT, NO_AUTOFLUSH
 from .base import state_str, instance_str
 
 @inspection._self_inspects
     def get_history(self, state, dict_, passive=PASSIVE_OFF):
         raise NotImplementedError()
 
-    def get_all_pending(self, state, dict_):
+    def get_all_pending(self, state, dict_, passive=PASSIVE_NO_INITIALIZE):
         """Return a list of tuples of (state, obj)
         for all objects in this attribute's current state
         + history.
             else:
                 return History.from_object_attribute(self, state, current)
 
-    def get_all_pending(self, state, dict_):
+    def get_all_pending(self, state, dict_, passive=PASSIVE_NO_INITIALIZE):
         if self.key in dict_:
             current = dict_[self.key]
-            if current is not None:
-                ret = [(instance_state(current), current)]
-            else:
-                ret = [(None, None)]
+        elif passive & CALLABLES_OK:
+            current = self.get(state, dict_, passive=passive)
+        else:
+            return []
+
+        # can't use __hash__(), can't use __eq__() here
+        if current is not None and \
+                current is not PASSIVE_NO_RESULT and \
+                current is not NEVER_SET:
+            ret = [(instance_state(current), current)]
+        else:
+            ret = [(None, None)]
 
-            if self.key in state.committed_state:
-                original = state.committed_state[self.key]
-                if original not in (NEVER_SET, PASSIVE_NO_RESULT, None) and \
+        if self.key in state.committed_state:
+            original = state.committed_state[self.key]
+            if original is not None and \
+                    original is not PASSIVE_NO_RESULT and \
+                    original is not NEVER_SET and \
                     original is not current:
 
-                    ret.append((instance_state(original), original))
-            return ret
-        else:
-            return []
+                ret.append((instance_state(original), original))
+        return ret
 
     def set(self, state, dict_, value, initiator,
                 passive=PASSIVE_OFF, check_old=None, pop=False):
         else:
             return History.from_collection(self, state, current)
 
-    def get_all_pending(self, state, dict_):
+    def get_all_pending(self, state, dict_, passive=PASSIVE_NO_INITIALIZE):
+        # NOTE: passive is ignored here at the moment
+
         if self.key not in dict_:
             return []
 
     def emit_backref_from_scalar_set_event(state, child, oldchild, initiator):
         if oldchild is child:
             return child
-        if oldchild not in _none_tuple:
+        if oldchild is not None and \
+                oldchild is not PASSIVE_NO_RESULT and \
+                oldchild is not NEVER_SET:
             # With lazy=None, there's no guarantee that the full collection is
             # present when updating via a backref.
             old_state, old_dict = instance_state(oldchild),\

File lib/sqlalchemy/orm/base.py

 """)
 
 _none_set = frozenset([None, NEVER_SET, PASSIVE_NO_RESULT])
-_none_tuple = tuple(_none_set)  # for "in" checks that won't trip __hash__
 
 
 def _generative(*assertions):

File lib/sqlalchemy/orm/dependency.py

                 parent_in_cycles = True
 
         # now create actions /dependencies for each state.
+
         for state in states:
             # detect if there's anything changed or loaded
-            # by a preprocessor on this state/attribute.  if not,
-            # we should be able to skip it entirely.
+            # by a preprocessor on this state/attribute.   In the
+            # case of deletes we may try to load missing items here as well.
             sum_ = state.manager[self.key].impl.get_all_pending(
-                state, state.dict)
+                state, state.dict,
+                                self._passive_delete_flag
+                                        if isdelete
+                                        else attributes.PASSIVE_NO_INITIALIZE)
 
             if not sum_:
                 continue

File lib/sqlalchemy/orm/strategies.py

         if not state.key:
             return attributes.ATTR_EMPTY
 
-        if not passive & attributes.SQL_OK:
+        if not passive & attributes.CALLABLES_OK:
             return attributes.PASSIVE_NO_RESULT
 
         localparent = state.manager.mapper

File lib/sqlalchemy/orm/unitofwork.py

                     not sess._contains_state(newvalue_state):
                     sess._save_or_update_state(newvalue_state)
 
-            if oldvalue not in orm_util._none_tuple and \
+            if oldvalue is not None and \
+                oldvalue is not attributes.NEVER_SET and \
+                oldvalue is not attributes.PASSIVE_NO_RESULT and \
                     prop._cascade.delete_orphan:
                 # possible to reach here with attributes.NEVER_SET ?
                 oldvalue_state = attributes.instance_state(oldvalue)

File lib/sqlalchemy/orm/util.py

 import re
 
 from .base import instance_str, state_str, state_class_str, attribute_str, \
-        state_attribute_str, object_mapper, object_state, _none_set, \
-        _none_tuple
+        state_attribute_str, object_mapper, object_state, _none_set
 from .base import class_mapper, _class_to_mapper
 from .base import _InspectionAttr
 from .path_registry import PathRegistry

File test/orm/test_unitofworkv2.py

                     "WHERE nodes.id = :param_1",
                     lambda ctx: {'param_1': c2id}
                 ),
-                Or(
-                    AllOf(
-                        CompiledSQL(
-                            "DELETE FROM nodes WHERE nodes.id = :id",
-                            lambda ctx: [{'id': c1id}, {'id': c2id}]
-                        ),
-                        CompiledSQL(
-                            "DELETE FROM nodes WHERE nodes.id = :id",
-                            lambda ctx: {'id': pid}
-                        ),
+                AllOf(
+                    CompiledSQL(
+                        "DELETE FROM nodes WHERE nodes.id = :id",
+                        lambda ctx: [{'id': c1id}, {'id': c2id}]
                     ),
                     CompiledSQL(
                         "DELETE FROM nodes WHERE nodes.id = :id",
-                        lambda ctx: [{'id': c1id}, {'id': c2id}, {'id': pid}]
+                        lambda ctx: {'id': pid}
                     ),
-
-                )
+                ),
             ),
         )