]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- Related to :ticket:`3060`, an adjustment has been made to the unit
authorMike Bayer <mike_mp@zzzcomputing.com>
Fri, 30 May 2014 05:32:53 +0000 (01:32 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 30 May 2014 05:32:53 +0000 (01:32 -0400)
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.

doc/build/changelog/changelog_09.rst
lib/sqlalchemy/orm/attributes.py
lib/sqlalchemy/orm/base.py
lib/sqlalchemy/orm/dependency.py
lib/sqlalchemy/orm/strategies.py
lib/sqlalchemy/orm/unitofwork.py
lib/sqlalchemy/orm/util.py
test/orm/test_unitofworkv2.py

index 240cc48f962558bf0c76dcda437289ffd888d342..e21117f0285e233ed525f110c24cd801cb9baaa8 100644 (file)
         which reverts the more patchwork version of the fix as it exists
         in 0.9.5.
 
+    .. 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
index 306c86e3badaae1b0264a92bf33fc3b3418d46d1..df1f328b731c3d3208775d106fc8d2ecd7d870d7 100644 (file)
@@ -23,8 +23,7 @@ from .base import PASSIVE_NO_RESULT, ATTR_WAS_SET, ATTR_EMPTY, NO_VALUE,\
             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
@@ -536,7 +535,7 @@ class AttributeImpl(object):
     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.
@@ -748,23 +747,31 @@ class ScalarObjectAttributeImpl(ScalarAttributeImpl):
             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):
@@ -863,7 +870,9 @@ class CollectionAttributeImpl(AttributeImpl):
         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 []
 
@@ -1091,7 +1100,9 @@ def backref_listeners(attribute, key, uselist):
     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),\
index 896041980999ee88e692d9d4412205467e0c0984..30603cba18fba10882ac02918f9707df1172a8ce 100644 (file)
@@ -152,7 +152,6 @@ NOT_EXTENSION = util.symbol('NOT_EXTENSION',
 """)
 
 _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):
index 68ae0a0e4c917e09ea14bb2729257d37905b6d1b..40d6bd77627db2029263086675d2727073e0c71b 100644 (file)
@@ -154,12 +154,16 @@ class DependencyProcessor(object):
                 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
index 2674b9c6f7f78c6668eab38223b87fb506ed0299..b1334636ab669902d9c8918031545b4ac9fd24d3 100644 (file)
@@ -220,7 +220,7 @@ class DeferredColumnLoader(LoaderStrategy):
         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
index 3ef2b2edf7395d696f09225c4af8a91254a39b5d..bb6a961b12a733f8ce17d0fc073e6177acc5a27f 100644 (file)
@@ -86,7 +86,9 @@ def track_cascade_events(descriptor, prop):
                     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)
index fd902adafa57051778c497160dce72af4e12155b..8694705a48f4c42427d66e4794f36e1951331309 100644 (file)
@@ -12,8 +12,7 @@ from . import attributes
 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
index 7025e087c5f35a3c17ca3c5db0d504897376d7b3..00cc044bfa37f1c4ce9f2d2d3680952d6f43b4c7 100644 (file)
@@ -1008,23 +1008,16 @@ class SingleCycleTest(UOWTest):
                     "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}
                     ),
-
-                )
+                ),
             ),
         )