diff --git a/surfsense_backend/alembic/versions/144_add_automation_tables.py b/surfsense_backend/alembic/versions/144_add_automation_tables.py index 39f927417..296c33585 100644 --- a/surfsense_backend/alembic/versions/144_add_automation_tables.py +++ b/surfsense_backend/alembic/versions/144_add_automation_tables.py @@ -25,34 +25,60 @@ depends_on: str | Sequence[str] | None = 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( """ - CREATE TYPE automation_status AS ENUM ( - 'active', 'paused', 'archived' - ); + DO $$ + 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( """ - CREATE TYPE automation_trigger_type AS ENUM ( - 'schedule', 'manual' - ); + DO $$ + 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( """ - CREATE TYPE automation_run_status AS ENUM ( - 'pending', 'running', 'succeeded', 'failed', - 'cancelled', 'timed_out' - ); + DO $$ + BEGIN + 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 op.execute( """ - CREATE TABLE automations ( + CREATE TABLE IF NOT EXISTS automations ( id SERIAL PRIMARY KEY, search_space_id INTEGER NOT NULL REFERENCES searchspaces(id) ON DELETE CASCADE, @@ -69,19 +95,25 @@ def upgrade() -> None: """ ) 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( - "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 op.execute( """ - CREATE TABLE automation_triggers ( + CREATE TABLE IF NOT EXISTS automation_triggers ( id SERIAL PRIMARY KEY, automation_id INTEGER NOT NULL REFERENCES automations(id) ON DELETE CASCADE, @@ -96,20 +128,22 @@ def upgrade() -> None: """ ) op.execute( - "CREATE INDEX 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);" + "CREATE INDEX IF NOT EXISTS ix_automation_triggers_automation_id ON automation_triggers(automation_id);" ) 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 # with a scheduled next fire are ever scanned for due rows. op.execute( """ - CREATE INDEX ix_automation_triggers_due + CREATE INDEX IF NOT EXISTS ix_automation_triggers_due ON automation_triggers (next_fire_at) WHERE enabled = true AND type = 'schedule' @@ -120,7 +154,7 @@ def upgrade() -> None: # automation_runs — the immutable per-fire execution record op.execute( """ - CREATE TABLE automation_runs ( + CREATE TABLE IF NOT EXISTS automation_runs ( id SERIAL PRIMARY KEY, automation_id INTEGER NOT NULL REFERENCES automations(id) ON DELETE CASCADE, @@ -140,14 +174,16 @@ def upgrade() -> None: """ ) 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( - "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( - "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);" )