From 80015852b7e6ea695f42434ec7d0a649fc2ced6d Mon Sep 17 00:00:00 2001 From: Doug Mealing Date: Sun, 2 Aug 2026 09:36:44 -0400 Subject: [PATCH] =?UTF-8?q?docs(bugs):=20record=20#258=20=E2=80=94=20migra?= =?UTF-8?q?te=20has=20no=20primary-key=20change=20kind?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01W6J7r8cEzA7ApfJD3CZt2L --- .../2026-08-02-no-primary-key-change-kind.md | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 docs/bugs/2026-08-02-no-primary-key-change-kind.md diff --git a/docs/bugs/2026-08-02-no-primary-key-change-kind.md b/docs/bugs/2026-08-02-no-primary-key-change-kind.md new file mode 100644 index 000000000..a94b320d5 --- /dev/null +++ b/docs/bugs/2026-08-02-no-primary-key-change-kind.md @@ -0,0 +1,126 @@ +--- +name: Bug report +about: Report a defect +title: "migrate: no primary-key change kind, so moving a table's PK leaves it with none and every referencing FK fails" +labels: bug +--- + +> **Filed as** https://github.com/metaobjectsdev/metaobjects/issues/258 (2026-08-02). Open. Follow-on from #255. + +**Affected port(s):** TypeScript (diff + emit; shared migration engine, so all ports) +**Package + version:** `@metaobjectsdev/cli` + `@metaobjectsdev/migrate-ts` 0.20.10 + +## What happened + +Adopting an existing Postgres database whose primary key differs from the metadata, +the apply fails once it reaches the foreign keys: + +``` +meta: migrate: apply failed: there is no unique constraint matching given keys + for referenced table "user_profiles" +``` + +Concretely: the live table has `PRIMARY KEY (user_id)`. The metadata declares the +entity's identity as an inherited `id` (`field.uuid` on an abstract base). The +generated migration emits: + +```sql +ALTER TABLE "user_profiles" ADD COLUMN "id" UUID DEFAULT gen_random_uuid() NOT NULL; -- line 243 +ALTER TABLE "user_profiles" DROP COLUMN "user_id"; -- line 253 +CREATE UNIQUE INDEX "user_profiles_auth_user_id_unique" ON "user_profiles" ("auth_user_id"); +ALTER TABLE "agent_configs" ADD CONSTRAINT … FOREIGN KEY ("created_by") + REFERENCES "user_profiles" ("id"); -- ×49 +``` + +`DROP COLUMN "user_id"` takes the old primary key with it, and **nothing ever makes +`id` the primary key** (or unique). The table is left with no PK, so all 49 FKs +referencing `user_profiles(id)` are rejected. + +The apply is transactional and rolls back cleanly — verified, the table still has +`PRIMARY KEY (user_id)` and no `id` column afterwards. + +## What you expected + +The diff recognises that the table's primary key moved from `user_id` to `id`, and +emits the key migration (`ADD PRIMARY KEY ("id")`, plus dropping the old one) after +the column is added and before any FK references it. + +## Root cause + +The table model *knows* about primary keys — +`server/typescript/packages/migrate-ts/src/types.ts:35`: + +```ts + primaryKey: string[]; // column names; [] if none +``` + +— but there is **no primary-key change kind** in the emitted plan. +`STAGE_ORDER` in `src/emit/postgres.ts` is an exhaustive +`Record` and contains only: + +``` +drop-view, drop-fk, drop-check, create-table, drop-index, +add-column, drop-column, change-column-type, change-column-nullable, +change-column-default, rename-column, rename-table, add-index, +add-fk, add-check, drop-table, create-view, replace-view +``` + +No `add-primary-key` / `drop-primary-key` / `change-primary-key`. So the diff can +*read* a table's PK but cannot *express a change to it*: a PK move degrades silently +into an unrelated add-column plus drop-column, and the constraint is simply lost. + +This is only observable when adopting an existing database (`baseline --from-db`) +whose PK disagrees with the metadata — a greenfield `create-table` carries its PK +inline, which is why it has not shown up before. + +## Suggested fix + +Add primary-key change kinds to the `Change` union and to both emitters, staged so +the key exists before anything references it and after the column exists: + +```ts + "drop-fk": 1, "drop-check": 1, + "drop-primary-key": 1.6, // NEW: after drop-fk, before column mutation + "add-column": 2, "drop-column": 2, + … + "add-primary-key": 4.5, // NEW: after columns/indexes, before add-fk + "add-fk": 5, +``` + +The invariant is `drop-fk < drop-primary-key`, `add-column < add-primary-key < add-fk`. +`src/emit/sqlite.ts` needs the same kinds (SQLite requires the recreate-and-copy path +for a PK change). + +If a PK change is considered too dangerous to automate, the alternative is to +**detect and refuse** it — fail the diff with a clear "primary key differs; not +migratable" message rather than emitting SQL that cannot apply. + +## Reproduction + +``` +-- live DB +CREATE TABLE parent (legacy_id UUID PRIMARY KEY, name TEXT); +CREATE TABLE child (id UUID PRIMARY KEY, parent_ref UUID); +``` + +Metadata declares `Parent` with identity `id` (not `legacy_id`) and a +`identity.reference` from `Child.parentRef` → `Parent`. + +``` +meta migrate baseline --from-db --db postgresql://… --dialect postgres +meta migrate --db postgresql://… --dialect postgres --slug repro \ + --allow drop-column,drop-fk --apply +``` + +→ `there is no unique constraint matching given keys for referenced table "parent"`. + +## Environment + +Linux, Node 24, Postgres 16, pnpm. Reproduced against a restored copy of a real +database (~198 pending changes), not a synthetic fixture. + +## Context + +Found immediately behind #255 (statement ordering), which is fixed in 0.20.10 — the +apply now gets past the column drops and fails at the FK stage instead. This is the +"second failure class" that issue's closing note predicted.