"""Add V3 enterprise automation foundation tables. Revision ID: 202607080002 Revises: 202607080001 Create Date: 2026-07-08 """ from alembic import op import sqlalchemy as sa from sqlalchemy import inspect revision = "202607080002" down_revision = "202607080001" branch_labels = None depends_on = None AUDIT_LOGS_TABLE = "audit_logs" DOMAIN_EVENTS_TABLE = "domain_events" WORKFLOW_INSTANCES_TABLE = "workflow_instances" WORKFLOW_ACTIONS_TABLE = "workflow_actions" OFFICIAL_WRITEBACK_RUNS_TABLE = "official_writeback_runs" def _table_exists(table_name: str) -> bool: inspector = inspect(op.get_bind()) return table_name in inspector.get_table_names() def _column_names(table_name: str) -> set[str]: inspector = inspect(op.get_bind()) if table_name not in inspector.get_table_names(): return set() return {column["name"] for column in inspector.get_columns(table_name)} def _add_column_if_missing(table_name: str, column: sa.Column) -> None: if column.name not in _column_names(table_name): op.add_column(table_name, column) def _create_indexes(table_name: str, indexes: list[tuple[str, bool]]) -> None: for column_name, unique in indexes: op.create_index( op.f(f"ix_{table_name}_{column_name}"), table_name, [column_name], unique=unique, if_not_exists=True, ) def upgrade() -> None: _add_column_if_missing( AUDIT_LOGS_TABLE, sa.Column("request_id", sa.String(length=64), nullable=True), ) op.create_index( op.f("ix_audit_logs_request_id"), AUDIT_LOGS_TABLE, ["request_id"], unique=False, if_not_exists=True, ) if not _table_exists(DOMAIN_EVENTS_TABLE): op.create_table( DOMAIN_EVENTS_TABLE, sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), sa.Column("event_id", sa.String(length=64), nullable=False), sa.Column("event_type", sa.String(length=128), nullable=False), sa.Column("source", sa.String(length=64), nullable=False), sa.Column("aggregate_type", sa.String(length=128), nullable=False), sa.Column("aggregate_id", sa.String(length=128), nullable=True), sa.Column("actor", sa.String(length=128), nullable=False), sa.Column("payload", sa.JSON(), nullable=True), sa.Column("status", sa.String(length=32), nullable=False), sa.Column("attempts", sa.Integer(), nullable=False), sa.Column("idempotency_key", sa.String(length=255), nullable=True), sa.Column("last_error", sa.Text(), nullable=True), sa.Column("created_at", sa.DateTime(), nullable=False), sa.Column("processed_at", sa.DateTime(), nullable=True), sa.PrimaryKeyConstraint("id"), ) _create_indexes( DOMAIN_EVENTS_TABLE, [ ("event_id", True), ("event_type", False), ("source", False), ("aggregate_type", False), ("aggregate_id", False), ("actor", False), ("status", False), ("idempotency_key", True), ("created_at", False), ("processed_at", False), ], ) if not _table_exists(WORKFLOW_INSTANCES_TABLE): op.create_table( WORKFLOW_INSTANCES_TABLE, sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), sa.Column("code", sa.String(length=64), nullable=False), sa.Column("workflow_type", sa.String(length=128), nullable=False), sa.Column("aggregate_type", sa.String(length=128), nullable=False), sa.Column("aggregate_id", sa.String(length=128), nullable=True), sa.Column("status", sa.String(length=32), nullable=False), sa.Column("actor", sa.String(length=128), nullable=False), sa.Column("current_step", sa.String(length=128), nullable=True), sa.Column("payload", sa.JSON(), nullable=True), sa.Column("created_at", sa.DateTime(), nullable=False), sa.Column("updated_at", sa.DateTime(), nullable=False), sa.Column("completed_at", sa.DateTime(), nullable=True), sa.PrimaryKeyConstraint("id"), ) _create_indexes( WORKFLOW_INSTANCES_TABLE, [ ("code", True), ("workflow_type", False), ("aggregate_type", False), ("aggregate_id", False), ("status", False), ("actor", False), ("created_at", False), ("completed_at", False), ], ) if not _table_exists(WORKFLOW_ACTIONS_TABLE): op.create_table( WORKFLOW_ACTIONS_TABLE, sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), sa.Column("code", sa.String(length=64), nullable=False), sa.Column("workflow_code", sa.String(length=64), nullable=False), sa.Column("action", sa.String(length=128), nullable=False), sa.Column("actor", sa.String(length=128), nullable=False), sa.Column("from_status", sa.String(length=32), nullable=True), sa.Column("to_status", sa.String(length=32), nullable=True), sa.Column("payload", sa.JSON(), nullable=True), sa.Column("created_at", sa.DateTime(), nullable=False), sa.PrimaryKeyConstraint("id"), ) _create_indexes( WORKFLOW_ACTIONS_TABLE, [ ("code", True), ("workflow_code", False), ("action", False), ("actor", False), ("created_at", False), ], ) if not _table_exists(OFFICIAL_WRITEBACK_RUNS_TABLE): op.create_table( OFFICIAL_WRITEBACK_RUNS_TABLE, sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), sa.Column("code", sa.String(length=64), nullable=False), sa.Column("domain", sa.String(length=128), nullable=False), sa.Column("record_id", sa.String(length=128), nullable=True), sa.Column("action", sa.String(length=128), nullable=False), sa.Column("actor", sa.String(length=128), nullable=False), sa.Column("status", sa.String(length=32), nullable=False), sa.Column("approval_ticket_id", sa.String(length=64), nullable=True), sa.Column("idempotency_key", sa.String(length=255), nullable=True), sa.Column("request_payload", sa.JSON(), nullable=True), sa.Column("provider_response", sa.JSON(), nullable=True), sa.Column("error_message", sa.Text(), nullable=True), sa.Column("created_at", sa.DateTime(), nullable=False), sa.Column("updated_at", sa.DateTime(), nullable=False), sa.Column("submitted_at", sa.DateTime(), nullable=True), sa.Column("sent_at", sa.DateTime(), nullable=True), sa.PrimaryKeyConstraint("id"), ) _create_indexes( OFFICIAL_WRITEBACK_RUNS_TABLE, [ ("code", True), ("domain", False), ("record_id", False), ("action", False), ("actor", False), ("status", False), ("approval_ticket_id", False), ("idempotency_key", True), ("created_at", False), ("submitted_at", False), ("sent_at", False), ], ) def downgrade() -> None: for table_name in [ OFFICIAL_WRITEBACK_RUNS_TABLE, WORKFLOW_ACTIONS_TABLE, WORKFLOW_INSTANCES_TABLE, DOMAIN_EVENTS_TABLE, ]: if _table_exists(table_name): op.drop_table(table_name) if "request_id" in _column_names(AUDIT_LOGS_TABLE): op.drop_index(op.f("ix_audit_logs_request_id"), table_name=AUDIT_LOGS_TABLE) op.drop_column(AUDIT_LOGS_TABLE, "request_id")