0035_schema_repair.py
python
sha256:94ef169c149a452bff7c604ded8b280b19bd477c2dabcb56972780b0b784c7aa
Merge 'fix/assignee-sigil-inline' into 'dev' — proposal: As…
Human
1 day ago
| 1 | """Schema repair migration — idempotently add all columns/tables that may be missing. |
| 2 | |
| 3 | Staging's local postgres was stamped to 0034 via the deploy script's escape hatch |
| 4 | before migrations 0019–0034 actually ran. This migration re-applies every |
| 5 | structural change from that range using ADD COLUMN IF NOT EXISTS and CREATE TABLE |
| 6 | IF NOT EXISTS, so it is safe to run on both fresh instances and partially-migrated |
| 7 | ones. |
| 8 | |
| 9 | Revision ID: 0035 |
| 10 | Revises: 0034 |
| 11 | """ |
| 12 | from __future__ import annotations |
| 13 | |
| 14 | from alembic import op |
| 15 | import sqlalchemy as sa |
| 16 | from sqlalchemy import text |
| 17 | from sqlalchemy.dialects.postgresql import JSONB, ARRAY |
| 18 | |
| 19 | revision = "0035" |
| 20 | down_revision = "0034" |
| 21 | branch_labels = None |
| 22 | depends_on = None |
| 23 | |
| 24 | |
| 25 | def _has_column(conn, table: str, column: str) -> bool: |
| 26 | result = conn.execute(text( |
| 27 | "SELECT 1 FROM information_schema.columns " |
| 28 | "WHERE table_schema='public' AND table_name=:t AND column_name=:c" |
| 29 | ), {"t": table, "c": column}) |
| 30 | return result.fetchone() is not None |
| 31 | |
| 32 | |
| 33 | def _has_table(conn, table: str) -> bool: |
| 34 | result = conn.execute(text( |
| 35 | "SELECT 1 FROM information_schema.tables " |
| 36 | "WHERE table_schema='public' AND table_name=:t" |
| 37 | ), {"t": table}) |
| 38 | return result.fetchone() is not None |
| 39 | |
| 40 | |
| 41 | def upgrade() -> None: |
| 42 | conn = op.get_bind() |
| 43 | |
| 44 | # ── musehub_commits ──────────────────────────────────────────────────────── |
| 45 | # From migration 0019: agent_id, model_id, commit_branch |
| 46 | if not _has_column(conn, "musehub_commits", "agent_id"): |
| 47 | op.add_column("musehub_commits", sa.Column("agent_id", sa.String(255), nullable=True, server_default="")) |
| 48 | if not _has_column(conn, "musehub_commits", "model_id"): |
| 49 | op.add_column("musehub_commits", sa.Column("model_id", sa.String(255), nullable=True, server_default="")) |
| 50 | if not _has_column(conn, "musehub_commits", "commit_branch"): |
| 51 | op.add_column("musehub_commits", sa.Column("commit_branch", sa.String(255), nullable=True)) |
| 52 | |
| 53 | # From migration 0020: signature, signer_public_key, signer_key_id, toolchain_id, |
| 54 | # sem_ver_bump, breaking_changes, reviewed_by, test_runs, prompt_hash |
| 55 | if not _has_column(conn, "musehub_commits", "signature"): |
| 56 | op.add_column("musehub_commits", sa.Column("signature", sa.Text(), nullable=True, server_default="")) |
| 57 | if not _has_column(conn, "musehub_commits", "signer_public_key"): |
| 58 | op.add_column("musehub_commits", sa.Column("signer_public_key", sa.Text(), nullable=True, server_default="")) |
| 59 | if not _has_column(conn, "musehub_commits", "signer_key_id"): |
| 60 | op.add_column("musehub_commits", sa.Column("signer_key_id", sa.String(255), nullable=True, server_default="")) |
| 61 | if not _has_column(conn, "musehub_commits", "toolchain_id"): |
| 62 | op.add_column("musehub_commits", sa.Column("toolchain_id", sa.String(255), nullable=True, server_default="")) |
| 63 | if not _has_column(conn, "musehub_commits", "sem_ver_bump"): |
| 64 | op.add_column("musehub_commits", sa.Column("sem_ver_bump", sa.String(10), nullable=True, server_default="none")) |
| 65 | if not _has_column(conn, "musehub_commits", "breaking_changes"): |
| 66 | op.add_column("musehub_commits", sa.Column("breaking_changes", ARRAY(sa.Text()), nullable=True)) |
| 67 | if not _has_column(conn, "musehub_commits", "reviewed_by"): |
| 68 | op.add_column("musehub_commits", sa.Column("reviewed_by", ARRAY(sa.Text()), nullable=True)) |
| 69 | if not _has_column(conn, "musehub_commits", "test_runs"): |
| 70 | op.add_column("musehub_commits", sa.Column("test_runs", sa.Integer(), nullable=True, server_default="0")) |
| 71 | if not _has_column(conn, "musehub_commits", "prompt_hash"): |
| 72 | op.add_column("musehub_commits", sa.Column("prompt_hash", sa.String(255), nullable=True, server_default="")) |
| 73 | |
| 74 | # From migration 0021: structured_delta |
| 75 | if not _has_column(conn, "musehub_commits", "structured_delta"): |
| 76 | op.add_column("musehub_commits", sa.Column("structured_delta", JSONB(), nullable=True)) |
| 77 | |
| 78 | # Drop legacy commit_meta if still present (was dropped in 0020) |
| 79 | if _has_column(conn, "musehub_commits", "commit_meta"): |
| 80 | op.drop_column("musehub_commits", "commit_meta") |
| 81 | |
| 82 | # ── musehub_symbol_history_entries ───────────────────────────────────────── |
| 83 | # From migration 0018: op_payload |
| 84 | if not _has_column(conn, "musehub_symbol_history_entries", "op_payload"): |
| 85 | op.add_column("musehub_symbol_history_entries", sa.Column("op_payload", JSONB(), nullable=True)) |
| 86 | # From migration 0031: message, commit_branch |
| 87 | if not _has_column(conn, "musehub_symbol_history_entries", "message"): |
| 88 | op.add_column("musehub_symbol_history_entries", sa.Column("message", sa.Text(), nullable=True)) |
| 89 | if not _has_column(conn, "musehub_symbol_history_entries", "commit_branch"): |
| 90 | op.add_column("musehub_symbol_history_entries", sa.Column("commit_branch", sa.String(512), nullable=True)) |
| 91 | |
| 92 | # ── musehub_symbol_vitals ────────────────────────────────────────────────── |
| 93 | # From migration 0031 (re-created with new schema) |
| 94 | if not _has_table(conn, "musehub_symbol_vitals"): |
| 95 | op.create_table( |
| 96 | "musehub_symbol_vitals", |
| 97 | sa.Column("repo_id", sa.String(128), sa.ForeignKey("musehub_repos.repo_id", ondelete="CASCADE"), primary_key=True, nullable=False), |
| 98 | sa.Column("address", sa.String(512), primary_key=True, nullable=False), |
| 99 | sa.Column("first_introduced", sa.DateTime(timezone=True), nullable=True), |
| 100 | sa.Column("change_count", sa.Integer, nullable=False, server_default="0"), |
| 101 | sa.Column("version_count", sa.Integer, nullable=False, server_default="0"), |
| 102 | sa.Column("op_add", sa.Integer, nullable=False, server_default="0"), |
| 103 | sa.Column("op_modify", sa.Integer, nullable=False, server_default="0"), |
| 104 | sa.Column("op_delete", sa.Integer, nullable=False, server_default="0"), |
| 105 | sa.Column("op_move", sa.Integer, nullable=False, server_default="0"), |
| 106 | ) |
| 107 | op.create_index("ix_symbol_vitals_repo", "musehub_symbol_vitals", ["repo_id"]) |
| 108 | # From migration 0032: coupling_count |
| 109 | if not _has_column(conn, "musehub_symbol_vitals", "coupling_count"): |
| 110 | op.add_column("musehub_symbol_vitals", sa.Column("coupling_count", sa.Integer, nullable=False, server_default="0")) |
| 111 | |
| 112 | # ── musehub_symbol_coupling ──────────────────────────────────────────────── |
| 113 | if not _has_table(conn, "musehub_symbol_coupling"): |
| 114 | op.create_table( |
| 115 | "musehub_symbol_coupling", |
| 116 | sa.Column("repo_id", sa.String(128), sa.ForeignKey("musehub_repos.repo_id", ondelete="CASCADE"), primary_key=True, nullable=False), |
| 117 | sa.Column("address", sa.String(512), primary_key=True, nullable=False), |
| 118 | sa.Column("co_address", sa.String(512), primary_key=True, nullable=False), |
| 119 | sa.Column("shared_commits", sa.Integer, nullable=False, server_default="0"), |
| 120 | ) |
| 121 | op.create_index("ix_symbol_coupling_repo_address", "musehub_symbol_coupling", ["repo_id", "address"]) |
| 122 | |
| 123 | # ── musehub_coord_records ────────────────────────────────────────────────── |
| 124 | # From migration 0027: rename record_uuid → record_id |
| 125 | if _has_column(conn, "musehub_coord_records", "record_uuid") and not _has_column(conn, "musehub_coord_records", "record_id"): |
| 126 | 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) |
| 127 | |
| 128 | # ── musehub_file_last_commits ────────────────────────────────────────────── |
| 129 | # From migration 0034 (may not have run) |
| 130 | if not _has_table(conn, "musehub_file_last_commits"): |
| 131 | op.create_table( |
| 132 | "musehub_file_last_commits", |
| 133 | sa.Column("repo_id", sa.String(128), sa.ForeignKey("musehub_repos.repo_id", ondelete="CASCADE"), primary_key=True), |
| 134 | sa.Column("branch", sa.String(255), primary_key=True), |
| 135 | sa.Column("path", sa.Text, primary_key=True), |
| 136 | sa.Column("commit_id", sa.String(128), nullable=False), |
| 137 | sa.Column("commit_message", sa.Text, nullable=False, server_default=""), |
| 138 | sa.Column("commit_author", sa.String(255), nullable=False, server_default=""), |
| 139 | sa.Column("commit_timestamp", sa.DateTime(timezone=True), nullable=False), |
| 140 | sa.Column("agent_id", sa.String(128), nullable=True), |
| 141 | sa.Column("model_id", sa.String(128), nullable=True), |
| 142 | ) |
| 143 | op.create_index("ix_file_last_commits_repo_branch", "musehub_file_last_commits", ["repo_id", "branch"], if_not_exists=True) |
| 144 | |
| 145 | |
| 146 | def downgrade() -> None: |
| 147 | from sqlalchemy import text |
| 148 | # Drop tables this migration conditionally created (IF EXISTS — idempotent with owning migrations). |
| 149 | op.execute(text("DROP INDEX IF EXISTS ix_symbol_coupling_repo_address")) |
| 150 | op.execute(text("DROP TABLE IF EXISTS musehub_symbol_coupling")) |
| 151 | op.execute(text("DROP INDEX IF EXISTS ix_symbol_vitals_repo")) |
| 152 | op.execute(text("DROP TABLE IF EXISTS musehub_symbol_vitals")) |
| 153 | op.execute(text("DROP INDEX IF EXISTS ix_file_last_commits_repo_branch")) |
| 154 | op.execute(text("DROP TABLE IF EXISTS musehub_file_last_commits")) |
| 155 | # Undo column repairs — all IF EXISTS so idempotent with owning migrations. |
| 156 | for col in ("structured_delta", "prompt_hash", "test_runs", "reviewed_by", |
| 157 | "breaking_changes", "sem_ver_bump", "toolchain_id", "signer_key_id", |
| 158 | "signer_public_key", "signature", "commit_branch", "model_id", "agent_id"): |
| 159 | op.execute(text(f"ALTER TABLE musehub_commits DROP COLUMN IF EXISTS {col}")) |
| 160 | for col in ("commit_branch", "message", "op_payload"): |
| 161 | op.execute(text(f"ALTER TABLE musehub_symbol_history_entries DROP COLUMN IF EXISTS {col}")) |
File History
3 commits
sha256:94ef169c149a452bff7c604ded8b280b19bd477c2dabcb56972780b0b784c7aa
Merge 'fix/assignee-sigil-inline' into 'dev' — proposal: As…
Human
1 day ago
sha256:6b1949fc2797ca4c1936a637a4cbfec828ef56cf52398a2e74ca3c4f494e728f
fix: use wire_bytes not mpack_bytes_raw in compute_object_b…
Sonnet 4.6
patch
10 days ago
sha256:4aed3d8601c8dd3ed37074de35f11f4a9699a0a4b99d43727048fd3f8e6fd13d
chore: doc sweep, ignore wrangler build state, misc fixes
Sonnet 4.6
minor
⚠
12 days ago