]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- establish a consistent pattern of behavior along o2m, m2m, and m2o relationships
authorMike Bayer <mike_mp@zzzcomputing.com>
Thu, 18 Nov 2010 00:34:47 +0000 (19:34 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Thu, 18 Nov 2010 00:34:47 +0000 (19:34 -0500)
when "save-update" cascade is disabled, or the target object is otherwise not
present in the session, and collection/scalar changes have taken place.  A warning
is emitted describing the type of operation, the target reference, and the relationship
description, stating that the operation will not take place.  The operation then doesn't
take place.   [ticket:1973]
- clean up test_cascade a little bit, remove cruft

lib/sqlalchemy/orm/dependency.py
lib/sqlalchemy/orm/unitofwork.py
test/orm/test_cascade.py
test/orm/test_session.py

index 4458a85470e8cb4c44cc5baf757f0386fda40ad8..e57b1575ac7bc89d5fca62d327d4c86b109d3977 100644 (file)
@@ -405,7 +405,9 @@ class OneToManyDP(DependencyProcessor):
                 if should_null_fks:
                     for child in history.unchanged:
                         if child is not None:
-                            uowcommit.register_object(child)
+                            uowcommit.register_object(child, 
+                                    operation="delete", prop=self.prop)
+
         
             
     def presort_saves(self, uowcommit, states):
@@ -422,15 +424,20 @@ class OneToManyDP(DependencyProcessor):
             if history:
                 for child in history.added:
                     if child is not None:
-                        uowcommit.register_object(child, cancel_delete=True)
+                        uowcommit.register_object(child, cancel_delete=True, 
+                                                    operation="add", 
+                                                    prop=self.prop)
 
                 children_added.update(history.added)
 
                 for child in history.deleted:
                     if not self.cascade.delete_orphan:
-                        uowcommit.register_object(child, isdelete=False)
+                        uowcommit.register_object(child, isdelete=False, 
+                                                    operation='delete', 
+                                                    prop=self.prop)
                     elif self.hasparent(child) is False:
-                        uowcommit.register_object(child, isdelete=True)
+                        uowcommit.register_object(child, isdelete=True, 
+                                            operation="delete", prop=self.prop)
                         for c, m in self.mapper.cascade_iterator(
                                                     'delete', child):
                             uowcommit.register_object(
@@ -444,7 +451,9 @@ class OneToManyDP(DependencyProcessor):
                             uowcommit.register_object(
                                         child, 
                                         False, 
-                                        self.passive_updates)
+                                        self.passive_updates,
+                                        operation="pk change",
+                                        prop=self.prop)
         
     def process_deletes(self, uowcommit, states):
         # head object is being deleted, and we manage its list of 
@@ -649,7 +658,8 @@ class ManyToOneDP(DependencyProcessor):
                     for child in todelete:
                         if child is None:
                             continue
-                        uowcommit.register_object(child, isdelete=True)
+                        uowcommit.register_object(child, isdelete=True, 
+                                        operation="delete", prop=self.prop)
                         for c, m in self.mapper.cascade_iterator(
                                                             'delete', child):
                             uowcommit.register_object(
@@ -657,7 +667,7 @@ class ManyToOneDP(DependencyProcessor):
         
     def presort_saves(self, uowcommit, states):
         for state in states:
-            uowcommit.register_object(state)
+            uowcommit.register_object(state, operation="add", prop=self.prop)
             if self.cascade.delete_orphan:
                 history = uowcommit.get_attribute_history(
                                         state, 
@@ -667,7 +677,9 @@ class ManyToOneDP(DependencyProcessor):
                     ret = True
                     for child in history.deleted:
                         if self.hasparent(child) is False:
-                            uowcommit.register_object(child, isdelete=True)
+                            uowcommit.register_object(child, isdelete=True, 
+                                        operation="delete", prop=self.prop)
+                                            
                             for c, m in self.mapper.cascade_iterator(
                                                             'delete', child):
                                 uowcommit.register_object(
@@ -699,17 +711,27 @@ class ManyToOneDP(DependencyProcessor):
                                                         passive=True)
             if history:
                 for child in history.added:
-                    self._synchronize(state, child, None, False, uowcommit)
+                    self._synchronize(state, child, None, False, 
+                                            uowcommit, "add")
                 
                 if self.post_update:
                     self._post_update(state, uowcommit, history.sum())
 
-    def _synchronize(self, state, child, associationrow, 
-                                                    clearkeys, uowcommit):
+    def _synchronize(self, state, child, associationrow,
+                                        clearkeys, uowcommit, operation=None):
         if state is None or \
             (not self.post_update and uowcommit.is_deleted(state)):
             return
 
+        if operation is not None and \
+            child is not None and \
+            not uowcommit.session._contains_state(child):
+            util.warn(
+                "Child %s not in session, %s "
+                "operation along '%s' won't proceed" % 
+                (mapperutil.state_str(child), operation, self.prop))
+            return
+            
         if clearkeys or child is None:
             sync.clear(state, self.parent, self.prop.synchronize_pairs)
         else:
@@ -914,7 +936,8 @@ class ManyToManyDP(DependencyProcessor):
             if history:
                 for child in history.deleted:
                     if self.hasparent(child) is False:
-                        uowcommit.register_object(child, isdelete=True)
+                        uowcommit.register_object(child, isdelete=True, 
+                                            operation="delete", prop=self.prop)
                         for c, m in self.mapper.cascade_iterator(
                                                     'delete', 
                                                     child):
@@ -939,15 +962,15 @@ class ManyToManyDP(DependencyProcessor):
                 for child in history.non_added():
                     if child is None or \
                         (processed is not None and 
-                            (state, child) in processed) or \
-                        not uowcommit.session._contains_state(child):
+                            (state, child) in processed):
                         continue
                     associationrow = {}
-                    self._synchronize(
+                    if not self._synchronize(
                                         state, 
                                         child, 
                                         associationrow, 
-                                        False, uowcommit)
+                                        False, uowcommit, "delete"):
+                        continue
                     secondary_delete.append(associationrow)
                 
                 tmp.update((c, state) for c in history.non_added())
@@ -978,22 +1001,23 @@ class ManyToManyDP(DependencyProcessor):
                                 (state, child) in processed):
                         continue
                     associationrow = {}
-                    self._synchronize(state, 
+                    if not self._synchronize(state, 
                                         child, 
                                         associationrow, 
-                                        False, uowcommit)
+                                        False, uowcommit, "add"):
+                        continue
                     secondary_insert.append(associationrow)
                 for child in history.deleted:
                     if child is None or \
                             (processed is not None and 
-                            (state, child) in processed) or \
-                            not uowcommit.session._contains_state(child):
+                            (state, child) in processed):
                         continue
                     associationrow = {}
