"""Complete V3 read-only operations foundation. Revision ID: 202607090001 Revises: 202607080002 Create Date: 2026-07-09 """ from alembic import op import sqlalchemy as sa from sqlalchemy import inspect revision = "202607090001" down_revision = "202607080002" branch_labels = None depends_on = None DOMAIN_EVENTS_TABLE = "domain_events" AI_MEMORY_ENTRIES_TABLE = "ai_memory_entries" SYSTEM_HEARTBEATS_TABLE = "system_heartbeats" 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: for column in [ sa.Column("next_attempt_at", sa.DateTime(), nullable=True), sa.Column("locked_until", sa.DateTime(), nullable=True), sa.Column("locked_by", sa.String(length=128), nullable=True), sa.Column("max_attempts", sa.Integer(), nullable=True), ]: _add_column_if_missing(DOMAIN_EVENTS_TABLE, column) _create_indexes( DOMAIN_EVENTS_TABLE, [ ("next_attempt_at", False), ("locked_until", False), ("locked_by", False), ], ) if not _table_exists(AI_MEMORY_ENTRIES_TABLE): op.create_table( AI_MEMORY_ENTRIES_TABLE, sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), sa.Column("code", sa.String(length=64), nullable=False), sa.Column("scope", sa.String(length=64), nullable=False), sa.Column("subject", sa.String(length=128), nullable=False), sa.Column("content", sa.Text(), nullable=False), sa.Column("summary", sa.Text(), nullable=True), sa.Column("tags", sa.JSON(), nullable=True), sa.Column("source", sa.String(length=64), nullable=False), sa.Column("importance", sa.Integer(), nullable=False), sa.Column("status", sa.String(length=32), nullable=False), sa.Column("actor", sa.String(length=128), nullable=False), sa.Column("last_used_at", sa.DateTime(), nullable=True), sa.Column("expires_at", sa.DateTime(), nullable=True), sa.Column("created_at", sa.DateTime(), nullable=False), sa.Column("updated_at", sa.DateTime(), nullable=False), sa.PrimaryKeyConstraint("id"), ) _create_indexes( AI_MEMORY_ENTRIES_TABLE, [ ("code", True), ("scope", False), ("subject", False), ("source", False), ("importance", False), ("status", False), ("actor", False), ("last_used_at", False), ("expires_at", False), ("created_at", False), ], ) if not _table_exists(SYSTEM_HEARTBEATS_TABLE): op.create_table( SYSTEM_HEARTBEATS_TABLE, sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), sa.Column("component", sa.String(length=128), nullable=False), sa.Column("instance_id", sa.String(length=128), nullable=False), sa.Column("status", sa.String(length=32), nullable=False), sa.Column("last_seen_at", sa.DateTime(), nullable=False), sa.Column("created_at", sa.DateTime(), nullable=False), sa.Column("updated_at", sa.DateTime(), nullable=False), sa.PrimaryKeyConstraint("id"), ) _create_indexes( SYSTEM_HEARTBEATS_TABLE, [ ("component", False), ("instance_id", False), ("status", False), ("last_seen_at", False), ("created_at", False), ], ) def downgrade() -> None: for table_name in [SYSTEM_HEARTBEATS_TABLE, AI_MEMORY_ENTRIES_TABLE]: if _table_exists(table_name): op.drop_table(table_name) event_columns = _column_names(DOMAIN_EVENTS_TABLE) for column_name in ["locked_by", "locked_until", "next_attempt_at"]: if column_name in event_columns: op.drop_index(op.f(f"ix_{DOMAIN_EVENTS_TABLE}_{column_name}"), table_name=DOMAIN_EVENTS_TABLE) op.drop_column(DOMAIN_EVENTS_TABLE, column_name) if "max_attempts" in _column_names(DOMAIN_EVENTS_TABLE): op.drop_column(DOMAIN_EVENTS_TABLE, "max_attempts")