fix(phase-35): rewrite migration to avoid SQLAlchemy enum auto-create
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled

Replace sa.Enum column types with raw SQL DO $$ IF NOT EXISTS blocks
for enum creation, then sa.Text columns + ALTER TYPE USING casts.
This completely bypasses SQLAlchemy's _on_table_create hook that
triggers CREATE TYPE without checkfirst, causing DuplicateObject
on PostgreSQL when the entrypoint retries after a failed migration.
This commit is contained in:
2026-02-17 16:22:43 +01:00
parent 703dd891d3
commit 7e33746539

View File

@@ -19,36 +19,29 @@ depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None: def upgrade() -> None:
# ── Enums — create once, then reference with create_type=False ─── # ── Enums via raw SQL to avoid any SQLAlchemy auto-create hooks ───
jira_link_entity_type = sa.Enum( op.execute("""
"test", "technique", "campaign", "evidence", DO $$ BEGIN
name="jiralinkentitytype", IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'jiralinkentitytype') THEN
) CREATE TYPE jiralinkentitytype AS ENUM ('test', 'technique', 'campaign', 'evidence');
jira_sync_direction = sa.Enum( END IF;
"aegis_to_jira", "jira_to_aegis", "bidirectional", END $$
name="jirasyncdirection", """)
) op.execute("""
jira_link_entity_type.create(op.get_bind(), checkfirst=True) DO $$ BEGIN
jira_sync_direction.create(op.get_bind(), checkfirst=True) IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'jirasyncdirection') THEN
CREATE TYPE jirasyncdirection AS ENUM ('aegis_to_jira', 'jira_to_aegis', 'bidirectional');
END IF;
END $$
""")
# Re-reference with create_type=False so create_table won't try to # Use sa.String for column types — the actual PG column will use the
# CREATE TYPE again (which causes DuplicateObject on PostgreSQL). # enum type via explicit USING casts below. This completely avoids
entity_type_col = sa.Enum( # SQLAlchemy's _on_table_create hook that causes DuplicateObject.
"test", "technique", "campaign", "evidence",
name="jiralinkentitytype",
create_type=False,
)
sync_dir_col = sa.Enum(
"aegis_to_jira", "jira_to_aegis", "bidirectional",
name="jirasyncdirection",
create_type=False,
)
# ── jira_links table ─────────────────────────────────────────────
op.create_table( op.create_table(
"jira_links", "jira_links",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True), sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("entity_type", entity_type_col, nullable=False), sa.Column("entity_type", sa.Text, nullable=False),
sa.Column("entity_id", postgresql.UUID(as_uuid=True), nullable=False), sa.Column("entity_id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("jira_issue_key", sa.String(50), nullable=False), sa.Column("jira_issue_key", sa.String(50), nullable=False),
sa.Column("jira_issue_id", sa.String(50)), sa.Column("jira_issue_id", sa.String(50)),
@@ -57,13 +50,18 @@ def upgrade() -> None:
sa.Column("jira_priority", sa.String(50)), sa.Column("jira_priority", sa.String(50)),
sa.Column("jira_assignee", sa.String(255)), sa.Column("jira_assignee", sa.String(255)),
sa.Column("jira_story_points", sa.String(10)), sa.Column("jira_story_points", sa.String(10)),
sa.Column("sync_direction", sync_dir_col, server_default="bidirectional"), sa.Column("sync_direction", sa.Text, server_default="bidirectional"),
sa.Column("last_synced_at", sa.DateTime), sa.Column("last_synced_at", sa.DateTime),
sa.Column("sync_metadata", postgresql.JSONB, server_default="{}"), sa.Column("sync_metadata", postgresql.JSONB, server_default="{}"),
sa.Column("created_by", postgresql.UUID(as_uuid=True), sa.ForeignKey("users.id")), sa.Column("created_by", postgresql.UUID(as_uuid=True), sa.ForeignKey("users.id")),
sa.Column("created_at", sa.DateTime, server_default=sa.func.now()), sa.Column("created_at", sa.DateTime, server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime, server_default=sa.func.now()), sa.Column("updated_at", sa.DateTime, server_default=sa.func.now()),
) )
# Now ALTER the columns to use the actual enum types
op.execute("ALTER TABLE jira_links ALTER COLUMN entity_type TYPE jiralinkentitytype USING entity_type::jiralinkentitytype")
op.execute("ALTER TABLE jira_links ALTER COLUMN sync_direction TYPE jirasyncdirection USING sync_direction::jirasyncdirection")
op.create_index("ix_jira_links_entity_id", "jira_links", ["entity_id"]) op.create_index("ix_jira_links_entity_id", "jira_links", ["entity_id"])
op.create_index("ix_jira_links_issue_key", "jira_links", ["jira_issue_key"]) op.create_index("ix_jira_links_issue_key", "jira_links", ["jira_issue_key"])
op.create_index( op.create_index(
@@ -102,5 +100,5 @@ def upgrade() -> None:
def downgrade() -> None: def downgrade() -> None:
op.drop_table("worklogs") op.drop_table("worklogs")
op.drop_table("jira_links") op.drop_table("jira_links")
sa.Enum(name="jirasyncdirection").drop(op.get_bind(), checkfirst=True) op.execute("DROP TYPE IF EXISTS jirasyncdirection")
sa.Enum(name="jiralinkentitytype").drop(op.get_bind(), checkfirst=True) op.execute("DROP TYPE IF EXISTS jiralinkentitytype")