-                    self._synchronize(state, 
+                    if not self._synchronize(state, 
                                         child, 
                                         associationrow, 
-                                        False, uowcommit)
+                                        False, uowcommit, "delete"):
+                        continue
                     secondary_delete.append(associationrow)
                 
                 tmp.update((c, state) 
@@ -1066,16 +1090,27 @@ class ManyToManyDP(DependencyProcessor):
             connection.execute(statement, secondary_insert)
         
     def _synchronize(self, state, child, associationrow, 
-                                            clearkeys, uowcommit):
+                                            clearkeys, uowcommit, operation):
         if associationrow is None:
             return
+
+        if child is not None and not uowcommit.session._contains_state(child):
+            if not child.deleted:
+                util.warn(
+                    "Child %s not in session, %s "
+                    "operation along '%s' won't proceed" % 
+                    (mapperutil.state_str(child), operation, self.prop))
+            return False
+            
         self._verify_canload(child)
         
         sync.populate_dict(state, self.parent, associationrow, 
                                         self.prop.synchronize_pairs)
         sync.populate_dict(child, self.mapper, associationrow,
                                         self.prop.secondary_synchronize_pairs)
-
+        
+        return True
+        
     def _pks_changed(self, uowcommit, state):
         return sync.source_modified(
                             uowcommit, 
index 673591e8e352b082ce35c28bfe482696c05ecb10..8f7475c4b7daccdcf42fa3cad154aa23000486a6 100644 (file)
@@ -176,9 +176,14 @@ class UOWTransaction(object):
             self.presort_actions[key] = Preprocess(processor, fromparent)
             
     def register_object(self, state, isdelete=False, 
-                            listonly=False, cancel_delete=False):
+                            listonly=False, cancel_delete=False,
+                            operation=None, prop=None):
         if not self.session._contains_state(state):
-            return
+            if not state.deleted and operation is not None:
+                util.warn("Object %s not in session, %s operation "
+                            "along '%s' will not proceed" % 
+                            (mapperutil.state_str(state), operation, prop))
+            return False
 
         if state not in self.states:
             mapper = _state_mapper(state)
@@ -191,7 +196,8 @@ class UOWTransaction(object):
         else:
             if not listonly and (isdelete or cancel_delete):
                 self.states[state] = (isdelete, False)
-    
+        return True
+        
     def issue_post_update(self, state, post_update_cols):
         mapper = state.manager.mapper.base_mapper
         states, cols = self.post_update_states[mapper]
index 75b9e22ec1c5ebeb2fadcda0144ad9c2a6224f1b..ca38f918c826ab68d1d2f5921640c2b1048d508e 100644 (file)
@@ -11,7 +11,7 @@ from sqlalchemy.test.testing import eq_
 from test.orm import _base, _fixtures
 
 
-class O2MCascadeTest(_fixtures.FixtureTest):
+class O2MCascadeDeleteOrphanTest(_fixtures.FixtureTest):
     run_inserts = None
 
     @classmethod
@@ -27,23 +27,30 @@ class O2MCascadeTest(_fixtures.FixtureTest):
                : relationship(Address)})
 
     @testing.resolve_artifact_names
-    def test_list_assignment(self):
-        sess = create_session()
+    def test_list_assignment_new(self):
+        sess = Session()
         u = User(name='jack', orders=[
                  Order(description='someorder'),
                  Order(description='someotherorder')])
         sess.add(u)
-        sess.flush()
-        sess.expunge_all()
+        sess.commit()
 
         u = sess.query(User).get(u.id)
         eq_(u, User(name='jack',
                     orders=[Order(description='someorder'),
                             Order(description='someotherorder')]))
 
+    @testing.resolve_artifact_names
+    def test_list_assignment_replace(self):
+        sess = Session()
+        u = User(name='jack', orders=[
+                 Order(description='someorder'),
+                 Order(description='someotherorder')])
+        sess.add(u)
+        sess.commit()
+
         u.orders=[Order(description="order 3"), Order(description="order 4")]
