Skip to content

Commit debbd91

Browse files
authored
fix(migrations): repair mental_models.subtype at current head (#1553) (#1627)
Three production deployments (issue #1553, plus confirmations from @4Lienau and @khanhduyvt0101) report `column "subtype" of relation "mental_models" does not exist` on `create_mental_model`, despite their alembic_version showing the current head `m3rg3h3ad5f6`. Both h3c4d5e6f7g8_mental_models_v4 (which uses `CREATE TABLE IF NOT EXISTS` and is a no-op on databases that came through the reflections rename) and d5y6z7a8b9c0_backfill_mental_models_subtype were meant to ensure the column exists, but on these specific deployments neither fired successfully — likely a casualty of the divergent-heads reorganization that put d5y6z7a8b9c0 on a branch the affected DBs bypassed. Add a new migration at the current head so every stuck deployment picks it up on next container start. Idempotent (`ADD COLUMN IF NOT EXISTS`), guarded by an existence check on the table, and matches the canonical v4 column set and CHECK allowlist from d5y6z7a8b9c0. PG-only: Oracle's baseline creates mental_models with a different topology and constraint shape, so this repair does not apply there.
1 parent ed35894 commit debbd91

1 file changed

Lines changed: 117 additions & 0 deletions

File tree

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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

Comments
 (0)