"""Schema repair migration — idempotently add all columns/tables that may be missing. Staging's local postgres was stamped to 0034 via the deploy script's escape hatch before migrations 0019–0034 actually ran. This migration re-applies every structural change from that range using ADD COLUMN IF NOT EXISTS and CREATE TABLE IF NOT EXISTS, so it is safe to run on both fresh instances and partially-migrated ones. Revision ID: 0035 Revises: 0034 """ from __future__ import annotations from alembic import op import sqlalchemy as sa from sqlalchemy import text from sqlalchemy.dialects.postgresql import JSONB, ARRAY revision = "0035" down_revision = "0034" branch_labels = None depends_on = None def _has_column(conn, table: str, column: str) -> bool: result = conn.execute(text( "SELECT 1 FROM information_schema.columns " "WHERE table_schema='public' AND table_name=:t AND column_name=:c" ), {"t": table, "c": column}) return result.fetchone() is not None def _has_table(conn, table: str) -> bool: result = conn.execute(text( "SELECT 1 FROM information_schema.tables " "WHERE table_schema='public' AND table_name=:t" ), {"t": table}) return result.fetchone() is not None def upgrade() -> None: conn = op.get_bind() # ── musehub_commits ──────────────────────────────────────────────────────── # From migration 0019: agent_id, model_id, commit_branch if not _has_column(conn, "musehub_commits", "agent_id"): op.add_column("musehub_commits", sa.Column("agent_id", sa.String(255), nullable=True, server_default="")) if not _has_column(conn, "musehub_commits", "model_id"): op.add_column("musehub_commits", sa.Column("model_id", sa.String(255), nullable=True, server_default="")) if not _has_column(conn, "musehub_commits", "commit_branch"): op.add_column("musehub_commits", sa.Column("commit_branch", sa.String(255), nullable=True)) # From migration 0020: signature, signer_public_key, signer_key_id, toolchain_id, # sem_ver_bump, breaking_changes, reviewed_by, test_runs, prompt_hash if not _has_column(conn, "musehub_commits", "signature"): op.add_column("musehub_commits", sa.Column("signature", sa.Text(), nullable=True, server_default="")) if not _has_column(conn, "musehub_commits", "signer_public_key"): op.add_column("musehub_commits", sa.Column("signer_public_key", sa.Text(), nullable=True, server_default="")) if not _has_column(conn, "musehub_commits", "signer_key_id"): op.add_column("musehub_commits", sa.Column("signer_key_id", sa.String(255), nullable=True, server_default="")) if not _has_column(conn, "musehub_commits", "toolchain_id"): op.add_column("musehub_commits", sa.Column("toolchain_id", sa.String(255), nullable=True, server_default="")) if not _has_column(conn, "musehub_commits", "sem_ver_bump"): op.add_column("musehub_commits", sa.Column("sem_ver_bump", sa.String(10), nullable=True, server_default="none")) if not _has_column(conn, "musehub_commits", "breaking_changes"): op.add_column("musehub_commits", sa.Column("breaking_changes", ARRAY(sa.Text()), nullable=True)) if not _has_column(conn, "musehub_commits", "reviewed_by"): op.add_column("musehub_commits", sa.Column("reviewed_by", ARRAY(sa.Text()), nullable=True)) if not _has_column(conn, "musehub_commits", "test_runs"): op.add_column("musehub_commits", sa.Column("test_runs", sa.Integer(), nullable=True, server_default="0")) if not _has_column(conn, "musehub_commits", "prompt_hash"): op.add_column("musehub_commits", sa.Column("prompt_hash", sa.String(255), nullable=True, server_default="")) # From migration 0021: structured_delta if not _has_column(conn, "musehub_commits", "structured_delta"): op.add_column("musehub_commits", sa.Column("structured_delta", JSONB(), nullable=True)) # Drop legacy commit_meta if still present (was dropped in 0020) if _has_column(conn, "musehub_commits", "commit_meta"): op.drop_column("musehub_commits", "commit_meta") # ── musehub_symbol_history_entries ───────────────────────────────────────── # From migration 0018: op_payload if not _has_column(conn, "musehub_symbol_history_entries", "op_payload"): op.add_column("musehub_symbol_history_entries", sa.Column("op_payload", JSONB(), nullable=True)) # From migration 0031: message, commit_branch if not _has_column(conn, "musehub_symbol_history_entries", "message"): op.add_column("musehub_symbol_history_entries", sa.Column("message", sa.Text(), nullable=True)) if not _has_column(conn, "musehub_symbol_history_entries", "commit_branch"): op.add_column("musehub_symbol_history_entries", sa.Column("commit_branch", sa.String(512), nullable=True)) # ── musehub_symbol_vitals ────────────────────────────────────────────────── # From migration 0031 (re-created with new schema) if not _has_table(conn, "musehub_symbol_vitals"): op.create_table( "musehub_symbol_vitals", sa.Column("repo_id", sa.String(128), sa.ForeignKey("musehub_repos.repo_id", ondelete="CASCADE"), primary_key=True, nullable=False), sa.Column("address", sa.String(512), primary_key=True, nullable=False), sa.Column("first_introduced", sa.DateTime(timezone=True), nullable=True), sa.Column("change_count", sa.Integer, nullable=False, server_default="0"), sa.Column("version_count", sa.Integer, nullable=False, server_default="0"), sa.Column("op_add", sa.Integer, nullable=False, server_default="0"), sa.Column("op_modify", sa.Integer, nullable=False, server_default="0"), sa.Column("op_delete", sa.Integer, nullable=False, server_default="0"), sa.Column("op_move", sa.Integer, nullable=False, server_default="0"), ) op.create_index("ix_symbol_vitals_repo", "musehub_symbol_vitals", ["repo_id"]) # From migration 0032: coupling_count if not _has_column(conn, "musehub_symbol_vitals", "coupling_count"): op.add_column("musehub_symbol_vitals", sa.Column("coupling_count", sa.Integer, nullable=False, server_default="0")) # ── musehub_symbol_coupling ──────────────────────────────────────────────── if not _has_table(conn, "musehub_symbol_coupling"): op.create_table( "musehub_symbol_coupling", sa.Column("repo_id", sa.String(128), sa.ForeignKey("musehub_repos.repo_id", ondelete="CASCADE"), primary_key=True, nullable=False), sa.Column("address", sa.String(512), primary_key=True, nullable=False), sa.Column("co_address", sa.String(512), primary_key=True, nullable=False), sa.Column("shared_commits", sa.Integer, nullable=False, server_default="0"), ) op.create_index("ix_symbol_coupling_repo_address", "musehub_symbol_coupling", ["repo_id", "address"]) # ── musehub_coord_records ────────────────────────────────────────────────── # From migration 0027: rename record_uuid → record_id if _has_column(conn, "musehub_coord_records", "record_uuid") and not _has_column(conn, "musehub_coord_records", "record_id"): op.alter_column("musehub_coord_records", "record_uuid", new_column_name="record_id", type_=sa.String(128), existing_type=sa.String(36), existing_nullable=False) # ── musehub_file_last_commits ────────────────────────────────────────────── # From migration 0034 (may not have run) if not _has_table(conn, "musehub_file_last_commits"): op.create_table( "musehub_file_last_commits", sa.Column("repo_id", sa.String(128), sa.ForeignKey("musehub_repos.repo_id", ondelete="CASCADE"), primary_key=True), sa.Column("branch", sa.String(255), primary_key=True), sa.Column("path", sa.Text, primary_key=True), sa.Column("commit_id", sa.String(128), nullable=False), sa.Column("commit_message", sa.Text, nullable=False, server_default=""), sa.Column("commit_author", sa.String(255), nullable=False, server_default=""), sa.Column("commit_timestamp", sa.DateTime(timezone=True), nullable=False), sa.Column("agent_id", sa.String(128), nullable=True), sa.Column("model_id", sa.String(128), nullable=True), ) op.create_index("ix_file_last_commits_repo_branch", "musehub_file_last_commits", ["repo_id", "branch"], if_not_exists=True) def downgrade() -> None: from sqlalchemy import text # Drop tables this migration conditionally created (IF EXISTS — idempotent with owning migrations). op.execute(text("DROP INDEX IF EXISTS ix_symbol_coupling_repo_address")) op.execute(text("DROP TABLE IF EXISTS musehub_symbol_coupling")) op.execute(text("DROP INDEX IF EXISTS ix_symbol_vitals_repo")) op.execute(text("DROP TABLE IF EXISTS musehub_symbol_vitals")) op.execute(text("DROP INDEX IF EXISTS ix_file_last_commits_repo_branch")) op.execute(text("DROP TABLE IF EXISTS musehub_file_last_commits")) # Undo column repairs — all IF EXISTS so idempotent with owning migrations. for col in ("structured_delta", "prompt_hash", "test_runs", "reviewed_by", "breaking_changes", "sem_ver_bump", "toolchain_id", "signer_key_id", "signer_public_key", "signature", "commit_branch", "model_id", "agent_id"): op.execute(text(f"ALTER TABLE musehub_commits DROP COLUMN IF EXISTS {col}")) for col in ("commit_branch", "message", "op_payload"): op.execute(text(f"ALTER TABLE musehub_symbol_history_entries DROP COLUMN IF EXISTS {col}"))