feat(identifiers): add reversible projection migration
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a023c0-a0a3-7c03-b395-5a0d2757214d
This commit is contained in:
parent
ce52e9d1e2
commit
cb1b028fd1
18 changed files with 921 additions and 27 deletions
|
|
@ -0,0 +1,182 @@
|
|||
"""add reversible work-record identifier migration support
|
||||
|
||||
Revision ID: b8d4f0a2c6e1
|
||||
Revises: a7c3e9f1b4d2
|
||||
Create Date: 2026-08-22
|
||||
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
|
||||
revision = "b8d4f0a2c6e1"
|
||||
down_revision = "a7c3e9f1b4d2"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
_CASCADE_REFERENCES = """
|
||||
DO $$
|
||||
DECLARE
|
||||
fk RECORD;
|
||||
updated_definition TEXT;
|
||||
BEGIN
|
||||
FOR fk IN
|
||||
SELECT
|
||||
constraint_row.conrelid::regclass AS relation_name,
|
||||
constraint_row.conname AS constraint_name,
|
||||
pg_get_constraintdef(constraint_row.oid) AS definition
|
||||
FROM pg_constraint AS constraint_row
|
||||
WHERE constraint_row.contype = 'f'
|
||||
AND constraint_row.confrelid IN (
|
||||
'workplans'::regclass,
|
||||
'tasks'::regclass
|
||||
)
|
||||
LOOP
|
||||
updated_definition := regexp_replace(
|
||||
fk.definition,
|
||||
' ON UPDATE (NO ACTION|RESTRICT|CASCADE|SET NULL|SET DEFAULT)',
|
||||
'',
|
||||
'i'
|
||||
);
|
||||
IF position(' ON DELETE ' IN updated_definition) > 0 THEN
|
||||
updated_definition := regexp_replace(
|
||||
updated_definition,
|
||||
' ON DELETE ',
|
||||
' ON UPDATE CASCADE ON DELETE ',
|
||||
'i'
|
||||
);
|
||||
ELSE
|
||||
updated_definition := updated_definition || ' ON UPDATE CASCADE';
|
||||
END IF;
|
||||
|
||||
EXECUTE format(
|
||||
'ALTER TABLE %s DROP CONSTRAINT %I',
|
||||
fk.relation_name,
|
||||
fk.constraint_name
|
||||
);
|
||||
EXECUTE format(
|
||||
'ALTER TABLE %s ADD CONSTRAINT %I %s',
|
||||
fk.relation_name,
|
||||
fk.constraint_name,
|
||||
updated_definition
|
||||
);
|
||||
END LOOP;
|
||||
END $$;
|
||||
"""
|
||||
|
||||
_REMOVE_UPDATE_CASCADE = """
|
||||
DO $$
|
||||
DECLARE
|
||||
fk RECORD;
|
||||
updated_definition TEXT;
|
||||
BEGIN
|
||||
FOR fk IN
|
||||
SELECT
|
||||
constraint_row.conrelid::regclass AS relation_name,
|
||||
constraint_row.conname AS constraint_name,
|
||||
pg_get_constraintdef(constraint_row.oid) AS definition
|
||||
FROM pg_constraint AS constraint_row
|
||||
WHERE constraint_row.contype = 'f'
|
||||
AND constraint_row.confrelid IN (
|
||||
'workplans'::regclass,
|
||||
'tasks'::regclass
|
||||
)
|
||||
LOOP
|
||||
updated_definition := regexp_replace(
|
||||
fk.definition,
|
||||
' ON UPDATE CASCADE',
|
||||
'',
|
||||
'i'
|
||||
);
|
||||
EXECUTE format(
|
||||
'ALTER TABLE %s DROP CONSTRAINT %I',
|
||||
fk.relation_name,
|
||||
fk.constraint_name
|
||||
);
|
||||
EXECUTE format(
|
||||
'ALTER TABLE %s ADD CONSTRAINT %I %s',
|
||||
fk.relation_name,
|
||||
fk.constraint_name,
|
||||
updated_definition
|
||||
);
|
||||
END LOOP;
|
||||
END $$;
|
||||
"""
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"work_record_identifier_aliases",
|
||||
sa.Column("id", UUID(as_uuid=True), primary_key=True),
|
||||
sa.Column("old_id", UUID(as_uuid=True), nullable=False),
|
||||
sa.Column("new_id", UUID(as_uuid=True), nullable=False),
|
||||
sa.Column("record_kind", sa.String(length=20), nullable=False),
|
||||
sa.Column("record_id", sa.String(length=160), nullable=False),
|
||||
sa.Column("repo_slug", sa.String(length=100), nullable=False),
|
||||
sa.Column("namespace", sa.String(length=64), nullable=False),
|
||||
sa.Column("plan_sha256", sa.String(length=64), nullable=False),
|
||||
sa.Column(
|
||||
"migration_status",
|
||||
sa.String(length=20),
|
||||
nullable=False,
|
||||
server_default="prepared",
|
||||
),
|
||||
sa.Column("applied_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("reversed_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"record_kind IN ('workplan', 'task')",
|
||||
name="ck_work_record_identifier_aliases_kind",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"migration_status IN ('prepared', 'applied', 'reversed')",
|
||||
name="ck_work_record_identifier_aliases_status",
|
||||
),
|
||||
sa.UniqueConstraint("old_id", name="uq_work_record_identifier_aliases_old_id"),
|
||||
sa.UniqueConstraint("new_id", name="uq_work_record_identifier_aliases_new_id"),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_work_record_identifier_aliases_record_id",
|
||||
"work_record_identifier_aliases",
|
||||
["record_id"],
|
||||
)
|
||||
op.create_index(
|
||||
"ix_work_record_identifier_aliases_repo_slug",
|
||||
"work_record_identifier_aliases",
|
||||
["repo_slug"],
|
||||
)
|
||||
op.create_index(
|
||||
"ix_work_record_identifier_aliases_plan_sha256",
|
||||
"work_record_identifier_aliases",
|
||||
["plan_sha256"],
|
||||
)
|
||||
op.execute(_CASCADE_REFERENCES)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute(_REMOVE_UPDATE_CASCADE)
|
||||
op.drop_index(
|
||||
"ix_work_record_identifier_aliases_plan_sha256",
|
||||
table_name="work_record_identifier_aliases",
|
||||
)
|
||||
op.drop_index(
|
||||
"ix_work_record_identifier_aliases_repo_slug",
|
||||
table_name="work_record_identifier_aliases",
|
||||
)
|
||||
op.drop_index(
|
||||
"ix_work_record_identifier_aliases_record_id",
|
||||
table_name="work_record_identifier_aliases",
|
||||
)
|
||||
op.drop_table("work_record_identifier_aliases")
|
||||
Loading…
Add table
Add a link
Reference in a new issue