feat(identifiers): add reversible projection migration
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Multi-Context Image / build-and-push (push) Successful in 24s

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a023c0-a0a3-7c03-b395-5a0d2757214d
This commit is contained in:
tegwick 2026-08-22 00:27:48 +02:00
parent ce52e9d1e2
commit cb1b028fd1
18 changed files with 921 additions and 27 deletions

View file

@ -41,6 +41,7 @@ from api.models.workplan_launch_request import WorkplanLaunchRequest
from api.models.fabric_graph import FabricGraphImport, FabricGraphNode, FabricGraphEdge
from api.models.legacy_meter import LegacyInterface, LegacyInterfaceUsageBucket
from api.models.write_idempotency_key import WriteIdempotencyKey
from api.models.work_record_identifier_alias import WorkRecordIdentifierAlias
from api.models.suggestion import (
Suggestion,
SuggestionNote,
@ -81,5 +82,6 @@ __all__ = [
"FabricGraphImport", "FabricGraphNode", "FabricGraphEdge",
"LegacyInterface", "LegacyInterfaceUsageBucket",
"WriteIdempotencyKey",
"WorkRecordIdentifierAlias",
"Suggestion", "SuggestionNote", "SuggestionRelevanceBump", "SuggestionStage",
]
]

View file

@ -33,7 +33,7 @@ class CapabilityRequest(Base, TimestampMixin):
)
requesting_workplan_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True),
ForeignKey("workplans.id", ondelete="SET NULL"),
ForeignKey("workplans.id", ondelete="SET NULL", onupdate="CASCADE"),
nullable=True,
)
requesting_agent: Mapped[str] = mapped_column(String(100), nullable=False)
@ -47,7 +47,7 @@ class CapabilityRequest(Base, TimestampMixin):
)
fulfilling_workplan_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True),
ForeignKey("workplans.id", ondelete="SET NULL"),
ForeignKey("workplans.id", ondelete="SET NULL", onupdate="CASCADE"),
nullable=True,
)
fulfilling_agent: Mapped[str | None] = mapped_column(String(100), nullable=True)
@ -55,7 +55,7 @@ class CapabilityRequest(Base, TimestampMixin):
# Links
blocking_task_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True),
ForeignKey("tasks.id", ondelete="SET NULL"),
ForeignKey("tasks.id", ondelete="SET NULL", onupdate="CASCADE"),
nullable=True,
)
catalog_entry_id: Mapped[uuid.UUID | None] = mapped_column(

View file

@ -2,7 +2,7 @@ import enum
import uuid
from datetime import datetime
from sqlalchemy import Boolean, DateTime, Enum, ForeignKey, String, Text
from sqlalchemy import DateTime, Enum, ForeignKey, String, Text
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
@ -48,7 +48,9 @@ class Contribution(Base, TimestampMixin):
UUID(as_uuid=True), ForeignKey("topics.id", ondelete="SET NULL"), nullable=True
)
related_workplan_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("workplans.id", ondelete="SET NULL"), nullable=True
UUID(as_uuid=True),
ForeignKey("workplans.id", ondelete="SET NULL", onupdate="CASCADE"),
nullable=True,
)
repo_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("managed_repos.id", ondelete="SET NULL"), nullable=True

View file

@ -37,7 +37,10 @@ class Decision(Base, TimestampMixin):
UUID(as_uuid=True), ForeignKey("topics.id", ondelete="RESTRICT"), nullable=True, index=True
)
workplan_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("workplans.id", ondelete="RESTRICT"), nullable=True, index=True
UUID(as_uuid=True),
ForeignKey("workplans.id", ondelete="RESTRICT", onupdate="CASCADE"),
nullable=True,
index=True,
)
title: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[str | None] = mapped_column(Text, nullable=True)

View file

@ -45,7 +45,9 @@ class ExtensionPoint(Base, TimestampMixin):
UUID(as_uuid=True), ForeignKey("topics.id", ondelete="SET NULL"), nullable=True
)
workplan_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("workplans.id", ondelete="SET NULL"), nullable=True
UUID(as_uuid=True),
ForeignKey("workplans.id", ondelete="SET NULL", onupdate="CASCADE"),
nullable=True,
)
domain: Mapped["Domain"] = relationship("Domain", lazy="selectin") # noqa: F821

