Skip to content

Commit 0aacb68

Browse files
dmealingclaude
andauthored
docs(bugs): record #258 — migrate has no primary-key change kind (#261)
Adopting an existing Postgres database whose primary key differs from the metadata, the apply fails at the foreign keys with "there is no unique constraint matching given keys for referenced table". The table model reads a PK (TableDescriptor.primaryKey) but the Change union has no add-primary-key or drop-primary-key, so a key move degrades into an unrelated add-column plus drop-column and the constraint is simply lost. Only observable when adopting an existing database — a greenfield create-table carries its PK inline, which is why it had not surfaced before. Written up alongside the existing docs/bugs entries: root cause with the STAGE_ORDER excerpt, a suggested staging (drop-fk < drop-primary-key, add-column < add-primary-key < add-fk), the note that the SQLite emitter needs the same kinds, a synthetic two-table reproduction, and the alternative of detecting and refusing a PK change rather than emitting SQL that cannot apply. Found immediately behind #255, which 0.20.10 fixed — the apply now clears the column drops and fails at the FK stage instead. Claude-Session: https://claude.ai/code/session_01W6J7r8cEzA7ApfJD3CZt2L Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent c2dce37 commit 0aacb68

1 file changed

Lines changed: 126 additions & 0 deletions

File tree

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
---
2+
name: Bug report
3+
about: Report a defect
4+
title: "migrate: no primary-key change kind, so moving a table's PK leaves it with none and every referencing FK fails"
5+
labels: bug
6+
---
7+
8+
> **Filed as** https://github.com/metaobjectsdev/metaobjects/issues/258 (2026-08-02). Open. Follow-on from #255.
9+
10+
**Affected port(s):** TypeScript (diff + emit; shared migration engine, so all ports)
11+
**Package + version:** `@metaobjectsdev/cli` + `@metaobjectsdev/migrate-ts` 0.20.10
12+
13+
## What happened
14+
15+
Adopting an existing Postgres database whose primary key differs from the metadata,
16+
the apply fails once it reaches the foreign keys:
17+
18+
```
19+
meta: migrate: apply failed: there is no unique constraint matching given keys
20+
for referenced table "user_profiles"
21+
```
22+
23+
Concretely: the live table has `PRIMARY KEY (user_id)`. The metadata declares the
24+
entity's identity as an inherited `id` (`field.uuid` on an abstract base). The
25+
generated migration emits:
26+
27+
```sql
28+
ALTER TABLE "user_profiles" ADD COLUMN "id" UUID DEFAULT gen_random_uuid() NOT NULL; -- line 243
29+
ALTER TABLE "user_profiles" DROP COLUMN "user_id"; -- line 253
30+
CREATE UNIQUE INDEX "user_profiles_auth_user_id_unique" ON "user_profiles" ("auth_user_id");
31+
ALTER TABLE "agent_configs" ADD CONSTRAINTFOREIGN KEY ("created_by")
32+
REFERENCES "user_profiles" ("id"); -- ×49
33+
```
34+
35+
`DROP COLUMN "user_id"` takes the old primary key with it, and **nothing ever makes
36+
`id` the primary key** (or unique). The table is left with no PK, so all 49 FKs
37+
referencing `user_profiles(id)` are rejected.
38+
39+
The apply is transactional and rolls back cleanly — verified, the table still has
40+
`PRIMARY KEY (user_id)` and no `id` column afterwards.
41+
42+
## What you expected
43+
44+
The diff recognises that the table's primary key moved from `user_id` to `id`, and
45+
emits the key migration (`ADD PRIMARY KEY ("id")`, plus dropping the old one) after
46+
the column is added and before any FK references it.
47+
48+
## Root cause
49+
50+
The table model *knows* about primary keys —
51+
`server/typescript/packages/migrate-ts/src/types.ts:35`:
52+
53+
```ts
54+
primaryKey: string[]; // column names; [] if none
55+
```
56+
57+
— but there is **no primary-key change kind** in the emitted plan.
58+
`STAGE_ORDER` in `src/emit/postgres.ts` is an exhaustive
59+
`Record<Change["kind"], number>` and contains only:
60+
61+
```
62+
drop-view, drop-fk, drop-check, create-table, drop-index,
63+
add-column, drop-column, change-column-type, change-column-nullable,
64+
change-column-default, rename-column, rename-table, add-index,
65+
add-fk, add-check, drop-table, create-view, replace-view
66+
```
67+
68+
No `add-primary-key` / `drop-primary-key` / `change-primary-key`. So the diff can
69+
*read* a table's PK but cannot *express a change to it*: a PK move degrades silently
70+
into an unrelated add-column plus drop-column, and the constraint is simply lost.
71+
72+
This is only observable when adopting an existing database (`baseline --from-db`)
73+
whose PK disagrees with the metadata — a greenfield `create-table` carries its PK
74+
inline, which is why it has not shown up before.
75+
76+
## Suggested fix
77+
78+
Add primary-key change kinds to the `Change` union and to both emitters, staged so
79+
the key exists before anything references it and after the column exists:
80+
81+
```ts
82+
"drop-fk": 1, "drop-check": 1,
83+
"drop-primary-key": 1.6, // NEW: after drop-fk, before column mutation
84+
"add-column": 2, "drop-column": 2,
85+
86+
"add-primary-key": 4.5, // NEW: after columns/indexes, before add-fk
87+
"add-fk": 5,
88+
```
89+
90+
The invariant is `drop-fk < drop-primary-key`, `add-column < add-primary-key < add-fk`.
91+
`src/emit/sqlite.ts` needs the same kinds (SQLite requires the recreate-and-copy path
92+
for a PK change).
93+
94+
If a PK change is considered too dangerous to automate, the alternative is to
95+
**detect and refuse** it — fail the diff with a clear "primary key differs; not
96+
migratable" message rather than emitting SQL that cannot apply.
97+
98+
## Reproduction
99+
100+
```
101+
-- live DB
102+
CREATE TABLE parent (legacy_id UUID PRIMARY KEY, name TEXT);
103+
CREATE TABLE child (id UUID PRIMARY KEY, parent_ref UUID);
104+
```
105+
106+
Metadata declares `Parent` with identity `id` (not `legacy_id`) and a
107+
`identity.reference` from `Child.parentRef``Parent`.
108+
109+
```
110+
meta migrate baseline --from-db --db postgresql://… --dialect postgres
111+
meta migrate --db postgresql://… --dialect postgres --slug repro \
112+
--allow drop-column,drop-fk --apply
113+
```
114+
115+
`there is no unique constraint matching given keys for referenced table "parent"`.
116+
117+
## Environment
118+
119+
Linux, Node 24, Postgres 16, pnpm. Reproduced against a restored copy of a real
120+
database (~198 pending changes), not a synthetic fixture.
121+
122+
## Context
123+
124+
Found immediately behind #255 (statement ordering), which is fixed in 0.20.10 — the
125+
apply now gets past the column drops and fails at the FK stage instead. This is the
126+
"second failure class" that issue's closing note predicted.

0 commit comments

Comments
 (0)