]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
pop prefetch values from committed_state when they are available
authorMike Bayer <mike_mp@zzzcomputing.com>
Thu, 28 Dec 2023 21:02:48 +0000 (16:02 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Thu, 28 Dec 2023 21:02:48 +0000 (16:02 -0500)
Fixed issue where when making use of the
:paramref:`_orm.relationship.post_update` feature at the same time as using
a mapper version_id_col could lead to a situation where the second UPDATE
statement emitted by the post-update feature would fail to make use of the
correct version identifier, assuming an UPDATE was already emitted in that
flush which had already bumped the version counter.

Fixes: #10800
Change-Id: I3fccdb26ebbd2d987bb4f0e894449b7413556054

doc/build/changelog/unreleased_20/10800.rst [new file with mode: 0644]
lib/sqlalchemy/orm/persistence.py
test/orm/test_versioning.py

diff --git a/doc/build/changelog/unreleased_20/10800.rst b/doc/build/changelog/unreleased_20/10800.rst
new file mode 100644 (file)
index 0000000..346ae1f
--- /dev/null
@@ -0,0 +1,10 @@
+.. change::
+    :tags: bug, orm
+    :tickets: 10800
+
+    Fixed issue where when making use of the
+    :paramref:`_orm.relationship.post_update` feature at the same time as using
+    a mapper version_id_col could lead to a situation where the second UPDATE
+    statement emitted by the post-update feature would fail to make use of the
+    correct version identifier, assuming an UPDATE was already emitted in that
+    flush which had already bumped the version counter.
index 3f537fb761600730891cd0754821c3e95f7d905e..1728b4ac88c5a7d5bfb1c8dfd3e4ed803523bc8a 100644 (file)
@@ -1659,9 +1659,18 @@ def _postfetch(
 
     for c in prefetch_cols:
         if c.key in params and c in mapper._columntoproperty:
-            dict_[mapper._columntoproperty[c].key] = params[c.key]
+            pkey = mapper._columntoproperty[c].key
+
+            # set prefetched value in dict and also pop from committed_state,
+            # since this is new database state that replaces whatever might
+            # have previously been fetched (see #10800).  this is essentially a
+            # shorthand version of set_committed_value(), which could also be
+            # used here directly (with more overhead)
+            dict_[pkey] = params[c.key]
+            state.committed_state.pop(pkey, None)
+
             if refresh_flush:
-                load_evt_attrs.append(mapper._columntoproperty[c].key)
+                load_evt_attrs.append(pkey)
 
     if refresh_flush and load_evt_attrs:
         mapper.class_manager.dispatch.refresh_flush(
index 7f52af71561248f8d4ff75eebb1d92f5bec5a2bc..a0325059a81f06900901017be581d8219514d6fa 100644 (file)
@@ -2029,3 +2029,89 @@ class QuotedBindVersioningTest(fixtures.MappedTest):
             fixture_session.commit()
 
         eq_(f1.version, 2)
+
+
+class PostUpdateVersioningTest(fixtures.DeclarativeMappedTest):
+    """test for #10800"""
+
+    @classmethod
+    def setup_classes(cls):
+        Base = cls.DeclarativeBasic
+
+        class User(Base):
+            __tablename__ = "user"
+
+            id = Column(Integer, primary_key=True)
+
+        class Parent(Base):
+            __tablename__ = "parent"
+
+            id = Column(Integer, primary_key=True)
+            version_id = Column(Integer)
+            updated_by_id = Column(
+                Integer,
+                ForeignKey("user.id"),
+            )
+
+            updated_by = relationship(
+                "User",
+                foreign_keys=[updated_by_id],
+                post_update=True,
+            )
+
+            __mapper_args__ = {
+                "version_id_col": version_id,
+            }
+
+    def test_bumped_version_id(self):
+        User, Parent = self.classes("User", "Parent")
+
+        session = fixture_session()
+        u1 = User(id=1)
+        u2 = User(id=2)
+        p1 = Parent(id=1, updated_by=u1)
+        session.add(u1)
+        session.add(u2)
+        session.add(p1)
+
+        u2id = u2.id
+        session.commit()
+        session.close()
+
+        p1 = session.get(Parent, 1)
+        p1.updated_by
+        p1.version_id = p1.version_id
+        p1.updated_by_id = u2id
+        assert "version_id" in inspect(p1).committed_state
+
+        with self.sql_execution_asserter(testing.db) as asserter:
+            session.commit()
+
+        asserter.assert_(
+            CompiledSQL(
+                "UPDATE parent SET version_id=:version_id, "
+                "updated_by_id=:updated_by_id WHERE parent.id = :parent_id "
+                "AND parent.version_id = :parent_version_id",
+                [
+                    {
+                        "version_id": 2,
+                        "updated_by_id": 2,
+                        "parent_id": 1,
+                        "parent_version_id": 1,
+                    }
+                ],
+            ),
+            CompiledSQL(
+                "UPDATE parent SET version_id=:version_id, "
+                "updated_by_id=:updated_by_id WHERE parent.id = :parent_id "
+                "AND parent.version_id = :parent_version_id",
+                [
+                    {
+                        "version_id": 3,
+                        "updated_by_id": 2,
+                        "parent_id": 1,
+                        "parent_version_id": 2,
+                    }
+                ],
+            ),
+        )