Persist per-migration contract snapshots into the ledger#908
Conversation
The prisma_contract.ledger DDL has always carried contract_json_before / contract_json_after columns, but the write path never populated them. Thread each migration bundle's on-disk start-contract.json / end-contract.json snapshots through the aggregate planner edge refs and the Postgres runner into the ledger insert, so database tooling (e.g. Prisma Studio) can render model-level diffs per applied migration. - MigrationPackage gains optional contractSnapshots, read by the on-disk loaders; missing or malformed snapshot files stay non-fatal (ADR 197 / ADR 199: snapshots are author-time conveniences, never runner inputs). - AggregateMigrationEdgeRef gains contractJsonBefore/After; graph-walk fills them from bundle snapshots, synth fills the destination side from the member contract. - SqlControlAdapter.writeLedgerEntry accepts the snapshots and the Postgres adapter inserts them; absent snapshots write NULL, matching prior rows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Søren Bramer Schmidt <sorenbs@gmail.com>
Two changes that together produce a rich, replayable 20-migration history for migration-aware tooling: - Fix contract canonicalization dropping literal false / empty-array column default values. isDefaultValue() treats false and [] as omittable shape defaults, but a default literal payload is data — dropping it produced a contract.json that failed its own structural validation on the next read (surfaced as PN-CLI-4003 when planning a migration for a `Boolean @default(false)` column). - Add examples/migrations-showcase: 20 cumulative schema snapshots of a project tracker (models, enums, relations, indexes, uniques, plus a destructive column drop and a retired model) and a generate script that runs the real emit → migration plan → migrate flow per step against a Postgres database, then captures prisma_contract.marker and prisma_contract.ledger (including contract_json_before/after) into fixture/prisma-contract.json for downstream demos. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Søren Bramer Schmidt <sorenbs@gmail.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (190)
📝 WalkthroughWalkthroughAdds a new "migrations-showcase" example with a README and ten sequential generated Prisma Next Postgres migrations (init_users through add_milestones). Each migration folder contains generated start/end contract JSON and TypeScript declaration files, migration metadata, a migration.ts script, and an ops.json operation manifest. ChangesMigrations Showcase Example
Estimated code review effort: 2 (Simple) | ~15 minutes Sequence Diagram(s)sequenceDiagram
participant Generator as Migration Generator
participant MigrationCLI
participant M as Migration Class
participant DB as PostgreSQL
Generator->>MigrationCLI: run(import.meta.url, M) per step
MigrationCLI->>M: load start/end contract JSON
M->>DB: precheck (object absent)
M->>DB: execute DDL (CREATE TABLE / ADD COLUMN / ADD CONSTRAINT)
M->>DB: postcheck (object present)
DB-->>Generator: contract ledger row captured
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
@prisma-next/extension-author-tools
@prisma-next/mongo-runtime
@prisma-next/family-mongo
@prisma-next/sql-runtime
@prisma-next/family-sql
@prisma-next/extension-arktype-json
@prisma-next/middleware-cache
@prisma-next/mongo
@prisma-next/extension-paradedb
@prisma-next/extension-pgvector
@prisma-next/extension-postgis
@prisma-next/postgres
@prisma-next/sql-orm-client
@prisma-next/sqlite
@prisma-next/extension-supabase
@prisma-next/target-mongo
@prisma-next/adapter-mongo
@prisma-next/driver-mongo
@prisma-next/contract
@prisma-next/utils
@prisma-next/config
@prisma-next/errors
@prisma-next/framework-components
@prisma-next/operations
@prisma-next/ts-render
@prisma-next/contract-authoring
@prisma-next/ids
@prisma-next/psl-parser
@prisma-next/psl-printer
@prisma-next/cli
@prisma-next/cli-telemetry
@prisma-next/config-loader
@prisma-next/emitter
@prisma-next/language-server
@prisma-next/migration-tools
prisma-next
@prisma-next/vite-plugin-contract-emit
@prisma-next/mongo-codec
@prisma-next/mongo-contract
@prisma-next/mongo-value
@prisma-next/mongo-contract-psl
@prisma-next/mongo-contract-ts
@prisma-next/mongo-emitter
@prisma-next/mongo-schema-ir
@prisma-next/mongo-query-ast
@prisma-next/mongo-orm
@prisma-next/mongo-query-builder
@prisma-next/mongo-lowering
@prisma-next/mongo-wire
@prisma-next/sql-contract
@prisma-next/sql-errors
@prisma-next/sql-operations
@prisma-next/sql-schema-ir
@prisma-next/sql-contract-psl
@prisma-next/sql-contract-ts
@prisma-next/sql-contract-emitter
@prisma-next/sql-lane-query-builder
@prisma-next/sql-relational-core
@prisma-next/sql-builder
@prisma-next/target-postgres
@prisma-next/target-sqlite
@prisma-next/adapter-postgres
@prisma-next/adapter-sqlite
@prisma-next/driver-postgres
@prisma-next/driver-sqlite
commit: |
size-limit report 📦
|
Linked issue
n/a — built to power the Prisma Studio Migrations view; companion PR: prisma/studio#1533.
At a glance
The
contract_json_before/contract_json_aftercolumns have existed in the ledger bootstrap DDL since day one, but the write path never populated them — every row carriedNULLs where the schema history should be.Summary
This PR ships three things:
start-contract.json/end-contract.json(already author-time artifacts per ADR 197) is threaded through the aggregate planner's edge refs and the Postgres runner into the ledger insert, so database tooling can render model-level diffs per applied migration without touching the repo.false/[]column defaults.canonicalizeContracttreatedfalseand empty arrays as omittable shape-defaults and dropped them fromdefault.value, producing acontract.jsonthat failed its own structural validation on the next read (PN-CLI-4003on anyBoolean @default(false)schema).examples/migrations-showcase— 20 cumulative schema snapshots of a project tracker driven through the realcontract emit → migration plan → migrateflow, with the resulting marker + ledger captured into a replayable fixture. This is both the regression playground for (1) and the seed source for the Studio demo.Notes for the reviewer
examples/migrations-showcase/migrations/(20 attested bundles: manifest, ops, contract snapshots each). The hand-written surface is the example'ssteps/schemas,scripts/generate.ts, and the framework changes.default.valueto the preserved set, which changes storage hashes only for contracts that carry literalfalse/[]defaults — and those contracts were previously unloadable (they failed validation on read), so no working contract's hash changes.--no-verify) for these commits: the machine's Node 23.7 is rejected by dependency-cruiser's engines check (repo wants ≥24), which failslint:depsand the husky pre-commit for purely environmental reasons. CI runs the real checks.MigrationPackage.contractSnapshots,AggregateMigrationEdgeRef.contractJsonBefore/After, and thewriteLedgerEntryentry fields. The SQLite and Mongo runners compile untouched and keep writing rows exactly as before; the ledger DDL is unchanged (the columns already existed). Snapshots remain non-structural: a package holding onlymigration.json+ops.jsonstill loads and applies (ADR 199), and missing or malformed snapshot files are treated as absent rather than fatal.How it fits together
readMigrationPackage/readMigrationsDirpick up the optionalstart-contract.json/end-contract.jsonnext to each manifest intoMigrationPackage.contractSnapshots(packages/1-framework/3-tooling/migration/src/io.ts). Missing files →nullside; malformed JSON → treated as absent.AggregateMigrationEdgeRef(graph-walk.ts); the synth strategy (used bydb init/db update) fills the destination side from the member contract (synth.ts) — its origin is a live-schema diff with no prior contract, sobeforestaysNULLthere.writeLedgerEntry(runner.ts), and the adapter inserts them into the existingcontract_json_before/contract_json_aftercolumns (control-adapter.ts).examples/migrations-showcaseruns the whole pipeline 20 times against a real Postgres — additive steps, enum introductions, uniques/indexes, a destructive column drop, and a retired model — and captures the resulting ledger (snapshots included) intofixture/prisma-contract.json.Behavior changes & evidence
migratewrites contract snapshots into every ledger row it records; absent snapshots still writeNULL, matching prior rows. Implementation: runner.ts, control-adapter.ts, marker-ledger.ts. Evidence: runner.ledger.integration.test.ts (new snapshot-persistence case), marker-ledger-writes.test.ts (pinned INSERT shape + jsonb params).Boolean @default(false)(and empty-array-default) schemas now emit contracts that validate. Implementation: canonicalization.ts. Evidence: canonicalization.test.ts (false +[]literal defaults preserved).pnpm --filter migrations-showcase generate, acceptsDATABASE_URL).Testing performed
pnpm build(Turbo, all 68 tasks green)removed-verb-redirects500ms spawn timeout, reproduced on cleanorigin/main)pnpm test:packagesfull sweep; failures triaged as pre-existing/environmental (Node 23.7 vsengines >=24; mongo-family typecheck noise on clean main)examples/migrations-showcasegenerate run against Postgres — 20 bundles planned, applied, and captured with populatedcontract_json_before/afteron every rowAlternatives considered
operationsSQL instead of storing snapshots — rejected: parsing DDL back into a model is lossy and target-specific, and the contract IR is the canonical model-level representation the planner already produces for free.NULLrows.)shouldPreserveEmptyhook instead of the core omit rule — rejected: a default literal's payload is data in every family, not a SQL-specific shape preference; the core rule is the single right place.Skill update
n/a — no CLI commands/flags, config fields, or error codes changed; the new fields are optional additions to internal control-plane types. The ledger columns were already part of the bootstrap DDL.
Checklist
git commit -s) per the DCO. The DCO status check will block merge if any commit is missing aSigned-off-by:trailer.n/aif the change is doc-only / refactor with no behavioural delta).TML-NNNN: <sentence-case title>form (Linear ticket prefix + concise title naming the concrete deliverable). See.claude/skills/create-pr/SKILL.mdfor the full convention.n/a — internal only).🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation