From: Mike Bayer Date: Thu, 18 Nov 2010 00:34:47 +0000 (-0500) Subject: - establish a consistent pattern of behavior along o2m, m2m, and m2o relationships X-Git-Tag: rel_0_7b1~247^2~1 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=1321a0c2b95ce4381157d0633f2d292a1d286151;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - establish a consistent pattern of behavior along o2m, m2m, and m2o relationships 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 --- diff --git a/lib/sqlalchemy/orm/dependency.py b/lib/sqlalchemy/orm/dependency.py index 4458a85470..e57b1575ac 100644 --- a/lib/sqlalchemy/orm/dependency.py +++ b/lib/sqlalchemy/orm/dependency.py @@ -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, diff --git a/lib/sqlalchemy/orm/unitofwork.py b/lib/sqlalchemy/orm/unitofwork.py index 673591e8e3..8f7475c4b7 100644 --- a/lib/sqlalchemy/orm/unitofwork.py +++ b/lib/sqlalchemy/orm/unitofwork.py @@ -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] diff --git a/test/orm/test_cascade.py b/test/orm/test_cascade.py index 75b9e22ec1..ca38f918c8 100644 --- a/test/orm/test_cascade.py +++ b/test/orm/test_cascade.py @@ -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, diff --git a/test/orm/test_session.py b/test/orm/test_session.py index 6ac42a6b38..7bfaf51316 100644 --- a/test/orm/test_session.py +++ b/test/orm/test_session.py @@ -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)