--- /dev/null
+.. change::
+ :tags: bug, orm
+ :tickets: 12748
+
+ Fixed issue where using the ``post_update`` feature would apply incorrect
+ "pre-fetched" values to the ORM objects after a multi-row UPDATE process
+ completed. These "pre-fetched" values would come from any column that had
+ an :paramref:`.Column.onupdate` callable or a version id generator used by
+ :paramref:`.orm.Mapper.version_id_generator`; for a version id generator
+ that delivered random identifiers like timestamps or UUIDs, this incorrect
+ data would lead to a DELETE statement against those same rows to fail in
+ the next step.
+
)
rows += c.rowcount
- for state, state_dict, mapper_rec, connection, params in records:
+ for i, (
+ state,
+ state_dict,
+ mapper_rec,
+ connection,
+ params,
+ ) in enumerate(records):
_postfetch_post_update(
mapper_rec,
uowtransaction,
state,
state_dict,
c,
- c.context.compiled_parameters[0],
+ c.context.compiled_parameters[i],
)
if check_rowcount:
from sqlalchemy import String
from sqlalchemy import testing
from sqlalchemy.orm import backref
+from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship
+from sqlalchemy.orm import Session
from sqlalchemy.testing import eq_
from sqlalchemy.testing import fixtures
from sqlalchemy.testing import is_
session.flush()
+class PostUpdatePrefetchTest(fixtures.DeclarativeMappedTest):
+ """test #12748"""
+
+ run_setup_classes = "each"
+
+ @classmethod
+ def setup_classes(cls):
+ Base = cls.DeclarativeBasic
+
+ count = 0
+
+ def _counter():
+ nonlocal count
+ count += 1
+ return count
+
+ class Parent(Base):
+ __tablename__ = "parent"
+ id = mapped_column(Integer, primary_key=True)
+
+ related = relationship("Related", post_update=True)
+
+ class Related(Base):
+ __tablename__ = "related"
+
+ id = mapped_column(Integer, primary_key=True)
+ parent_id = mapped_column(ForeignKey("parent.id"))
+ counter = mapped_column(Integer, onupdate=_counter)
+
+ def test_update_counter(self, connection):
+ Parent, Related = self.classes("Parent", "Related")
+
+ p1 = Parent(related=[Related(), Related(), Related()])
+ with Session(connection, expire_on_commit=False) as sess:
+ sess.add(p1)
+ sess.commit()
+
+ eq_([rel.counter for rel in p1.related], [1, 2, 3])
+
+
class PostUpdateBatchingTest(fixtures.MappedTest):
"""test that lots of post update cols batch together into a single
UPDATE."""
from sqlalchemy import testing
from sqlalchemy import TypeDecorator
from sqlalchemy import util
+from sqlalchemy import Uuid
from sqlalchemy.orm import configure_mappers
from sqlalchemy.orm import exc as orm_exc
+from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship
from sqlalchemy.orm import Session
from sqlalchemy.testing import assert_raises
)
+class PostUpdatePrefetchTest(fixtures.DeclarativeMappedTest):
+ """test #12748"""
+
+ run_setup_classes = "each"
+
+ @classmethod
+ def setup_classes(cls):
+ Base = cls.DeclarativeBasic
+
+ class Parent(Base):
+ __tablename__ = "parent"
+ id = mapped_column(Integer, primary_key=True)
+
+ related = relationship(
+ "Related", post_update=True, cascade="all, delete-orphan"
+ )
+
+ class Related(Base):
+ __tablename__ = "related"
+
+ id = mapped_column(Integer, primary_key=True)
+ parent_id = mapped_column(ForeignKey("parent.id"))
+ version = mapped_column(Uuid)
+
+ __mapper_args__ = {
+ "version_id_col": version,
+ "version_id_generator": lambda v: uuid.uuid4(),
+ }
+
+ def test_random_versionids(self, connection):
+ Parent, Related = self.classes("Parent", "Related")
+
+ p1 = Parent(related=[Related(), Related(), Related()])
+ with Session(connection, expire_on_commit=False) as sess:
+ sess.add(p1)
+ sess.commit()
+
+ with Session(connection, expire_on_commit=False) as sess:
+ sess.delete(p1)
+ sess.commit()
+
+
class NoBumpOnRelationshipTest(fixtures.MappedTest):
__backend__ = True