from sqlalchemy import ForeignKey, Index, Integer, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column from scribe.models import Base from scribe.models.base import CreatedAtMixin, iso class NoteSupersession(Base, CreatedAtMixin): """A newer record's claim that it has overtaken an older one. WHY THE RELATION POINTS FORWARD The note being WRITTEN declares what it supersedes. The old record cannot know it has been overtaken — asking it to record its own obsolescence is asking it to predict the future. So the claim is made by the party that has the knowledge, and the demotion is derived from the far end. WHY A TABLE RATHER THAN A COLUMN It is genuinely many-to-many and partial: one note may supersede parts of several others, and a note may be overtaken piecemeal by several later ones. Both directions are queried and neither is rare — `superseded_id` answers the ranking question ("has this been overtaken?"), `superseder_id` answers the record view ("what does this replace?"). An array column on `notes` could be indexed for one and not the other. WHAT IT MEANS, AND WHAT IT DOES NOT A claim, never a proof. Supersession DEMOTES a record in ranked retrieval; it does not assert the older record was wrong, and it never hides it. A note that accurately described how something worked in June is still accurate about June — it is just no longer the answer to "how does this work". CASCADE IS SAFE HERE BECAUSE TRASHING IS NOT A DELETE `trash_svc` stamps `deleted_at` (an UPDATE), so a trashed note keeps its claims and `restore` brings them back intact. The cascade fires only on `purge_trash`, where the row genuinely goes — and a supersession claim about a row that no longer exists is not a fact anyone can act on. """ __tablename__ = "note_supersessions" id: Mapped[int] = mapped_column(primary_key=True) # The newer record, making the claim. superseder_id: Mapped[int] = mapped_column( Integer, ForeignKey("notes.id", ondelete="CASCADE") ) # The older record, demoted by it. superseded_id: Mapped[int] = mapped_column( Integer, ForeignKey("notes.id", ondelete="CASCADE") ) __table_args__ = ( UniqueConstraint( "superseder_id", "superseded_id", name="uq_note_supersessions_pair" ), # Both directions indexed — see the class docstring for why neither is # the rare one. Index("ix_note_supersessions_superseder", "superseder_id"), Index("ix_note_supersessions_superseded", "superseded_id"), ) def to_dict(self) -> dict: return { "id": self.id, "superseder_id": self.superseder_id, "superseded_id": self.superseded_id, "created_at": iso(self.created_at), }