-        sess.flush()
-        sess.expunge_all()
+        sess.commit()
 
         u = sess.query(User).get(u.id)
         eq_(u, User(name='jack',
@@ -53,6 +60,9 @@ class O2MCascadeTest(_fixtures.FixtureTest):
         eq_(sess.query(Order).order_by(Order.id).all(),
             [Order(description="order 3"), Order(description="order 4")])
 
+    @testing.resolve_artifact_names
+    def test_standalone_orphan(self):
+        sess = Session()
         o5 = Order(description="order 5")
         sess.add(o5)
         assert_raises_message(orm_exc.FlushError, "is an orphan", sess.flush)
@@ -228,32 +238,476 @@ class O2OCascadeTest(_fixtures.FixtureTest):
         assert u1.address is not a1
         assert a1.user is None
         
+class NoSaveCascadeFlushTest(_fixtures.FixtureTest):
+    """Test related item not present in session, commit proceeds."""
+    
+    @testing.resolve_artifact_names
+    def _one_to_many_fixture(self, o2m_cascade=True, 
+                                    m2o_cascade=True, 
+                                    o2m=False,
+                                    m2o=False,
+                                    o2m_cascade_backrefs=True,
+                                    m2o_cascade_backrefs=True):
         
+        if o2m:
+            if m2o:
+                addresses_rel = {'addresses':relationship(
+                                Address, 
+                                cascade_backrefs=o2m_cascade_backrefs,
+                                cascade=o2m_cascade and 'save-update' or '',
+                                backref=backref('user', 
+                                            cascade=m2o_cascade and 'save-update' or '',
+                                            cascade_backrefs=m2o_cascade_backrefs
+                                        )
+                                )}
+                                
+            else:
+                addresses_rel = {'addresses':relationship(
+                                Address, 
+                                cascade=o2m_cascade and 'save-update' or '',
+                                cascade_backrefs=o2m_cascade_backrefs,
+                                )}
+            user_rel = {}
+        elif m2o:
+            user_rel = {'user':relationship(User,
+                                cascade=m2o_cascade and 'save-update' or '',
+                                cascade_backrefs=m2o_cascade_backrefs
+                            )}
+            addresses_rel = {}
+        else:
+            addresses_rel = {}
+            user_rel = {}
         
-class O2MBackrefTest(_fixtures.FixtureTest):
-    run_inserts = None
+        mapper(User, users, properties=addresses_rel)
+        mapper(Address, addresses, properties=user_rel)
 
-    @classmethod
     @testing.resolve_artifact_names
-    def setup_mappers(cls):
-        mapper(User, users,
-               properties=dict(orders=relationship(mapper(Order,
-               orders), cascade='all, delete-orphan', backref='user')))
+    def _many_to_many_fixture(self, fwd_cascade=True, 
+                                    bkd_cascade=True, 
+                                    fwd=False,
+                                    bkd=False,
+                                    fwd_cascade_backrefs=True,
+                                    bkd_cascade_backrefs=True):
+        
+        if fwd:
+            if bkd:
+                keywords_rel = {'keywords':relationship(
+                                Keyword, 
+                                secondary=item_keywords,
+                                cascade_backrefs=fwd_cascade_backrefs,
+                                cascade=fwd_cascade and 'save-update' or '',
+                                backref=backref('items', 
+                                            cascade=bkd_cascade and 'save-update' or '',
+                                            cascade_backrefs=bkd_cascade_backrefs
+                                        )
+                                )}
+                                
+            else:
+                keywords_rel = {'keywords':relationship(
+                                Keyword, 
+                                secondary=item_keywords,
+                                cascade=fwd_cascade and 'save-update' or '',
+                                cascade_backrefs=fwd_cascade_backrefs,
+                                )}
+            items_rel = {}
+        elif bkd:
+            items_rel = {'items':relationship(Item,
+                                secondary=item_keywords,
+                                cascade=bkd_cascade and 'save-update' or '',
+                                cascade_backrefs=bkd_cascade_backrefs
+                            )}
+            keywords_rel = {}
+        else:
+            keywords_rel = {}
+            items_rel = {}
+        
+        mapper(Item, items, properties=keywords_rel)
+        mapper(Keyword, keywords, properties=items_rel)
 
     @testing.resolve_artifact_names
-    def test_lazyload_bug(self):
-        sess = create_session()
+    def test_o2m_only_child_pending(self):
+        self._one_to_many_fixture(o2m=True, m2o=False)
+        sess = Session()
+        u1 = User(name='u1')
+        a1 = Address(email_address='a1')
+        u1.addresses.append(a1)
+        sess.add(u1)
+        assert u1 in sess
+        assert a1 in sess
+        sess.flush()
+        
+    @testing.resolve_artifact_names
+    def test_o2m_only_child_transient(self):
+        self._one_to_many_fixture(o2m=True, m2o=False, o2m_cascade=False)
+        sess = Session()
+        u1 = User(name='u1')
+        a1 = Address(email_address='a1')
+        u1.addresses.append(a1)
+        sess.add(u1)
+        assert u1 in sess
+        assert a1 not in sess
+        assert_raises_message(
+            sa_exc.SAWarning, "not in session", sess.flush
+        )
 
-        u = User(name="jack")
-        sess.add(u)
-        sess.expunge(u)
+    @testing.resolve_artifact_names
+    def test_o2m_only_child_persistent(self):
+        self._one_to_many_fixture(o2m=True, m2o=False, o2m_cascade=False)
+        sess = Session()
+        u1 = User(name='u1')
+        a1 = Address(email_address='a1')
+        sess.add(a1)
+        sess.flush()
+        
+        sess.expunge_all()
+        
+        u1.addresses.append(a1)
+        sess.add(u1)
+        assert u1 in sess
+        assert a1 not in sess
+        assert_raises_message(
+            sa_exc.SAWarning, "not in session", sess.flush
+        )
+        
+    @testing.resolve_artifact_names
+    def test_o2m_backref_child_pending(self):
+        self._one_to_many_fixture(o2m=True, m2o=True)
+        sess = Session()
+        u1 = User(name='u1')
+        a1 = Address(email_address='a1')
+        u1.addresses.append(a1)
+        sess.add(u1)
+        assert u1 in sess
+        assert a1 in sess
+        sess.flush()
 
-        o1 = Order(description='someorder')
-        o1.user = u
-        sess.add(u)
-        assert u in sess
-        assert o1 in sess
+    @testing.resolve_artifact_names
+    def test_o2m_backref_child_transient(self):
+        self._one_to_many_fixture(o2m=True, m2o=True, 
+                                    o2m_cascade=False)
+        sess = Session()
+        u1 = User(name='u1')
+        a1 = Address(email_address='a1')
+        u1.addresses.append(a1)
+        sess.add(u1)
+        assert u1 in sess
+        assert a1 not in sess
+        assert_raises_message(
+            sa_exc.SAWarning, "not in session", sess.flush
+        )
+
+    @testing.resolve_artifact_names
+    def test_o2m_backref_child_transient_nochange(self):
+        self._one_to_many_fixture(o2m=True, m2o=True, 
+                                    o2m_cascade=False)
+        sess = Session()
+        u1 = User(name='u1')
+        a1 = Address(email_address='a1')
+        u1.addresses.append(a1)
+        sess.add(u1)
+        assert u1 in sess
+        assert a1 not in sess
+        @testing.emits_warning(r'.*not in session')
+        def go():
+            sess.commit()
+        go()
+        eq_(u1.addresses, [])
+        
+    @testing.resolve_artifact_names
+    def test_o2m_backref_child_expunged(self):
+        self._one_to_many_fixture(o2m=True, m2o=True, 
+                                    o2m_cascade=False)
+        sess = Session()
+        u1 = User(name='u1')
+        a1 = Address(email_address='a1')
+        sess.add(a1)
+        sess.flush()
+        
+        sess.add(u1)
+        u1.addresses.append(a1)
+        sess.expunge(a1)
+        assert u1 in sess
+        assert a1 not in sess
+        assert_raises_message(
+            sa_exc.SAWarning, "not in session", sess.flush
+        )
+
+    @testing.resolve_artifact_names
+    def test_o2m_backref_child_expunged_nochange(self):
+        self._one_to_many_fixture(o2m=True, m2o=True, 
+                                    o2m_cascade=False)
+        sess = Session()
+        u1 = User(name='u1')
+        a1 = Address(email_address='a1')
+        sess.add(a1)
+        sess.flush()
+        
+        sess.add(u1)
+        u1.addresses.append(a1)
+        sess.expunge(a1)
+        assert u1 in sess
+        assert a1 not in sess
+        @testing.emits_warning(r'.*not in session')
+        def go():
+            sess.commit()
+        go()
+        eq_(u1.addresses, [])
+
+    @testing.resolve_artifact_names
+    def test_m2o_only_child_pending(self):
+        self._one_to_many_fixture(o2m=False, m2o=True)
+        sess = Session()
+        u1 = User(name='u1')
+        a1 = Address(email_address='a1')
+        a1.user = u1
+        sess.add(a1)
+        assert u1 in sess
+        assert a1 in sess
+        sess.flush()
+
+    @testing.resolve_artifact_names
+    def test_m2o_only_child_transient(self):
+        self._one_to_many_fixture(o2m=False, m2o=True, m2o_cascade=False)
+        sess = Session()
+        u1 = User(name='u1')
+        a1 = Address(email_address='a1')
+        a1.user = u1
+        sess.add(a1)
+        assert u1 not in sess
+        assert a1 in sess
+        assert_raises_message(
+            sa_exc.SAWarning, "not in session", sess.flush
+        )
+
+    @testing.resolve_artifact_names
+    def test_m2o_only_child_expunged(self):
+        self._one_to_many_fixture(o2m=False, m2o=True, m2o_cascade=False)
+        sess = Session()
+        u1 = User(name='u1')
+        sess.add(u1)
+        sess.flush()
+        
+        a1 = Address(email_address='a1')
+        a1.user = u1
+        sess.add(a1)
+        sess.expunge(u1)
+        assert u1 not in sess
+        assert a1 in sess
+        assert_raises_message(
+            sa_exc.SAWarning, "not in session", sess.flush
+        )
+
+    @testing.resolve_artifact_names
+    def test_m2o_backref_child_pending(self):
+        self._one_to_many_fixture(o2m=True, m2o=True)
+        sess = Session()
+        u1 = User(name='u1')
+        a1 = Address(email_address='a1')
+        a1.user = u1
+        sess.add(a1)
+        assert u1 in sess
+        assert a1 in sess
+        sess.flush()
+
+    @testing.resolve_artifact_names
+    def test_m2o_backref_child_transient(self):
+        self._one_to_many_fixture(o2m=True, m2o=True, m2o_cascade=False)
+        sess = Session()
+        u1 = User(name='u1')
+        a1 = Address(email_address='a1')
+        a1.user = u1
+        sess.add(a1)
+        assert u1 not in sess
+        assert a1 in sess
+        assert_raises_message(
+            sa_exc.SAWarning, "not in session", sess.flush
+        )
+
+    @testing.resolve_artifact_names
+    def test_m2o_backref_child_expunged(self):
+        self._one_to_many_fixture(o2m=True, m2o=True, m2o_cascade=False)
+        sess = Session()
+        u1 = User(name='u1')
+        sess.add(u1)
+        sess.flush()
+        
+        a1 = Address(email_address='a1')
+        a1.user = u1
+        sess.add(a1)
+        sess.expunge(u1)
+        assert u1 not in sess
+        assert a1 in sess
+        assert_raises_message(
+            sa_exc.SAWarning, "not in session", sess.flush
+        )
+
+    @testing.resolve_artifact_names
+    def test_m2o_backref_child_pending_nochange(self):
+        self._one_to_many_fixture(o2m=True, m2o=True, m2o_cascade=False)
+        sess = Session()
+        u1 = User(name='u1')
+        
+        a1 = Address(email_address='a1')
+        a1.user = u1
+        sess.add(a1)
+        assert u1 not in sess
+        assert a1 in sess
+        @testing.emits_warning(r'.*not in session')
+        def go():
+            sess.commit()
+        go()
+        # didn't get flushed
+        assert a1.user is None
+
+    @testing.resolve_artifact_names
+    def test_m2o_backref_child_expunged_nochange(self):
+        self._one_to_many_fixture(o2m=True, m2o=True, m2o_cascade=False)
+        sess = Session()
+        u1 = User(name='u1')
+        sess.add(u1)
+        sess.flush()
+        
+        a1 = Address(email_address='a1')
+        a1.user = u1
+        sess.add(a1)
+        sess.expunge(u1)
+        assert u1 not in sess
+        assert a1 in sess
+        @testing.emits_warning(r'.*not in session')
+        def go():
+            sess.commit()
+        go()
+        # didn't get flushed
+        assert a1.user is None
+
+    @testing.resolve_artifact_names
+    def test_m2m_only_child_pending(self):
+        self._many_to_many_fixture(fwd=True, bkd=False)
+        sess = Session()
+        i1 = Item(description='i1')
+        k1 = Keyword(name='k1')
+        i1.keywords.append(k1)
+        sess.add(i1)
+        assert i1 in sess
+        assert k1 in sess
+        sess.flush()
+        
+    @testing.resolve_artifact_names
+    def test_m2m_only_child_transient(self):
+        self._many_to_many_fixture(fwd=True, bkd=False, fwd_cascade=False)
+        sess = Session()
+        i1 = Item(description='i1')
+        k1 = Keyword(name='k1')
+        i1.keywords.append(k1)
+        sess.add(i1)
+        assert i1 in sess
+        assert k1 not in sess
+        assert_raises_message(
+            sa_exc.SAWarning, "not in session", sess.flush
+        )
+
+    @testing.resolve_artifact_names
+    def test_m2m_only_child_persistent(self):
+        self._many_to_many_fixture(fwd=True, bkd=False, fwd_cascade=False)
+        sess = Session()
+        i1 = Item(description='i1')
+        k1 = Keyword(name='k1')
+        sess.add(k1)
+        sess.flush()
+        
+        sess.expunge_all()
+        
+        i1.keywords.append(k1)
+        sess.add(i1)
+        assert i1 in sess
+        assert k1 not in sess
+        assert_raises_message(
+            sa_exc.SAWarning, "not in session", sess.flush
+        )
+        
+    @testing.resolve_artifact_names
+    def test_m2m_backref_child_pending(self):
+        self._many_to_many_fixture(fwd=True, bkd=True)
+        sess = Session()
+        i1 = Item(description='i1')
+        k1 = Keyword(name='k1')
+        i1.keywords.append(k1)
+        sess.add(i1)
+        assert i1 in sess
+        assert k1 in sess
+        sess.flush()
 
+    @testing.resolve_artifact_names
+    def test_m2m_backref_child_transient(self):
+        self._many_to_many_fixture(fwd=True, bkd=True, 
+                                    fwd_cascade=False)
+        sess = Session()
+        i1 = Item(description='i1')
+        k1 = Keyword(name='k1')
+        i1.keywords.append(k1)
+        sess.add(i1)
+        assert i1 in sess
+        assert k1 not in sess
+        assert_raises_message(
+            sa_exc.SAWarning, "not in session", sess.flush
+        )
+
+    @testing.resolve_artifact_names
+    def test_m2m_backref_child_transient_nochange(self):
+        self._many_to_many_fixture(fwd=True, bkd=True, 
+                                    fwd_cascade=False)
+        sess = Session()
+        i1 = Item(description='i1')
+        k1 = Keyword(name='k1')
+        i1.keywords.append(k1)
+        sess.add(i1)
+        assert i1 in sess
+        assert k1 not in sess
+        @testing.emits_warning(r'.*not in session')
+        def go():
+            sess.commit()
+        go()
+        eq_(i1.keywords, [])
+        
+    @testing.resolve_artifact_names
+    def test_m2m_backref_child_expunged(self):
+        self._many_to_many_fixture(fwd=True, bkd=True, 
+                                    fwd_cascade=False)
+        sess = Session()
+        i1 = Item(description='i1')
+        k1 = Keyword(name='k1')
+        sess.add(k1)
+        sess.flush()
+        
+        sess.add(i1)
+        i1.keywords.append(k1)
+        sess.expunge(k1)
+        assert i1 in sess
+        assert k1 not in sess
+        assert_raises_message(
+            sa_exc.SAWarning, "not in session", sess.flush
+        )
+
+    @testing.resolve_artifact_names
+    def test_m2m_backref_child_expunged_nochange(self):
+        self._many_to_many_fixture(fwd=True, bkd=True, 
+                                    fwd_cascade=False)
+        sess = Session()
+        i1 = Item(description='i1')
+        k1 = Keyword(name='k1')
+        sess.add(k1)
+        sess.flush()
+        
+        sess.add(i1)
+        i1.keywords.append(k1)
+        sess.expunge(k1)
+        assert i1 in sess
+        assert k1 not in sess
+        @testing.emits_warning(r'.*not in session')
+        def go():
+            sess.commit()
+        go()
+        eq_(i1.keywords, [])
 
 class NoSaveCascadeTest(_fixtures.FixtureTest):
     """test that backrefs don't force save-update cascades to occur
@@ -332,7 +786,7 @@ class NoSaveCascadeTest(_fixtures.FixtureTest):
         k1.items.append(i1)
         assert i1 in sess
         assert k1 not in sess
-        
+
     
 class O2MCascadeNoOrphanTest(_fixtures.FixtureTest):
     run_inserts = None
@@ -957,7 +1411,7 @@ class NoBackrefCascadeTest(_fixtures.FixtureTest):
         })
 
     @testing.resolve_artifact_names
-    def test_o2m(self):
+    def test_o2m_basic(self):
         sess = Session()
         
         u1 = User(name='u1')
@@ -967,10 +1421,31 @@ class NoBackrefCascadeTest(_fixtures.FixtureTest):
         a1.user = u1
         assert a1 not in sess
 
-        sess.commit()
+
+    @testing.resolve_artifact_names
+    def test_o2m_commit_warns(self):
+        sess = Session()
+        
+        u1 = User(name='u1')
+        sess.add(u1)
+        
+        a1 = Address(email_address='a1')
+        a1.user = u1
+        
+        assert_raises_message(
+            sa_exc.SAWarning,
+            "not in session",
+            sess.commit
+        )
         
         assert a1 not in sess
         
+
+    @testing.resolve_artifact_names
+    def test_o2m_flag_on_backref(self):
+        sess = Session()
+        
+        a1 = Address(email_address='a1')
         sess.add(a1)
         
         d1 = Dingaling()
@@ -981,7 +1456,7 @@ class NoBackrefCascadeTest(_fixtures.FixtureTest):
         sess.commit()
 
     @testing.resolve_artifact_names
-    def test_m2o(self):
+    def test_m2o_basic(self):
         sess = Session()
 
         a1 = Address(email_address='a1')
@@ -990,15 +1465,34 @@ class NoBackrefCascadeTest(_fixtures.FixtureTest):
         
         a1.dingalings.append(d1)
         assert a1 not in sess
-    
-        a2 = Address(email_address='a2')
-        sess.add(a2)
+
+    @testing.resolve_artifact_names
+    def test_m2o_flag_on_backref(self):
+        sess = Session()
+
+        a1 = Address(email_address='a1')
+        sess.add(a1)
         
         u1 = User(name='u1')
-        u1.addresses.append(a2)
+        u1.addresses.append(a1)
         assert u1 in sess
 
-        sess.commit()
+    @testing.resolve_artifact_names
+    def test_m2o_commit_warns(self):
+        sess = Session()
+
+        a1 = Address(email_address='a1')
+        d1 = Dingaling()
+        sess.add(d1)
+        
+        a1.dingalings.append(d1)
+        assert a1 not in sess
+
+        assert_raises_message(
+            sa_exc.SAWarning,
+            "not in session",
+            sess.commit
+        )
 
 class UnsavedOrphansTest(_base.MappedTest):
     """Pending entities that are orphans"""
@@ -1395,7 +1889,7 @@ class CollectionAssignmentOrphanTest(_base.MappedTest):
 
 class O2MConflictTest(_base.MappedTest):
     """test that O2M dependency detects a change in parent, does the
-    right thing, and even updates the collection/attribute.
+    right thing, and updates the collection/attribute.
     
     """
     
@@ -1544,9 +2038,7 @@ class O2MConflictTest(_base.MappedTest):
         
 
 class PartialFlushTest(_base.MappedTest):
-    """test cascade behavior as it relates to object lists passed to flush().
-    
-    """
+    """test cascade behavior as it relates to object lists passed to flush()."""
     @classmethod
     def define_tables(cls, metadata):
         Table("base", metadata,
index 6ac42a6b38497e691a65ef82797e496858bfc5d0..7bfaf51316ef1143afee89ce63284c1376eef421 100644 (file)
@@ -1125,45 +1125,6 @@ class SessionTest(_fixtures.FixtureTest):
         self.assert_(s.prune() == 0)
         self.assert_(len(s.identity_map) == 0)
 
-    @testing.resolve_artifact_names
-    def test_no_save_cascade_1(self):
-        mapper(Address, addresses)
-        mapper(User, users, properties=dict(
-            addresses=relationship(Address, cascade="none", backref="user")))
-        s = create_session()
-
-        u = User(name='u1')
-        s.add(u)
-        a = Address(email_address='u1@e')
-        u.addresses.append(a)
-        assert u in s
-        assert a not in s
-        s.flush()
-        print "\n".join([repr(x.__dict__) for x in s])
-        s.expunge_all()
-        assert s.query(User).one().id == u.id
-        assert s.query(Address).first() is None
-
-    @testing.resolve_artifact_names
-    def test_no_save_cascade_2(self):
-        mapper(Address, addresses)
-        mapper(User, users, properties=dict(
-            addresses=relationship(Address,
-                               cascade="all",
-                               backref=backref("user", cascade="none"))))
-
-        s = create_session()
-        u = User(name='u1')
-        a = Address(email_address='u1@e')
-        a.user = u
-        s.add(a)
-        assert u not in s
-        assert a in s
-        s.flush()
-        s.expunge_all()
-        assert s.query(Address).one().id == a.id
-        assert s.query(User).first() is None
-
     @testing.resolve_artifact_names
     def test_extension(self):
         mapper(User, users)