View file

@ -67,7 +67,10 @@ class Intake(Base, TimestampMixin):
UUID(as_uuid=True), ForeignKey("topics.id", ondelete="SET NULL"), nullable=True, index=True
)
workplan_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("workplans.id", ondelete="SET NULL"), nullable=True, index=True
UUID(as_uuid=True),
ForeignKey("workplans.id", ondelete="SET NULL", onupdate="CASCADE"),
nullable=True,
index=True,
)
repo_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("managed_repos.id", ondelete="SET NULL"), nullable=True, index=True

View file

@ -20,10 +20,16 @@ class ProgressEvent(Base):
UUID(as_uuid=True), ForeignKey("topics.id", ondelete="RESTRICT"), nullable=True, index=True
)
workplan_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("workplans.id", ondelete="RESTRICT"), nullable=True, index=True
UUID(as_uuid=True),
ForeignKey("workplans.id", ondelete="RESTRICT", onupdate="CASCADE"),
nullable=True,
index=True,
)
task_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("tasks.id", ondelete="RESTRICT"), nullable=True, index=True
UUID(as_uuid=True),
ForeignKey("tasks.id", ondelete="RESTRICT", onupdate="CASCADE"),
nullable=True,
index=True,
)
decision_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("decisions.id", ondelete="RESTRICT"), nullable=True, index=True

View file

@ -36,7 +36,9 @@ class Suggestion(Base, TimestampMixin):
UUID(as_uuid=True), ForeignKey("topics.id", ondelete="SET NULL"), nullable=True
)
workplan_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("workplans.id", ondelete="SET NULL"), nullable=True
UUID(as_uuid=True),
ForeignKey("workplans.id", ondelete="SET NULL", onupdate="CASCADE"),
nullable=True,
)
title: Mapped[str] = mapped_column(String(500), nullable=False)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
@ -61,7 +63,9 @@ class Suggestion(Base, TimestampMixin):
Float, nullable=False, default=1.0, server_default="1"
)
promoted_task_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("tasks.id", ondelete="SET NULL"), nullable=True
UUID(as_uuid=True),
ForeignKey("tasks.id", ondelete="SET NULL", onupdate="CASCADE"),
nullable=True,
)
domain: Mapped["Domain"] = relationship("Domain", lazy="selectin") # noqa: F821
@ -117,4 +121,4 @@ class SuggestionRelevanceBump(Base):
reason: Mapped[str | None] = mapped_column(Text, nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)
)

View file

@ -31,7 +31,10 @@ class Task(Base, TimestampMixin):
UUID(as_uuid=True), primary_key=True, default=new_uuid
)
workplan_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("workplans.id", ondelete="RESTRICT"), nullable=False, index=True
UUID(as_uuid=True),
ForeignKey("workplans.id", ondelete="RESTRICT", onupdate="CASCADE"),
nullable=False,
index=True,
)
title: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
@ -47,7 +50,9 @@ class Task(Base, TimestampMixin):
needs_human: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False, index=True)
intervention_note: Mapped[str | None] = mapped_column(Text, nullable=True)
parent_task_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("tasks.id", ondelete="SET NULL"), nullable=True
UUID(as_uuid=True),
ForeignKey("tasks.id", ondelete="SET NULL", onupdate="CASCADE"),
nullable=True,
)
workplan: Mapped["Workplan"] = relationship("Workplan", back_populates="tasks") # noqa: F821

View file

@ -77,7 +77,9 @@ class TechnicalDebt(Base, TimestampMixin):
UUID(as_uuid=True), ForeignKey("topics.id", ondelete="SET NULL"), nullable=True
)
workplan_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("workplans.id", ondelete="SET NULL"), nullable=True
UUID(as_uuid=True),
ForeignKey("workplans.id", ondelete="SET NULL", onupdate="CASCADE"),
nullable=True,
)
domain: Mapped["Domain"] = relationship("Domain", lazy="selectin") # noqa: F821

