From 005a09b42f2a2079c99e0a753f2ced733876a3c1 Mon Sep 17 00:00:00 2001 From: Kitos Date: Tue, 17 Feb 2026 16:33:42 +0100 Subject: [PATCH] fix(phase-35): use pure SQL for jira_links migration Replace all sa.Enum / op.create_table / ALTER TABLE approach with a single op.execute() containing raw DDL. This sidesteps every SQLAlchemy hook (enum auto-create, default cast conflicts) by letting PostgreSQL handle CREATE TYPE IF NOT EXISTS, CREATE TABLE IF NOT EXISTS, and CREATE INDEX IF NOT EXISTS directly. --- .../b020_add_jira_links_and_worklogs.py | 123 ++++++++---------- 1 file changed, 57 insertions(+), 66 deletions(-) diff --git a/backend/alembic/versions/b020_add_jira_links_and_worklogs.py b/backend/alembic/versions/b020_add_jira_links_and_worklogs.py index 0ad0b18..8e152ff 100644 --- a/backend/alembic/versions/b020_add_jira_links_and_worklogs.py +++ b/backend/alembic/versions/b020_add_jira_links_and_worklogs.py @@ -19,86 +19,77 @@ depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: - # ── Enums via raw SQL to avoid any SQLAlchemy auto-create hooks ─── + # ── jira_links: 100 % raw SQL to avoid all SQLAlchemy enum hooks ── op.execute(""" DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'jiralinkentitytype') THEN CREATE TYPE jiralinkentitytype AS ENUM ('test', 'technique', 'campaign', 'evidence'); END IF; - END $$ - """) - op.execute(""" + END $$; + DO $$ BEGIN 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 $$ + END $$; + + CREATE TABLE IF NOT EXISTS jira_links ( + id UUID PRIMARY KEY, + entity_type jiralinkentitytype NOT NULL, + entity_id UUID NOT NULL, + jira_issue_key VARCHAR(50) NOT NULL, + jira_issue_id VARCHAR(50), + jira_project_key VARCHAR(20), + jira_status VARCHAR(100), + jira_priority VARCHAR(50), + jira_assignee VARCHAR(255), + jira_story_points VARCHAR(10), + sync_direction jirasyncdirection DEFAULT 'bidirectional', + last_synced_at TIMESTAMP, + sync_metadata JSONB DEFAULT '{}', + created_by UUID REFERENCES users(id), + created_at TIMESTAMP DEFAULT now(), + updated_at TIMESTAMP DEFAULT now() + ); + + CREATE INDEX IF NOT EXISTS ix_jira_links_entity_id + ON jira_links (entity_id); + CREATE INDEX IF NOT EXISTS ix_jira_links_issue_key + ON jira_links (jira_issue_key); + CREATE INDEX IF NOT EXISTS ix_jira_links_entity_type_entity_id + ON jira_links (entity_type, entity_id); """) - # Use sa.String for column types — the actual PG column will use the - # enum type via explicit USING casts below. This completely avoids - # SQLAlchemy's _on_table_create hook that causes DuplicateObject. - op.create_table( - "jira_links", - sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True), - sa.Column("entity_type", sa.Text, 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_id", sa.String(50)), - sa.Column("jira_project_key", sa.String(20)), - sa.Column("jira_status", sa.String(100)), - sa.Column("jira_priority", sa.String(50)), - sa.Column("jira_assignee", sa.String(255)), - sa.Column("jira_story_points", sa.String(10)), - sa.Column("sync_direction", sa.Text, server_default="bidirectional"), - sa.Column("last_synced_at", sa.DateTime), - sa.Column("sync_metadata", postgresql.JSONB, server_default="{}"), - 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("updated_at", sa.DateTime, server_default=sa.func.now()), - ) + # ── worklogs table (no enums, straightforward) ─────────────────── + op.execute(""" + CREATE TABLE IF NOT EXISTS worklogs ( + id UUID PRIMARY KEY, + entity_type VARCHAR(50) NOT NULL, + entity_id UUID NOT NULL, + user_id UUID NOT NULL REFERENCES users(id), + activity_type VARCHAR(100) NOT NULL, + started_at TIMESTAMP NOT NULL, + ended_at TIMESTAMP, + duration_seconds INTEGER NOT NULL, + description TEXT, + tempo_synced TIMESTAMP, + tempo_worklog_id VARCHAR(100), + integrity_hash VARCHAR(64), + created_at TIMESTAMP DEFAULT now(), + metadata JSONB DEFAULT '{}' + ); - # 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_issue_key", "jira_links", ["jira_issue_key"]) - op.create_index( - "ix_jira_links_entity_type_entity_id", - "jira_links", - ["entity_type", "entity_id"], - ) - - # ── worklogs table ─────────────────────────────────────────────── - op.create_table( - "worklogs", - sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True), - sa.Column("entity_type", sa.String(50), nullable=False), - sa.Column("entity_id", postgresql.UUID(as_uuid=True), nullable=False), - sa.Column("user_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("users.id"), nullable=False), - sa.Column("activity_type", sa.String(100), nullable=False), - sa.Column("started_at", sa.DateTime, nullable=False), - sa.Column("ended_at", sa.DateTime), - sa.Column("duration_seconds", sa.Integer, nullable=False), - sa.Column("description", sa.Text), - sa.Column("tempo_synced", sa.DateTime), - sa.Column("tempo_worklog_id", sa.String(100)), - sa.Column("integrity_hash", sa.String(64)), - sa.Column("created_at", sa.DateTime, server_default=sa.func.now()), - sa.Column("metadata", postgresql.JSONB, server_default="{}"), - ) - op.create_index("ix_worklogs_entity_id", "worklogs", ["entity_id"]) - op.create_index("ix_worklogs_user_id", "worklogs", ["user_id"]) - op.create_index( - "ix_worklogs_entity_type_entity_id", - "worklogs", - ["entity_type", "entity_id"], - ) + CREATE INDEX IF NOT EXISTS ix_worklogs_entity_id + ON worklogs (entity_id); + CREATE INDEX IF NOT EXISTS ix_worklogs_user_id + ON worklogs (user_id); + CREATE INDEX IF NOT EXISTS ix_worklogs_entity_type_entity_id + ON worklogs (entity_type, entity_id); + """) def downgrade() -> None: - op.drop_table("worklogs") - op.drop_table("jira_links") + op.execute("DROP TABLE IF EXISTS worklogs") + op.execute("DROP TABLE IF EXISTS jira_links") op.execute("DROP TYPE IF EXISTS jirasyncdirection") op.execute("DROP TYPE IF EXISTS jiralinkentitytype")