From d127f4936acb28dc00efd84678336a2be935a312 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Thu, 28 Oct 2021 17:10:15 -0400 Subject: [PATCH] warnings: cascade_backrefs this one is a little different in that the thing changing is the detection of a behavior, not an explicit API. Change-Id: Id142943a2b901b39fe9053d0120c1e820dc1a6d0 --- doc/build/orm/tutorial.rst | 4 + lib/sqlalchemy/orm/strategies.py | 1 - lib/sqlalchemy/testing/warnings.py | 2 - test/ext/test_associationproxy.py | 4 +- test/orm/test_backref_mutations.py | 7 +- test/orm/test_cascade.py | 29 +-- test/orm/test_deprecations.py | 247 +++++++++++++++++++++ test/orm/test_expire.py | 5 +- test/orm/test_lazy_relations.py | 2 +- test/orm/test_load_on_fks.py | 339 ++++++++++++++++------------- test/orm/test_mapper.py | 4 +- test/orm/test_merge.py | 4 +- test/orm/test_naturalpks.py | 2 +- test/orm/test_session.py | 5 +- test/orm/test_versioning.py | 2 +- 15 files changed, 474 insertions(+), 183 deletions(-) diff --git a/doc/build/orm/tutorial.rst b/doc/build/orm/tutorial.rst index 1f99a503a4..fab80da4a8 100644 --- a/doc/build/orm/tutorial.rst +++ b/doc/build/orm/tutorial.rst @@ -347,6 +347,10 @@ connect it to the :class:`~sqlalchemy.orm.session.Session` using >>> Session.configure(bind=engine) # once engine is available +.. Setup code, not for display - ensure no cascade_backrefs warnings occur + + >>> Session.configure(future=True) + .. sidebar:: Session Lifecycle Patterns The question of when to make a :class:`.Session` depends a lot on what diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index 4f361be2c9..2a283caad6 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -797,7 +797,6 @@ class LazyLoader(AbstractRelationshipLoader, util.MemoizedSlots): ) def _load_for_state(self, state, passive, loadopt=None, extra_criteria=()): - if not state.key and ( ( not self.parent_property.load_on_pending diff --git a/lib/sqlalchemy/testing/warnings.py b/lib/sqlalchemy/testing/warnings.py index dc1286295c..45d831fe1c 100644 --- a/lib/sqlalchemy/testing/warnings.py +++ b/lib/sqlalchemy/testing/warnings.py @@ -75,8 +75,6 @@ def setup_filters(): # ORM Session # r"The Session.autocommit parameter is deprecated ", - r".*object is being merged into a Session along the backref " - "cascade path", r"The merge_result\(\) method is superseded by the " r"merge_frozen_result\(\)", r"The Session.begin.subtransactions flag is deprecated", diff --git a/test/ext/test_associationproxy.py b/test/ext/test_associationproxy.py index 258ecb90c4..3e6652d1e4 100644 --- a/test/ext/test_associationproxy.py +++ b/test/ext/test_associationproxy.py @@ -146,7 +146,9 @@ class AutoFlushTest(fixtures.MappedTest): collection_class, is_dict=is_dict ) - session = Session(testing.db, autoflush=True, expire_on_commit=True) + session = Session( + testing.db, autoflush=True, expire_on_commit=True, future=True + ) p1 = Parent() c1 = Child("c1") diff --git a/test/orm/test_backref_mutations.py b/test/orm/test_backref_mutations.py index fd5d908cf5..0f10cff248 100644 --- a/test/orm/test_backref_mutations.py +++ b/test/orm/test_backref_mutations.py @@ -43,7 +43,7 @@ class O2MCollectionTest(_fixtures.FixtureTest): def test_collection_move_hitslazy(self): User, Address = self.classes.User, self.classes.Address - sess = fixture_session() + sess = fixture_session(future=True) a1 = Address(email_address="address1") a2 = Address(email_address="address2") a3 = Address(email_address="address3") @@ -667,7 +667,7 @@ class O2OScalarOrphanTest(_fixtures.FixtureTest): def test_m2o_event(self): User, Address = self.classes.User, self.classes.Address - sess = fixture_session() + sess = fixture_session(future=True) a1 = Address(email_address="address1") u1 = User(name="jack", address=a1) @@ -678,6 +678,7 @@ class O2OScalarOrphanTest(_fixtures.FixtureTest): u2 = User(name="ed") # the _SingleParent extension sets the backref get to "active" ! # u1 gets loaded and deleted + sess.add(u2) u2.address = a1 sess.commit() assert sess.query(User).count() == 1 @@ -712,7 +713,7 @@ class M2MCollectionMoveTest(_fixtures.FixtureTest): Item, Keyword = (self.classes.Item, self.classes.Keyword) - session = fixture_session(autoflush=False) + session = fixture_session(autoflush=False, future=True) i1 = Item(description="i1") session.add(i1) diff --git a/test/orm/test_cascade.py b/test/orm/test_cascade.py index 8749a01473..cd7e7c111a 100644 --- a/test/orm/test_cascade.py +++ b/test/orm/test_cascade.py @@ -4485,7 +4485,15 @@ class ViewonlyFlagWarningTest(fixtures.MappedTest): ) -class CollectionCascadesDespiteBackrefTest(fixtures.TestBase): +class CollectionCascadesNoBackrefTest(fixtures.TestBase): + """test the removal of cascade_backrefs behavior + + + see test/orm/test_deprecations.py::CollectionCascadesDespiteBackrefTest + for the deprecated version + + """ + @testing.fixture def cascade_fixture(self, registry): def go(collection_class): @@ -4495,7 +4503,10 @@ class CollectionCascadesDespiteBackrefTest(fixtures.TestBase): id = Column(Integer, primary_key=True) bs = relationship( - "B", backref="a", collection_class=collection_class + "B", + backref="a", + collection_class=collection_class, + cascade_backrefs=False, ) @registry.mapped @@ -4536,12 +4547,8 @@ class CollectionCascadesDespiteBackrefTest(fixtures.TestBase): b1.a = a1 b3.a = a1 - if future: - assert b1 not in s - assert b3 not in s - else: - assert b1 in s - assert b3 in s + assert b1 not in s + assert b3 not in s if methname == "__setitem__": meth = getattr(a1.bs, methname) @@ -4563,8 +4570,4 @@ class CollectionCascadesDespiteBackrefTest(fixtures.TestBase): assert b1 in s assert b2 in s - if future: - assert b3 not in s # the event never triggers from reverse - else: - # old behavior - assert b3 in s + assert b3 not in s # the event never triggers from reverse diff --git a/test/orm/test_deprecations.py b/test/orm/test_deprecations.py index 611754bdb3..3eb50ebee9 100644 --- a/test/orm/test_deprecations.py +++ b/test/orm/test_deprecations.py @@ -59,6 +59,7 @@ from sqlalchemy.orm import undefer from sqlalchemy.orm import with_loader_criteria from sqlalchemy.orm import with_parent from sqlalchemy.orm import with_polymorphic +from sqlalchemy.orm.collections import attribute_mapped_collection from sqlalchemy.orm.collections import collection from sqlalchemy.orm.util import polymorphic_union from sqlalchemy.sql import elements @@ -8456,3 +8457,249 @@ class ParentTest(QueryTest, AssertsCompiledSQL): "FROM addresses WHERE :param_2 = addresses.user_id) AS anon_1", checkparams={"param_1": 7, "param_2": 8}, ) + + +class CollectionCascadesDespiteBackrefTest(fixtures.TestBase): + """test old cascade_backrefs behavior + + see test/orm/test_cascade.py::class CollectionCascadesNoBackrefTest + for the future version + + """ + + @testing.fixture + def cascade_fixture(self, registry): + def go(collection_class): + @registry.mapped + class A(object): + __tablename__ = "a" + + id = Column(Integer, primary_key=True) + bs = relationship( + "B", backref="a", collection_class=collection_class + ) + + @registry.mapped + class B(object): + __tablename__ = "b_" + id = Column(Integer, primary_key=True) + a_id = Column(ForeignKey("a.id")) + key = Column(String) + + return A, B + + yield go + + @testing.combinations( + (set, "add"), + (list, "append"), + (attribute_mapped_collection("key"), "__setitem__"), + (attribute_mapped_collection("key"), "setdefault"), + (attribute_mapped_collection("key"), "update_dict"), + (attribute_mapped_collection("key"), "update_kw"), + argnames="collection_class,methname", + ) + @testing.combinations((True,), (False,), argnames="future") + def test_cascades_on_collection( + self, cascade_fixture, collection_class, methname, future + ): + A, B = cascade_fixture(collection_class) + + s = Session(future=future) + + a1 = A() + s.add(a1) + + b1 = B(key="b1") + b2 = B(key="b2") + b3 = B(key="b3") + + if future: + dep_ctx = util.nullcontext + else: + + def dep_ctx(): + return assertions.expect_deprecated_20( + '"B" object is being merged into a Session along the ' + 'backref cascade path for relationship "A.bs"' + ) + + with dep_ctx(): + b1.a = a1 + with dep_ctx(): + b3.a = a1 + + if future: + assert b1 not in s + assert b3 not in s + else: + assert b1 in s + assert b3 in s + + if methname == "__setitem__": + meth = getattr(a1.bs, methname) + meth(b1.key, b1) + meth(b2.key, b2) + elif methname == "setdefault": + meth = getattr(a1.bs, methname) + meth(b1.key, b1) + meth(b2.key, b2) + elif methname == "update_dict" and isinstance(a1.bs, dict): + a1.bs.update({b1.key: b1, b2.key: b2}) + elif methname == "update_kw" and isinstance(a1.bs, dict): + a1.bs.update(b1=b1, b2=b2) + else: + meth = getattr(a1.bs, methname) + meth(b1) + meth(b2) + + assert b1 in s + assert b2 in s + + # future version: + if future: + assert b3 not in s # the event never triggers from reverse + else: + # old behavior + assert b3 in s + + +class LoadOnFKsTest(fixtures.DeclarativeMappedTest): + @classmethod + def setup_classes(cls): + Base = cls.DeclarativeBasic + + class Parent(Base): + __tablename__ = "parent" + __table_args__ = {"mysql_engine": "InnoDB"} + + id = Column( + Integer, primary_key=True, test_needs_autoincrement=True + ) + + class Child(Base): + __tablename__ = "child" + __table_args__ = {"mysql_engine": "InnoDB"} + + id = Column( + Integer, primary_key=True, test_needs_autoincrement=True + ) + parent_id = Column(Integer, ForeignKey("parent.id")) + + parent = relationship(Parent, backref=backref("children")) + + @testing.fixture + def parent_fixture(self, connection): + Parent, Child = self.classes("Parent", "Child") + + sess = fixture_session(bind=connection, autoflush=False) + p1 = Parent() + p2 = Parent() + c1, c2 = Child(), Child() + c1.parent = p1 + sess.add_all([p1, p2]) + assert c1 in sess + + yield sess, p1, p2, c1, c2 + + sess.close() + + def test_enable_rel_loading_on_persistent_allows_backref_event( + self, parent_fixture + ): + sess, p1, p2, c1, c2 = parent_fixture + Parent, Child = self.classes("Parent", "Child") + + c3 = Child() + sess.enable_relationship_loading(c3) + c3.parent_id = p1.id + with assertions.expect_deprecated_20( + '"Child" object is being merged into a Session along the ' + 'backref cascade path for relationship "Parent.children"' + ): + c3.parent = p1 + + # backref fired off when c3.parent was set, + # because the "old" value was None + # change as of [ticket:3708] + assert c3 in p1.children + + def test_enable_rel_loading_allows_backref_event(self, parent_fixture): + sess, p1, p2, c1, c2 = parent_fixture + Parent, Child = self.classes("Parent", "Child") + + c3 = Child() + sess.enable_relationship_loading(c3) + c3.parent_id = p1.id + + with assertions.expect_deprecated_20( + '"Child" object is being merged into a Session along the ' + 'backref cascade path for relationship "Parent.children"' + ): + c3.parent = p1 + + # backref fired off when c3.parent was set, + # because the "old" value was None + # change as of [ticket:3708] + assert c3 in p1.children + + +class LazyTest(_fixtures.FixtureTest): + run_inserts = "once" + run_deletes = None + + def test_backrefs_dont_lazyload(self): + users, Address, addresses, User = ( + self.tables.users, + self.classes.Address, + self.tables.addresses, + self.classes.User, + ) + + self.mapper_registry.map_imperatively( + User, + users, + properties={"addresses": relationship(Address, backref="user")}, + ) + self.mapper_registry.map_imperatively(Address, addresses) + sess = fixture_session(autoflush=False) + ad = sess.query(Address).filter_by(id=1).one() + assert ad.user.id == 7 + + def go(): + ad.user = None + assert ad.user is None + + self.assert_sql_count(testing.db, go, 0) + + u1 = sess.query(User).filter_by(id=7).one() + + def go(): + assert ad not in u1.addresses + + self.assert_sql_count(testing.db, go, 1) + + sess.expire(u1, ["addresses"]) + + def go(): + assert ad in u1.addresses + + self.assert_sql_count(testing.db, go, 1) + + sess.expire(u1, ["addresses"]) + ad2 = Address() + + def go(): + with assertions.expect_deprecated_20( + ".* object is being merged into a Session along the " + "backref cascade path for relationship " + ): + ad2.user = u1 + assert ad2.user is u1 + + self.assert_sql_count(testing.db, go, 0) + + def go(): + assert ad2 in u1.addresses + + self.assert_sql_count(testing.db, go, 1) diff --git a/test/orm/test_expire.py b/test/orm/test_expire.py index d9204db96c..411e09d00e 100644 --- a/test/orm/test_expire.py +++ b/test/orm/test_expire.py @@ -1683,7 +1683,7 @@ class ExpiredPendingTest(_fixtures.FixtureTest): ) self.mapper_registry.map_imperatively(Address, addresses) - sess = fixture_session(autoflush=False) + sess = fixture_session(autoflush=False, future=True) a1 = Address(email_address="a1") sess.add(a1) sess.flush() @@ -1701,6 +1701,9 @@ class ExpiredPendingTest(_fixtures.FixtureTest): a2 = Address(email_address="a2") a2.user = u1 + # needed now that cascade backrefs is disabled + sess.add(a2) + # expire u1.addresses again. this expires # "pending" as well. sess.expire(u1, ["addresses"]) diff --git a/test/orm/test_lazy_relations.py b/test/orm/test_lazy_relations.py index 412637ec6a..295c9049cc 100644 --- a/test/orm/test_lazy_relations.py +++ b/test/orm/test_lazy_relations.py @@ -954,7 +954,7 @@ class LazyTest(_fixtures.FixtureTest): properties={"addresses": relationship(Address, backref="user")}, ) self.mapper_registry.map_imperatively(Address, addresses) - sess = fixture_session(autoflush=False) + sess = fixture_session(autoflush=False, future=True) ad = sess.query(Address).filter_by(id=1).one() assert ad.user.id == 7 diff --git a/test/orm/test_load_on_fks.py b/test/orm/test_load_on_fks.py index 02de9b2bb4..fda8be4236 100644 --- a/test/orm/test_load_on_fks.py +++ b/test/orm/test_load_on_fks.py @@ -5,7 +5,6 @@ from sqlalchemy import testing from sqlalchemy.orm import backref from sqlalchemy.orm import declarative_base from sqlalchemy.orm import relationship -from sqlalchemy.orm import Session from sqlalchemy.orm.attributes import instance_state from sqlalchemy.testing import AssertsExecutionResults from sqlalchemy.testing import fixtures @@ -65,12 +64,10 @@ class FlushOnPendingTest(AssertsExecutionResults, fixtures.TestBase): self.assert_sql_count(testing.db, go, 0) -class LoadOnFKsTest(AssertsExecutionResults, fixtures.TestBase): - __leave_connections_for_teardown__ = True - - def setup_test(self): - global Parent, Child, Base - Base = declarative_base() +class LoadOnFKsTest(fixtures.DeclarativeMappedTest): + @classmethod + def setup_classes(cls): + Base = cls.DeclarativeBasic class Parent(Base): __tablename__ = "parent" @@ -91,11 +88,11 @@ class LoadOnFKsTest(AssertsExecutionResults, fixtures.TestBase): parent = relationship(Parent, backref=backref("children")) - Base.metadata.create_all(testing.db) - - global sess, p1, p2, c1, c2 - sess = Session(bind=testing.db) + @testing.fixture + def parent_fixture(self, connection): + Parent, Child = self.classes("Parent", "Child") + sess = fixture_session(bind=connection) p1 = Parent() p2 = Parent() c1, c2 = Child(), Child() @@ -103,38 +100,22 @@ class LoadOnFKsTest(AssertsExecutionResults, fixtures.TestBase): sess.add_all([p1, p2]) assert c1 in sess - sess.commit() + sess.flush() - def teardown_test(self): - sess.rollback() - Base.metadata.drop_all(testing.db) + Child.parent.property.load_on_pending = False - def test_load_on_pending_allows_backref_event(self): - Child.parent.property.load_on_pending = True - sess.autoflush = False - c3 = Child() - sess.add(c3) - c3.parent_id = p1.id - c3.parent = p1 + sess.expire_all() - # backref fired off when c3.parent was set, - # because the "old" value was None. - # change as of [ticket:3708] - assert c3 in p1.children + yield sess, p1, p2, c1, c2 - def test_enable_rel_loading_allows_backref_event(self): - sess.autoflush = False - c3 = Child() - sess.enable_relationship_loading(c3) - c3.parent_id = p1.id - c3.parent = p1 + sess.close() - # backref fired off when c3.parent was set, - # because the "old" value was None - # change as of [ticket:3708] - assert c3 in p1.children + def test_m2o_history_on_persistent_allows_backref_event( + self, parent_fixture + ): + sess, p1, p2, c1, c2 = parent_fixture + Parent, Child = self.classes("Parent", "Child") - def test_m2o_history_on_persistent_allows_backref_event(self): c3 = Child() sess.add(c3) c3.parent_id = p1.id @@ -142,7 +123,10 @@ class LoadOnFKsTest(AssertsExecutionResults, fixtures.TestBase): assert c3 in p1.children - def test_load_on_persistent_allows_backref_event(self): + def test_load_on_persistent_allows_backref_event(self, parent_fixture): + sess, p1, p2, c1, c2 = parent_fixture + Parent, Child = self.classes("Parent", "Child") + Child.parent.property.load_on_pending = True c3 = Child() sess.add(c3) @@ -151,18 +135,28 @@ class LoadOnFKsTest(AssertsExecutionResults, fixtures.TestBase): assert c3 in p1.children - def test_enable_rel_loading_on_persistent_allows_backref_event(self): + def test_load_on_pending_allows_backref_event(self, parent_fixture): + sess, p1, p2, c1, c2 = parent_fixture + Parent, Child = self.classes("Parent", "Child") + + sess.autoflush = False + + Child.parent.property.load_on_pending = True c3 = Child() - sess.enable_relationship_loading(c3) + sess.add(c3) c3.parent_id = p1.id + c3.parent = p1 # backref fired off when c3.parent was set, - # because the "old" value was None + # because the "old" value was None. # change as of [ticket:3708] assert c3 in p1.children - def test_no_load_on_pending_allows_backref_event(self): + def test_no_load_on_pending_allows_backref_event(self, parent_fixture): + sess, p1, p2, c1, c2 = parent_fixture + Parent, Child = self.classes("Parent", "Child") + # users who stick with the program and don't use # 'load_on_pending' get expected behavior @@ -175,7 +169,10 @@ class LoadOnFKsTest(AssertsExecutionResults, fixtures.TestBase): assert c3 in p1.children - def test_autoflush_on_pending(self): + def test_autoflush_on_pending(self, parent_fixture): + sess, p1, p2, c1, c2 = parent_fixture + Parent, Child = self.classes("Parent", "Child") + # ensure p1.id is not expired p1.id @@ -186,7 +183,10 @@ class LoadOnFKsTest(AssertsExecutionResults, fixtures.TestBase): # pendings don't autoflush assert c3.parent is None - def test_autoflush_load_on_pending_on_pending(self): + def test_autoflush_load_on_pending_on_pending(self, parent_fixture): + sess, p1, p2, c1, c2 = parent_fixture + Parent, Child = self.classes("Parent", "Child") + # ensure p1.id is not expired p1.id @@ -198,7 +198,10 @@ class LoadOnFKsTest(AssertsExecutionResults, fixtures.TestBase): # ...unless the flag is on assert c3.parent is p1 - def test_collection_load_from_pending_populated(self): + def test_collection_load_from_pending_populated(self, parent_fixture): + sess, p1, p2, c1, c2 = parent_fixture + Parent, Child = self.classes("Parent", "Child") + Parent.children.property.load_on_pending = True p2 = Parent(id=p1.id) sess.add(p2) @@ -209,7 +212,10 @@ class LoadOnFKsTest(AssertsExecutionResults, fixtures.TestBase): self.assert_sql_count(testing.db, go, 1) - def test_collection_load_from_pending_no_sql(self): + def test_collection_load_from_pending_no_sql(self, parent_fixture): + sess, p1, p2, c1, c2 = parent_fixture + Parent, Child = self.classes("Parent", "Child") + Parent.children.property.load_on_pending = True p2 = Parent(id=None) sess.add(p2) @@ -221,7 +227,10 @@ class LoadOnFKsTest(AssertsExecutionResults, fixtures.TestBase): self.assert_sql_count(testing.db, go, 0) - def test_load_on_pending_with_set(self): + def test_load_on_pending_with_set(self, parent_fixture): + sess, p1, p2, c1, c2 = parent_fixture + Parent, Child = self.classes("Parent", "Child") + Child.parent.property.load_on_pending = True p1.children @@ -236,7 +245,10 @@ class LoadOnFKsTest(AssertsExecutionResults, fixtures.TestBase): self.assert_sql_count(testing.db, go, 0) - def test_backref_doesnt_double(self): + def test_backref_doesnt_double(self, parent_fixture): + sess, p1, p2, c1, c2 = parent_fixture + Parent, Child = self.classes("Parent", "Child") + Child.parent.property.load_on_pending = True sess.autoflush = False p1.children @@ -248,116 +260,133 @@ class LoadOnFKsTest(AssertsExecutionResults, fixtures.TestBase): c3.parent = p1 assert len(p1.children) == 2 - def test_m2o_lazy_loader_on_persistent(self): + @testing.combinations(True, False, argnames="loadfk") + @testing.combinations(True, False, argnames="loadrel") + @testing.combinations(True, False, argnames="autoflush") + @testing.combinations(True, False, argnames="manualflush") + @testing.combinations(True, False, argnames="fake_autoexpire") + def test_m2o_lazy_loader_on_persistent( + self, + parent_fixture, + loadfk, + loadrel, + autoflush, + manualflush, + fake_autoexpire, + ): """Compare the behaviors from the lazyloader using the "committed" state in all cases, vs. the lazyloader using the "current" state in all cases except during flush. """ - for loadfk in (True, False): - for loadrel in (True, False): - for autoflush in (True, False): - for manualflush in (True, False): - for fake_autoexpire in (True, False): - sess.autoflush = autoflush - - if loadfk: - c1.parent_id - if loadrel: - c1.parent - - c1.parent_id = p2.id - - if manualflush: - sess.flush() - - # fake_autoexpire refers to the eventual - # auto-expire of 'parent' when c1.parent_id - # is altered. - if fake_autoexpire: - sess.expire(c1, ["parent"]) - - # old 0.6 behavior - # if manualflush and (not loadrel or - # fake_autoexpire): - # # a flush occurs, we get p2 - # assert c1.parent is p2 - # elif not loadrel and not loadfk: - # # problematically - we get None since - # # committed state - # # is empty when c1.parent_id was mutated, - # # since we want - # # to save on selects. this is - # # why the patch goes in in 0.6 - this is - # # mostly a bug. - # assert c1.parent is None - # else: - # # if things were loaded, autoflush doesn't - # # even happen. - # assert c1.parent is p1 - - # new behavior - if loadrel and not fake_autoexpire: - assert c1.parent is p1 - else: - assert c1.parent is p2 - - sess.rollback() - - def test_m2o_lazy_loader_on_pending(self): - for loadonpending in (False, True): - for autoflush in (False, True): - for manualflush in (False, True): - Child.parent.property.load_on_pending = loadonpending - sess.autoflush = autoflush - - # ensure p2.id not expired - p2.id - - c2 = Child() - sess.add(c2) - c2.parent_id = p2.id - - if manualflush: - sess.flush() - - if loadonpending or manualflush: - assert c2.parent is p2 - else: - assert c2.parent is None - - sess.rollback() - - def test_m2o_lazy_loader_on_transient(self): - for loadonpending in (False, True): - for attach in (False, True): - for autoflush in (False, True): - for manualflush in (False, True): - for enable_relationship_rel in (False, True): - Child.parent.property.load_on_pending = ( - loadonpending - ) - sess.autoflush = autoflush - c2 = Child() - - if attach: - state = instance_state(c2) - state.session_id = sess.hash_key - - if enable_relationship_rel: - sess.enable_relationship_loading(c2) - - c2.parent_id = p2.id - - if manualflush: - sess.flush() - - if ( - loadonpending and attach - ) or enable_relationship_rel: - assert c2.parent is p2 - else: - assert c2.parent is None - - sess.rollback() + sess, p1, p2, c1, c2 = parent_fixture + Parent, Child = self.classes("Parent", "Child") + + sess.autoflush = autoflush + + if loadfk: + c1.parent_id + if loadrel: + c1.parent + + c1.parent_id = p2.id + + if manualflush: + sess.flush() + + # fake_autoexpire refers to the eventual + # auto-expire of 'parent' when c1.parent_id + # is altered. + if fake_autoexpire: + sess.expire(c1, ["parent"]) + + # old 0.6 behavior + # if manualflush and (not loadrel or + # fake_autoexpire): + # # a flush occurs, we get p2 + # assert c1.parent is p2 + # elif not loadrel and not loadfk: + # # problematically - we get None since + # # committed state + # # is empty when c1.parent_id was mutated, + # # since we want + # # to save on selects. this is + # # why the patch goes in in 0.6 - this is + # # mostly a bug. + # assert c1.parent is None + # else: + # # if things were loaded, autoflush doesn't + # # even happen. + # assert c1.parent is p1 + + # new behavior + if loadrel and not fake_autoexpire: + assert c1.parent is p1 + else: + assert c1.parent is p2 + + @testing.combinations(True, False, argnames="loadonpending") + @testing.combinations(True, False, argnames="autoflush") + @testing.combinations(True, False, argnames="manualflush") + def test_m2o_lazy_loader_on_pending( + self, parent_fixture, loadonpending, autoflush, manualflush + ): + sess, p1, p2, c1, c2 = parent_fixture + Parent, Child = self.classes("Parent", "Child") + + Child.parent.property.load_on_pending = loadonpending + sess.autoflush = autoflush + + # ensure p2.id not expired + p2.id + + c2 = Child() + sess.add(c2) + c2.parent_id = p2.id + + if manualflush: + sess.flush() + + if loadonpending or manualflush: + assert c2.parent is p2 + else: + assert c2.parent is None + + @testing.combinations(True, False, argnames="loadonpending") + @testing.combinations(True, False, argnames="attach") + @testing.combinations(True, False, argnames="autoflush") + @testing.combinations(True, False, argnames="manualflush") + @testing.combinations(True, False, argnames="enable_relationship_rel") + def test_m2o_lazy_loader_on_transient( + self, + parent_fixture, + loadonpending, + attach, + autoflush, + manualflush, + enable_relationship_rel, + ): + sess, p1, p2, c1, c2 = parent_fixture + Parent, Child = self.classes("Parent", "Child") + + Child.parent.property.load_on_pending = loadonpending + sess.autoflush = autoflush + c2 = Child() + + if attach: + state = instance_state(c2) + state.session_id = sess.hash_key + + if enable_relationship_rel: + sess.enable_relationship_loading(c2) + + c2.parent_id = p2.id + + if manualflush: + sess.flush() + + if (loadonpending and attach) or enable_relationship_rel: + assert c2.parent is p2 + else: + assert c2.parent is None diff --git a/test/orm/test_mapper.py b/test/orm/test_mapper.py index 7d9e6f9c38..cbc164ff3d 100644 --- a/test/orm/test_mapper.py +++ b/test/orm/test_mapper.py @@ -2349,7 +2349,7 @@ class RequirementsTest(fixtures.MappedTest): self.mapper(H3, ht3) self.mapper(H6, ht6) - s = fixture_session() + s = fixture_session(future=True) s.add_all([H1("abc"), H1("def")]) h1 = H1("ghi") s.add(h1) @@ -2367,7 +2367,7 @@ class RequirementsTest(fixtures.MappedTest): h6 = H6() h6.h1a = h1 h6.h1b = x = H1() - assert x in s + s.add(x) h6.h1b.h2s.append(H2("def")) diff --git a/test/orm/test_merge.py b/test/orm/test_merge.py index 3b97bd5a57..034339b29d 100644 --- a/test/orm/test_merge.py +++ b/test/orm/test_merge.py @@ -1432,7 +1432,7 @@ class MergeTest(_fixtures.FixtureTest): self.tables.users, ) - s = fixture_session(autoflush=True, autocommit=False) + s = fixture_session(autoflush=True, autocommit=False, future=True) self.mapper_registry.map_imperatively( User, users, @@ -1445,8 +1445,10 @@ class MergeTest(_fixtures.FixtureTest): ) a1 = Address(user=s.merge(User(id=1, name="ed")), email_address="x") + s.add(a1) before_id = id(a1.user) a2 = Address(user=s.merge(User(id=1, name="jack")), email_address="x") + s.add(a2) after_id = id(a1.user) other_id = id(a2.user) eq_(before_id, other_id) diff --git a/test/orm/test_naturalpks.py b/test/orm/test_naturalpks.py index fba335d285..8f9e366208 100644 --- a/test/orm/test_naturalpks.py +++ b/test/orm/test_naturalpks.py @@ -926,7 +926,7 @@ class SelfReferentialTest(fixtures.MappedTest): }, ) - sess = fixture_session() + sess = fixture_session(future=True) n1 = Node(name="n1") sess.add(n1) n2 = Node(name="n11", parentnode=n1) diff --git a/test/orm/test_session.py b/test/orm/test_session.py index 94b35c5b3f..bd1f9545a4 100644 --- a/test/orm/test_session.py +++ b/test/orm/test_session.py @@ -922,7 +922,7 @@ class SessionStateTest(_fixtures.FixtureTest): ) self.mapper_registry.map_imperatively(Address, addresses) - session = fixture_session() + session = fixture_session(future=True) @event.listens_for(session, "after_flush") def load_collections(session, flush_context): @@ -943,6 +943,9 @@ class SessionStateTest(_fixtures.FixtureTest): assert "addresses" not in inspect(u1).dict assert a2 in inspect(u1)._pending_mutations["addresses"].added_items + # this is needed now that cascade_backrefs is turned off + session.add(a2) + with assertions.expect_warnings( r"Identity map already had an identity " r"for \(.*Address.*\), replacing" diff --git a/test/orm/test_versioning.py b/test/orm/test_versioning.py index 57fe0b7a2c..c995f97715 100644 --- a/test/orm/test_versioning.py +++ b/test/orm/test_versioning.py @@ -828,7 +828,7 @@ class NoBumpOnRelationshipTest(fixtures.MappedTest): def _run_test(self, auto_version_counter=True): A, B = self.classes("A", "B") - s = fixture_session() + s = fixture_session(future=True) if auto_version_counter: a1 = A() else: -- 2.47.2