"""Add agent config and run log tables: local_agent_configs, cloud_agent_configs, agent_run_logs. Revision ID: 003 Revises: 002 Create Date: 2026-03-05 """ from __future__ import annotations from typing import Sequence, Union import sqlalchemy as sa from alembic import op from sqlalchemy.dialects import postgresql revision: str = "003" down_revision: Union[str, None] = "002" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # ── Enum types — idempotent creation ────────────────────────────────── op.execute(""" DO $$ BEGIN CREATE TYPE agent_type AS ENUM ('local', 'cloud'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; """) op.execute(""" DO $$ BEGIN CREATE TYPE agent_run_status AS ENUM ('running', 'success', 'error', 'partial'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; """) op.execute(""" DO $$ BEGIN CREATE TYPE cloud_provider AS ENUM ('gmail', 'teams', 'outlook'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; """) # ── local_agent_configs ─────────────────────────────────────────────── op.create_table( "local_agent_configs", sa.Column("id", postgresql.UUID(as_uuid=False), nullable=False), sa.Column("user_id", postgresql.UUID(as_uuid=False), nullable=False), sa.Column("device_id", sa.String(255), nullable=False), sa.Column("name", sa.String(255), nullable=False), sa.Column("directory_paths", sa.JSON, nullable=False, server_default="[]"), sa.Column("data_types", sa.JSON, nullable=False, server_default="[]"), sa.Column("prompt_template", sa.Text, nullable=False, server_default=""), sa.Column("file_extensions", sa.JSON, nullable=False, server_default="[]"), sa.Column("schedule_cron", sa.String(100), nullable=False, server_default="0 */6 * * *"), sa.Column("enabled", sa.Boolean, nullable=False, server_default=sa.true()), sa.Column("last_run_at", sa.DateTime(timezone=True), nullable=True), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")), sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")), sa.PrimaryKeyConstraint("id"), sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"), ) op.create_index("ix_local_agent_configs_user_id", "local_agent_configs", ["user_id"]) # ── cloud_agent_configs ─────────────────────────────────────────────── op.create_table( "cloud_agent_configs", sa.Column("id", postgresql.UUID(as_uuid=False), nullable=False), sa.Column("user_id", postgresql.UUID(as_uuid=False), nullable=False), sa.Column( "provider", postgresql.ENUM("gmail", "teams", "outlook", name="cloud_provider", create_type=False), nullable=False, ), sa.Column("name", sa.String(255), nullable=False), sa.Column("data_types", sa.JSON, nullable=False, server_default="[]"), sa.Column("prompt_template", sa.Text, nullable=False, server_default=""), sa.Column("oauth_token_encrypted", sa.Text, nullable=True), sa.Column("filter_config", sa.JSON, nullable=True), sa.Column("schedule_cron", sa.String(100), nullable=False, server_default="0 */6 * * *"), sa.Column("enabled", sa.Boolean, nullable=False, server_default=sa.true()), sa.Column("last_run_at", sa.DateTime(timezone=True), nullable=True), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")), sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")), sa.PrimaryKeyConstraint("id"), sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"), ) op.create_index("ix_cloud_agent_configs_user_id", "cloud_agent_configs", ["user_id"]) # ── agent_run_logs ───────────────────────────────────────────────────── op.create_table( "agent_run_logs", sa.Column("id", postgresql.UUID(as_uuid=False), nullable=False), # Plain string — not a FK because it references either local_agent_configs or # cloud_agent_configs depending on agent_type. sa.Column("agent_id", sa.String(255), nullable=False), sa.Column( "agent_type", postgresql.ENUM("local", "cloud", name="agent_type", create_type=False), nullable=False, ), sa.Column("user_id", postgresql.UUID(as_uuid=False), nullable=False), sa.Column( "status", postgresql.ENUM("running", "success", "error", "partial", name="agent_run_status", create_type=False), nullable=False, server_default="running", ), sa.Column("items_processed", sa.Integer, nullable=False, server_default="0"), sa.Column("items_created", sa.Integer, nullable=False, server_default="0"), sa.Column("errors", sa.JSON, nullable=True), sa.Column("started_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")), sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True), sa.PrimaryKeyConstraint("id"), sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"), ) op.create_index("ix_agent_run_logs_user_id", "agent_run_logs", ["user_id"]) op.create_index("ix_agent_run_logs_agent_id", "agent_run_logs", ["agent_id"]) def downgrade() -> None: op.drop_table("agent_run_logs") op.drop_table("cloud_agent_configs") op.drop_table("local_agent_configs") op.execute("DROP TYPE IF EXISTS cloud_provider;") op.execute("DROP TYPE IF EXISTS agent_run_status;") op.execute("DROP TYPE IF EXISTS agent_type;")