Skip to content

Reappearance of Deleted Rows while Versioning with a History Table #10267

Closed
@zzzeek

Description

@zzzeek
Member

Discussed in #10259

Originally posted by PHvL August 18, 2023
We are using the Versioned class from history_meta for tables that are synced from some external source. Sometimes rows disappear in the external source and later re-appear with the same primary key,

The result is that both the history table and the original table contain a row with version=1, resulting in a violation of the primary key constraint of the history table at the next update (or deletion) of this row.

I've hence changed the default value of the version column in the history table to get the maximum of the version already present in history columns +1

I don't know if this is the most elegant solution, but maybe it helps someone with the same issue.

@@ -11,6 +11,9 @@ from sqlalchemy import inspect
 from sqlalchemy import Integer
 from sqlalchemy import PrimaryKeyConstraint
 from sqlalchemy import util
+from sqlalchemy import select
+from sqlalchemy.sql.expression import func, and_
+from sqlalchemy.engine.default import DefaultExecutionContext
 from sqlalchemy.orm import attributes
 from sqlalchemy.orm import object_mapper
 from sqlalchemy.orm.exc import UnmappedColumnError
@@ -146,8 +149,15 @@ def _history_mapper(local_mapper):
 				super_history_table.append_column(col)
 
 	if not super_mapper:
+		def default_version_from_history(context: DefaultExecutionContext):
+			current_parameters = context.get_current_parameters()
+			return context.connection.scalar(select(func.coalesce(func.max(history_table.c.version), 0)+1).where(and_(*[getattr(history_table.c, c.name)==current_parameters.get(c.name, None) for c in inspect(local_mapper.local_table).primary_key])))
+		# Set default value of version column to the maximum of the version in history columns already present +1
+		# Otherwise re-appearance of deleted rows would cause an error with the next update
 		local_mapper.local_table.append_column(
-			Column("version", Integer, default=1, nullable=False),
+			Column("version", Integer,
+	  			default=default_version_from_history,
+				nullable=False),
 			replace_existing=True,
 		)
 		local_mapper.add_property(
```</div>

Activity

added
bugSomething isn't working
near-term releaseaddition to the milestone which indicates this should be in a near-term release
examplesissues with the example code, like dogpile cache, history meta, etc
on Aug 21, 2023
added this to the 2.0.x milestone on Aug 21, 2023
zzzeek

zzzeek commented on Aug 21, 2023

@zzzeek
MemberAuthor

test case submitted by reporter

    def test_external_id(self):
        class ObjectExternal(Versioned, self.Base, ComparableEntity):
            __tablename__ = "externalobjects"

            id1 = Column(String(3), primary_key=True)
            id2 = Column(String(3), primary_key=True)
            name = Column(String(50))

        self.create_tables()
        sess = self.session
        sc = ObjectExternal(id1='aaa', id2='bbb', name="sc1")
        sess.add(sc)
        sess.commit()

        sc.name = "sc1modified"
        sess.commit()

        assert sc.version == 2

        ObjectExternalHistory = ObjectExternal.__history_mapper__.class_

        eq_(
            sess.query(ObjectExternalHistory).all(),
            [
                ObjectExternalHistory(version=1, id1='aaa', id2='bbb', name="sc1"),
            ],
        )

        sess.delete(sc)
        sess.commit()

        assert sess.query(ObjectExternal).count()==0

        eq_(
            sess.query(ObjectExternalHistory).all(),
            [
                ObjectExternalHistory(version=1, id1='aaa', id2='bbb', name="sc1"),
                ObjectExternalHistory(version=2, id1='aaa', id2='bbb', name="sc1modified"),
            ],
        )

        sc = ObjectExternal(id1='aaa', id2='bbb', name="sc1reappeared")
        sess.add(sc)
        sess.commit()

        assert sc.version == 3

        sc.name = "sc1reappearedmodified"
        sess.commit()

        assert sc.version == 4

        eq_(
            sess.query(ObjectExternalHistory).all(),
            [
                ObjectExternalHistory(version=1, id1='aaa', id2='bbb', name="sc1"),
                ObjectExternalHistory(version=2, id1='aaa', id2='bbb', name="sc1modified"),
                ObjectExternalHistory(version=3, id1='aaa', id2='bbb', name="sc1reappeared"),
            ],
        )
added
patch providedissue has the correct patch present in the comments. needs test + merge
on Aug 21, 2023
sqla-tester

sqla-tester commented on Aug 1, 2024

@sqla-tester
Collaborator

Mike Bayer has proposed a fix for this issue in the main branch:

add check for pre-existing history records https://gerrit.sqlalchemy.org/c/sqlalchemy/sqlalchemy/+/5435

sqla-tester

sqla-tester commented on Aug 1, 2024

@sqla-tester
Collaborator

Mike Bayer has proposed a fix for this issue in the rel_2_0 branch:

add check for pre-existing history records https://gerrit.sqlalchemy.org/c/sqlalchemy/sqlalchemy/+/5436

added a commit that references this issue on Aug 2, 2024
ce13550
added a commit that references this issue on Aug 3, 2024
6a59eec
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingexamplesissues with the example code, like dogpile cache, history meta, etcnear-term releaseaddition to the milestone which indicates this should be in a near-term releaseormpatch providedissue has the correct patch present in the comments. needs test + merge

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @zzzeek@sqla-tester@CaselIT

        Issue actions

          Reappearance of Deleted Rows while Versioning with a History Table · Issue #10267 · sqlalchemy/sqlalchemy