|
| 1 | +"""Repair mental_models.subtype on databases stuck at m3rg3h3ad5f6 |
| 2 | +
|
| 3 | +Three production deployments reported `column "subtype" of relation |
| 4 | +"mental_models" does not exist` on `create_mental_model` even after their |
| 5 | +container reported `Database migrations completed successfully` and |
| 6 | +`alembic_version` advanced to `m3rg3h3ad5f6` (see issue #1553, #1553#1 |
| 7 | +confirmations from @4Lienau and @khanhduyvt0101). |
| 8 | +
|
| 9 | +Both `h3c4d5e6f7g8_mental_models_v4` and `d5y6z7a8b9c0_backfill_mental_models_subtype` |
| 10 | +were meant to ensure `subtype` exists, but on databases that came through the |
| 11 | +`reflections -> mental_models` rename chain *and* whose alembic_version |
| 12 | +advanced past `d5y6z7a8b9c0` along an alternate path during the divergent-heads |
| 13 | +reorganization, neither column-add actually fired. The result is a head-tagged |
| 14 | +database with a v3-shaped `mental_models` table missing six columns: |
| 15 | +``subtype``, ``description``, ``entity_id``, ``observations``, ``links``, |
| 16 | +``last_updated``. |
| 17 | +
|
| 18 | +This migration sits at the current head (`m3rg3h3ad5f6`) so every affected |
| 19 | +deployment will pick it up on next container start. It mirrors the column-add |
| 20 | +block from `d5y6z7a8b9c0_backfill_mental_models_subtype` using |
| 21 | +``ADD COLUMN IF NOT EXISTS`` so it is a no-op on databases where the columns |
| 22 | +are already present. |
| 23 | +
|
| 24 | +Revision ID: 86f7a033d372 |
| 25 | +Revises: m3rg3h3ad5f6 |
| 26 | +Create Date: 2026-05-14 |
| 27 | +""" |
| 28 | + |
| 29 | +from collections.abc import Sequence |
| 30 | + |
| 31 | +from alembic import context, op |
| 32 | + |
| 33 | +from hindsight_api.alembic._dialect import run_for_dialect |
| 34 | + |
| 35 | +revision: str = "86f7a033d372" |
| 36 | +down_revision: str | Sequence[str] | None = "m3rg3h3ad5f6" |
| 37 | +branch_labels: str | Sequence[str] | None = None |
| 38 | +depends_on: str | Sequence[str] | None = None |
| 39 | + |
| 40 | + |
| 41 | +def _pg_schema_prefix() -> str: |
| 42 | + """Schema-qualifier for raw SQL on PG (multi-tenant search_path).""" |
| 43 | + schema = context.config.get_main_option("target_schema") |
| 44 | + return f'"{schema}".' if schema else "" |
| 45 | + |
| 46 | + |
| 47 | +def _pg_upgrade() -> None: |
| 48 | + """Idempotently ensure mental_models has the v4 column set. |
| 49 | +
|
| 50 | + Safe to re-apply on databases that already received the columns via |
| 51 | + `h3c4d5e6f7g8_mental_models_v4` or `d5y6z7a8b9c0_backfill_mental_models_subtype` — |
| 52 | + every column-add uses ``IF NOT EXISTS`` and the constraint is recreated |
| 53 | + from scratch with the canonical v4 allowlist. |
| 54 | + """ |
| 55 | + schema = _pg_schema_prefix() |
| 56 | + bare_schema = schema.strip(".").strip('"') if schema else "" |
| 57 | + schema_clause = f"AND table_schema = '{bare_schema}'" if bare_schema else "" |
| 58 | + |
| 59 | + # Wrapped in a DO block so the existence check skips databases that |
| 60 | + # predate the reflections -> mental_models rename chain (no table to |
| 61 | + # repair). On those, every ALTER below would error. |
| 62 | + op.execute( |
| 63 | + f""" |
| 64 | + DO $$ |
| 65 | + BEGIN |
| 66 | + IF EXISTS ( |
| 67 | + SELECT 1 FROM information_schema.tables |
| 68 | + WHERE table_name = 'mental_models' |
| 69 | + {schema_clause} |
| 70 | + ) THEN |
| 71 | + -- Add the six v4 columns idempotently. |
| 72 | + ALTER TABLE {schema}mental_models |
| 73 | + ADD COLUMN IF NOT EXISTS subtype VARCHAR(32) NOT NULL DEFAULT 'structural'; |
| 74 | + ALTER TABLE {schema}mental_models |
| 75 | + ADD COLUMN IF NOT EXISTS description TEXT NOT NULL DEFAULT ''; |
| 76 | + ALTER TABLE {schema}mental_models |
| 77 | + ADD COLUMN IF NOT EXISTS entity_id UUID; |
| 78 | + ALTER TABLE {schema}mental_models |
| 79 | + ADD COLUMN IF NOT EXISTS observations JSONB DEFAULT '{{"observations": []}}'::jsonb; |
| 80 | + ALTER TABLE {schema}mental_models |
| 81 | + ADD COLUMN IF NOT EXISTS links VARCHAR[]; |
| 82 | + ALTER TABLE {schema}mental_models |
| 83 | + ADD COLUMN IF NOT EXISTS last_updated TIMESTAMP WITH TIME ZONE; |
| 84 | +
|
| 85 | + -- Recreate the CHECK constraint with the canonical v4 allowlist. |
| 86 | + -- Existing rows with subtype = 'directive' (possible on databases |
| 87 | + -- that ran the o0j1k2l3m4n5 directive-only path) are rewritten to |
| 88 | + -- 'structural' first so the constraint add succeeds. |
| 89 | + UPDATE {schema}mental_models SET subtype = 'structural' WHERE subtype = 'directive'; |
| 90 | +
|
| 91 | + ALTER TABLE {schema}mental_models DROP CONSTRAINT IF EXISTS ck_mental_models_subtype; |
| 92 | + ALTER TABLE {schema}mental_models |
| 93 | + ADD CONSTRAINT ck_mental_models_subtype |
| 94 | + CHECK (subtype IN ('structural', 'emergent', 'pinned', 'learned')); |
| 95 | +
|
| 96 | + CREATE INDEX IF NOT EXISTS idx_mental_models_subtype |
| 97 | + ON {schema}mental_models(bank_id, subtype); |
| 98 | + END IF; |
| 99 | + END$$; |
| 100 | + """ |
| 101 | + ) |
| 102 | + |
| 103 | + |
| 104 | +def _pg_downgrade() -> None: |
| 105 | + """No-op: dropping these columns would corrupt v4 application code.""" |
| 106 | + pass |
| 107 | + |
| 108 | + |
| 109 | +def upgrade() -> None: |
| 110 | + # PG-only: Oracle's baseline (o1a2b3c4d5e6) creates mental_models with its |
| 111 | + # own subtype shape (chk_mm_subtype IN ('directive', 'pinned')) and a |
| 112 | + # different table topology, so this PG-shaped repair does not apply. |
| 113 | + run_for_dialect(pg=_pg_upgrade) |
| 114 | + |
| 115 | + |
| 116 | +def downgrade() -> None: |
| 117 | + run_for_dialect(pg=_pg_downgrade) |
0 commit comments