gabriel / musehub public
0035_schema_repair.py python
161 lines 10.1 KB
Raw
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