Skip to content

fix(migrate-ts): stop meta migrate dropping a legacy Postgres serial PK's default - #279

Merged
dmealing merged 2 commits into
mainfrom
fix/migrate-serial-identity-adoption
Aug 8, 2026
Merged

fix(migrate-ts): stop meta migrate dropping a legacy Postgres serial PK's default#279
dmealing merged 2 commits into
mainfrom
fix/migrate-serial-identity-adoption

Conversation

@dmealing

@dmealing dmealing commented Aug 8, 2026

Copy link
Copy Markdown
Member

Adopting metadata onto an existing Postgres table whose PK was created as legacy serial made meta migrate --from-db propose:

ALTER TABLE "work_item" ALTER COLUMN "id" DROP DEFAULT;

with no replacement generation mechanism. Applying it leaves id NOT NULL with nothing to populate it, so every insert that doesn't supply id starts failing — against a live table, on the first thing a new adopter does. serial PKs are what Drizzle, Prisma, Rails and SQLAlchemy all produce, so this is the most common pre-adoption shape.

It also violated the repo's own adoption doctrine — "adopting onto existing code? Metadata FOLLOWS the code" — by modernizing serialIDENTITY as a side effect of adoption.

Root cause

Introspection is correct: it detects nextval(...) and sets identity = "increment", matching the expected side, so the identity dimension itself doesn't diff. The bug is the separate column-default comparison, guarded only against uuid on the strength of a comment claiming "an AUTOINCREMENT column has no DEFAULT" — true for SQLite and modern GENERATED ... AS IDENTITY, false for Postgres serial, which is sugar for integer + sequence + a real DEFAULT nextval(...).

The fix

An increment PK skips the default-diff only when the live default is that exact auto-sequence shape. A genuinely wrong, non-sequence default on an increment PK still reports as drift (two regression tests assert the change payload, not just a count). Detection lives in one shared isPgAutoSequenceDefault helper used by both introspection and the diff, so the rule can't drift into two spellings. The false comment is corrected.

No serialIDENTITY modernization is introduced — if ever wanted it should be opt-in, never a side effect of adoption.

Verification

Ran against a real Postgres (ephemeral container), not a skipped integration test: live serial PK introspects with a genuine nextval(...) → diff emits no DROP DEFAULT → apply → re-introspect → re-diff empty → an insert omitting id succeeds and auto-populates. The last two assertions are what prove the bug fixed rather than silenced.

Unit suite 401/401 · package 688 pass / 19 skip / 0 fail · reviewed, one fix round, re-reviewed clean.

npm-only — schema is TypeScript-owned (ADR-0015).

Known residual (not addressed here)

The guard keys on the expected side, and identity is only set when @generation is explicit — so an adopter declaring identity.primary without @generation: increment still hits the destructive path. Widening to key on the actual side would swallow a deliberate @generation removal, so it needs a design ruling rather than a blind patch.

dmealing and others added 2 commits August 8, 2026 17:55
…PK's default

Adopting MetaObjects metadata onto an existing Postgres table whose PK was
created as `serial` (Drizzle's serial().primaryKey(), Prisma's autoincrement(),
Rails, SQLAlchemy, or plain `id SERIAL PRIMARY KEY`) made `meta migrate
--from-db` propose `ALTER COLUMN "id" DROP DEFAULT` with no replacement
generation mechanism -- destructive against a live table, since every insert
that doesn't supply id explicitly then starts failing.

The default-diff guard at diff/index.ts skipped identity/default comparison
only for `identity: "uuid"`, on the strength of a comment claiming an
increment column never carries a DEFAULT. True for SQLite AUTOINCREMENT and
modern GENERATED ... AS IDENTITY; false for legacy Postgres `serial`, which is
sugar for integer + sequence + a genuine `DEFAULT nextval(...)` clause.

The guard now also skips the default-diff for `identity: "increment"` when
the live default matches that exact nextval(...) shape -- narrowly, via a
predicate (isPgAutoSequenceDefault) shared with the introspector that already
recognized it, so a genuinely wrong non-sequence default on an increment PK
still reports as drift. Rewrote the false comment.

Reported against an adopting project.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TqsuDye2SfXGf43vuoD3n
… arg

Three one-line fixes from review of the serial-identity default-diff fix:

- diff/index.ts: the replacement comment inverted its own causal clause
  ("Left undiffed, that surfaced as ... DROP DEFAULT" claims skipping the
  diff produced the bug -- the exact inverse; the bug was the case being
  left DIFFED). Fixed to "Left diffed".
- diff-uuid-identity-default.test.ts: the increment-PK regression-guard test
  name asserted "it never had a DEFAULT", the same false doctrine this
  change disproved. Renamed to "(no live default at all)".
- diff-serial-identity-default.test.ts: the new unit tests called diff()
  with no `dialect` for a Postgres-only bug. This repo's standing rule is
  that omitting `dialect` runs a different pipeline -- pass it always. Both
  diff() call sites now pass `dialect: "postgres"`, matching the
  integration test.

No production behavior change beyond the comment; the two test edits are
name/argument-only and do not change what either test asserts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TqsuDye2SfXGf43vuoD3n
@dmealing
dmealing merged commit 1bd4d5c into main Aug 8, 2026
1 check passed
@dmealing
dmealing deleted the fix/migrate-serial-identity-adoption branch August 8, 2026 22:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant