feat(migrations): fix automation tables migration for idempotency

- Updated migration script to ensure ENUM types and tables are created only if they do not already exist, preventing errors on re-runs.
- Added 'IF NOT EXISTS' clauses to table and index creation statements for improved safety during migrations.
- Ensured that the migration can be safely re-applied without causing conflicts or failures.
This commit is contained in:
DESKTOP-RTLN3BA\$punk 2026-06-01 17:40:03 -07:00
parent 2881b1be8c
commit bb5c8e49a8

View file

@ -25,34 +25,60 @@ depends_on: str | Sequence[str] | None = None
def upgrade() -> None: def upgrade() -> None:
# ENUM types (PostgreSQL requires types created before tables that use them) # Guard every object so the migration is safe to re-run after a partial
# apply (the types/tables outlive a failed run that never advanced
# alembic_version). Types must precede the tables that reference them.
op.execute( op.execute(
""" """
CREATE TYPE automation_status AS ENUM ( DO $$
'active', 'paused', 'archived' BEGIN
); IF NOT EXISTS (
SELECT 1 FROM pg_type WHERE typname = 'automation_status'
) THEN
CREATE TYPE automation_status AS ENUM (
'active', 'paused', 'archived'
);
END IF;
END
$$;
""" """
) )
op.execute( op.execute(
""" """
CREATE TYPE automation_trigger_type AS ENUM ( DO $$
'schedule', 'manual' BEGIN
); IF NOT EXISTS (
SELECT 1 FROM pg_type WHERE typname = 'automation_trigger_type'
) THEN
CREATE TYPE automation_trigger_type AS ENUM (
'schedule', 'manual'
);
END IF;
END
$$;
""" """
) )
op.execute( op.execute(
""" """
CREATE TYPE automation_run_status AS ENUM ( DO $$
'pending', 'running', 'succeeded', 'failed', BEGIN
'cancelled', 'timed_out' IF NOT EXISTS (
); SELECT 1 FROM pg_type WHERE typname = 'automation_run_status'
) THEN
CREATE TYPE automation_run_status AS ENUM (
'pending', 'running', 'succeeded', 'failed',
'cancelled', 'timed_out'
);
END IF;
END
$$;
""" """
) )
# automations — the editable, versioned automation definition # automations — the editable, versioned automation definition
op.execute( op.execute(
""" """
CREATE TABLE automations ( CREATE TABLE IF NOT EXISTS automations (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
search_space_id INTEGER NOT NULL search_space_id INTEGER NOT NULL
REFERENCES searchspaces(id) ON DELETE CASCADE, REFERENCES searchspaces(id) ON DELETE CASCADE,
@ -69,19 +95,25 @@ def upgrade() -> None:
""" """
) )
op.execute( op.execute(
"CREATE INDEX ix_automations_search_space_id ON automations(search_space_id);" "CREATE INDEX IF NOT EXISTS ix_automations_search_space_id ON automations(search_space_id);"
) )
op.execute( op.execute(
"CREATE INDEX ix_automations_created_by_user_id ON automations(created_by_user_id);" "CREATE INDEX IF NOT EXISTS ix_automations_created_by_user_id ON automations(created_by_user_id);"
)
op.execute(
"CREATE INDEX IF NOT EXISTS ix_automations_status ON automations(status);"
)
op.execute(
"CREATE INDEX IF NOT EXISTS ix_automations_created_at ON automations(created_at);"
)
op.execute(
"CREATE INDEX IF NOT EXISTS ix_automations_updated_at ON automations(updated_at);"
) )
op.execute("CREATE INDEX ix_automations_status ON automations(status);")
op.execute("CREATE INDEX ix_automations_created_at ON automations(created_at);")
op.execute("CREATE INDEX ix_automations_updated_at ON automations(updated_at);")
# automation_triggers — one row per (automation, trigger-instance) pair # automation_triggers — one row per (automation, trigger-instance) pair
op.execute( op.execute(
""" """
CREATE TABLE automation_triggers ( CREATE TABLE IF NOT EXISTS automation_triggers (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
automation_id INTEGER NOT NULL automation_id INTEGER NOT NULL
REFERENCES automations(id) ON DELETE CASCADE, REFERENCES automations(id) ON DELETE CASCADE,
@ -96,20 +128,22 @@ def upgrade() -> None:
""" """
) )
op.execute( op.execute(
"CREATE INDEX ix_automation_triggers_automation_id ON automation_triggers(automation_id);" "CREATE INDEX IF NOT EXISTS ix_automation_triggers_automation_id ON automation_triggers(automation_id);"
)
op.execute("CREATE INDEX ix_automation_triggers_type ON automation_triggers(type);")
op.execute(
"CREATE INDEX ix_automation_triggers_enabled ON automation_triggers(enabled);"
) )
op.execute( op.execute(
"CREATE INDEX ix_automation_triggers_created_at ON automation_triggers(created_at);" "CREATE INDEX IF NOT EXISTS ix_automation_triggers_type ON automation_triggers(type);"
)
op.execute(
"CREATE INDEX IF NOT EXISTS ix_automation_triggers_enabled ON automation_triggers(enabled);"
)
op.execute(
"CREATE INDEX IF NOT EXISTS ix_automation_triggers_created_at ON automation_triggers(created_at);"
) )
# Partial index for the schedule tick: only enabled schedule triggers # Partial index for the schedule tick: only enabled schedule triggers
# with a scheduled next fire are ever scanned for due rows. # with a scheduled next fire are ever scanned for due rows.
op.execute( op.execute(
""" """
CREATE INDEX ix_automation_triggers_due CREATE INDEX IF NOT EXISTS ix_automation_triggers_due
ON automation_triggers (next_fire_at) ON automation_triggers (next_fire_at)
WHERE enabled = true WHERE enabled = true
AND type = 'schedule' AND type = 'schedule'
@ -120,7 +154,7 @@ def upgrade() -> None:
# automation_runs — the immutable per-fire execution record # automation_runs — the immutable per-fire execution record
op.execute( op.execute(
""" """
CREATE TABLE automation_runs ( CREATE TABLE IF NOT EXISTS automation_runs (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
automation_id INTEGER NOT NULL automation_id INTEGER NOT NULL
REFERENCES automations(id) ON DELETE CASCADE, REFERENCES automations(id) ON DELETE CASCADE,
@ -140,14 +174,16 @@ def upgrade() -> None:
""" """
) )
op.execute( op.execute(
"CREATE INDEX ix_automation_runs_automation_id ON automation_runs(automation_id);" "CREATE INDEX IF NOT EXISTS ix_automation_runs_automation_id ON automation_runs(automation_id);"
) )
op.execute( op.execute(
"CREATE INDEX ix_automation_runs_trigger_id ON automation_runs(trigger_id);" "CREATE INDEX IF NOT EXISTS ix_automation_runs_trigger_id ON automation_runs(trigger_id);"
) )
op.execute("CREATE INDEX ix_automation_runs_status ON automation_runs(status);")
op.execute( op.execute(
"CREATE INDEX ix_automation_runs_created_at ON automation_runs(created_at);" "CREATE INDEX IF NOT EXISTS ix_automation_runs_status ON automation_runs(status);"
)
op.execute(
"CREATE INDEX IF NOT EXISTS ix_automation_runs_created_at ON automation_runs(created_at);"
) )