View file

@ -1,9 +1,16 @@
import uuid
from datetime import datetime
from typing import Any
from sqlalchemy import DateTime, Float, ForeignKey, Integer, Text, UniqueConstraint, func
from sqlalchemy import (
DateTime,
Float,
ForeignKey,
Integer,
Text,
UniqueConstraint,
func,
)
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
@ -25,10 +32,16 @@ class TokenEvent(Base):
UUID(as_uuid=True), primary_key=True, default=new_uuid
)
task_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("tasks.id", ondelete="SET NULL"), nullable=True, index=True
UUID(as_uuid=True),
ForeignKey("tasks.id", ondelete="SET NULL", onupdate="CASCADE"),
nullable=True,
index=True,
)
workplan_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("workplans.id", ondelete="SET NULL"), nullable=True, index=True
UUID(as_uuid=True),
ForeignKey("workplans.id", ondelete="SET NULL", onupdate="CASCADE"),
nullable=True,
index=True,
)
repo_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("managed_repos.id", ondelete="SET NULL"), nullable=True, index=True

View file

@ -0,0 +1,44 @@
from __future__ import annotations
import uuid
from datetime import datetime
from sqlalchemy import CheckConstraint, DateTime, String, UniqueConstraint
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column
from api.models.base import Base, TimestampMixin, new_uuid
class WorkRecordIdentifierAlias(Base, TimestampMixin):
"""Durable provenance for a canonical work-record UUID replacement."""
__tablename__ = "work_record_identifier_aliases"
__table_args__ = (
CheckConstraint(
"record_kind IN ('workplan', 'task')",
name="ck_work_record_identifier_aliases_kind",
),
CheckConstraint(
"migration_status IN ('prepared', 'applied', 'reversed')",
name="ck_work_record_identifier_aliases_status",
),
UniqueConstraint("old_id", name="uq_work_record_identifier_aliases_old_id"),
UniqueConstraint("new_id", name="uq_work_record_identifier_aliases_new_id"),
)
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=new_uuid
)
old_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), nullable=False)
new_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), nullable=False)
record_kind: Mapped[str] = mapped_column(String(20), nullable=False)
record_id: Mapped[str] = mapped_column(String(160), nullable=False, index=True)
repo_slug: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
namespace: Mapped[str] = mapped_column(String(64), nullable=False)
plan_sha256: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
migration_status: Mapped[str] = mapped_column(
String(20), nullable=False, default="prepared", server_default="prepared"
)
applied_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
reversed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)

View file

@ -45,19 +45,19 @@ class WorkplanDependency(Base, TimestampMixin):
)
from_workplan_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("workplans.id", ondelete="CASCADE"),
ForeignKey("workplans.id", ondelete="CASCADE", onupdate="CASCADE"),
nullable=False,
index=True,
)
to_workplan_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True),
ForeignKey("workplans.id", ondelete="CASCADE"),
ForeignKey("workplans.id", ondelete="CASCADE", onupdate="CASCADE"),
nullable=True,
index=True,
)
to_task_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True),
ForeignKey("tasks.id", ondelete="CASCADE"),
ForeignKey("tasks.id", ondelete="CASCADE", onupdate="CASCADE"),
nullable=True,
index=True,
)
@ -72,4 +72,4 @@ class WorkplanDependency(Base, TimestampMixin):
to_workplan: Mapped["Workplan | None"] = relationship( # noqa: F821
"Workplan", foreign_keys=[to_workplan_id]
)
to_task: Mapped["Task | None"] = relationship("Task", foreign_keys=[to_task_id]) # noqa: F821
to_task: Mapped["Task | None"] = relationship("Task", foreign_keys=[to_task_id]) # noqa: F821

View file

@ -15,7 +15,7 @@ class WorkplanLaunchRequest(Base, TimestampMixin):
)
workplan_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("workplans.id", ondelete="CASCADE"),
ForeignKey("workplans.id", ondelete="CASCADE", onupdate="CASCADE"),
nullable=False,
index=True,
)