fix(phase-35): use pure SQL for jira_links migration
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled

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.
This commit is contained in:
2026-02-17 16:33:42 +01:00
parent 7e33746539
commit 005a09b42f

View File

@@ -19,86 +19,77 @@ depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> 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(""" op.execute("""
DO $$ BEGIN DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'jiralinkentitytype') THEN IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'jiralinkentitytype') THEN
CREATE TYPE jiralinkentitytype AS ENUM ('test', 'technique', 'campaign', 'evidence'); CREATE TYPE jiralinkentitytype AS ENUM ('test', 'technique', 'campaign', 'evidence');
END IF; END IF;
END $$ END $$;
""")
op.execute("""
DO $$ BEGIN DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'jirasyncdirection') THEN IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'jirasyncdirection') THEN
CREATE TYPE jirasyncdirection AS ENUM ('aegis_to_jira', 'jira_to_aegis', 'bidirectional'); CREATE TYPE jirasyncdirection AS ENUM ('aegis_to_jira', 'jira_to_aegis', 'bidirectional');
END IF; 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 # ── worklogs table (no enums, straightforward) ───────────────────
# enum type via explicit USING casts below. This completely avoids op.execute("""
# SQLAlchemy's _on_table_create hook that causes DuplicateObject. CREATE TABLE IF NOT EXISTS worklogs (
op.create_table( id UUID PRIMARY KEY,
"jira_links", entity_type VARCHAR(50) NOT NULL,
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True), entity_id UUID NOT NULL,
sa.Column("entity_type", sa.Text, nullable=False), user_id UUID NOT NULL REFERENCES users(id),
sa.Column("entity_id", postgresql.UUID(as_uuid=True), nullable=False), activity_type VARCHAR(100) NOT NULL,
sa.Column("jira_issue_key", sa.String(50), nullable=False), started_at TIMESTAMP NOT NULL,
sa.Column("jira_issue_id", sa.String(50)), ended_at TIMESTAMP,
sa.Column("jira_project_key", sa.String(20)), duration_seconds INTEGER NOT NULL,
sa.Column("jira_status", sa.String(100)), description TEXT,
sa.Column("jira_priority", sa.String(50)), tempo_synced TIMESTAMP,
sa.Column("jira_assignee", sa.String(255)), tempo_worklog_id VARCHAR(100),
sa.Column("jira_story_points", sa.String(10)), integrity_hash VARCHAR(64),
sa.Column("sync_direction", sa.Text, server_default="bidirectional"), created_at TIMESTAMP DEFAULT now(),
sa.Column("last_synced_at", sa.DateTime), metadata JSONB 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_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 CREATE INDEX IF NOT EXISTS ix_worklogs_entity_id
op.execute("ALTER TABLE jira_links ALTER COLUMN entity_type TYPE jiralinkentitytype USING entity_type::jiralinkentitytype") ON worklogs (entity_id);
op.execute("ALTER TABLE jira_links ALTER COLUMN sync_direction TYPE jirasyncdirection USING sync_direction::jirasyncdirection") CREATE INDEX IF NOT EXISTS ix_worklogs_user_id
ON worklogs (user_id);
op.create_index("ix_jira_links_entity_id", "jira_links", ["entity_id"]) CREATE INDEX IF NOT EXISTS ix_worklogs_entity_type_entity_id
op.create_index("ix_jira_links_issue_key", "jira_links", ["jira_issue_key"]) ON worklogs (entity_type, entity_id);
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"],
)
def downgrade() -> None: def downgrade() -> None:
op.drop_table("worklogs") op.execute("DROP TABLE IF EXISTS worklogs")
op.drop_table("jira_links") op.execute("DROP TABLE IF EXISTS jira_links")
op.execute("DROP TYPE IF EXISTS jirasyncdirection") op.execute("DROP TYPE IF EXISTS jirasyncdirection")
op.execute("DROP TYPE IF EXISTS jiralinkentitytype") op.execute("DROP TYPE IF EXISTS jiralinkentitytype")