Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
a930072
fix(cli): contract references list only the forms they accept; migrat…
wmadden-electric Sep 28, 2026
20615a9
fix(cli): db migrate --to resolves @contract and @db like --show does
wmadden-electric Sep 28, 2026
a08c553
Merge remote-tracking branch 'origin/main' into gagarin/fix-cli-contr…
wmadden-electric Oct 5, 2026
b2eb5aa
fix(cli): db migrate --to @db and @empty leave an unmarked database a…
wmadden-electric Oct 5, 2026
2c0f06f
fix(cli): migration status stays quiet for an all-external extension …
wmadden-electric Oct 5, 2026
e7043a1
fix(cli): migration status applies --to and --from to the app space only
wmadden-electric Oct 5, 2026
3a1736a
fix(cli): db migrate keeps each space that needs no change away from …
wmadden-electric Oct 5, 2026
f3dea78
fix(cli): one source for reserved references and accepted forms; db s…
wmadden-electric Oct 6, 2026
e831fbe
fix(cli): the reserved-reference refusal takes its form list from the…
wmadden-electric Oct 6, 2026
4b90deb
test(integration): migration status warns when db update leaves the m…
wmadden-electric Oct 6, 2026
19561c0
fix(cli): db migrate keeps only an unmarked space that needs no chang…
wmadden-electric Oct 6, 2026
cbc1b22
refactor(cli): name the plans db migrate keeps from the runner; planR…
wmadden-electric Oct 6, 2026
c5321ae
refactor(migration-tools): contractHashAtMarker lives beside Contract…
wmadden-electric Oct 6, 2026
1bd26f7
fix(cli): db migrate --to @contract behaves exactly like an omitted --to
wmadden-electric Oct 6, 2026
707f283
fix(cli): db migrate --to @db reports a missing driver as a missing d…
wmadden-electric Oct 6, 2026
dba928b
fix(cli): the live-marker connection check keeps the user's --from an…
wmadden-electric Oct 6, 2026
f12e6cd
fix(cli): db migrate --show refuses a marker outside the graph and la…
wmadden-electric Oct 6, 2026
aa7be28
fix(cli): migration status checks the path from an offline --from and…
wmadden-electric Oct 6, 2026
6e70dbb
fix(migration-tools): one predicate decides whether a marker is in a …
wmadden-electric Oct 6, 2026
307fd9b
fix(cli): db update resolves --to before it checks for a connection
wmadden-electric Oct 6, 2026
a257ae3
fix(cli): the reserved-reference refusal names the argument, and the …
wmadden-electric Oct 6, 2026
2d18514
fix(cli): migration plan --to and migration ref set take their form l…
wmadden-electric Oct 6, 2026
6ffd27f
docs: one contract-reference grammar in the migration subsystem doc; …
wmadden-electric Oct 6, 2026
1d3f017
docs(migration-tools): the contract-reference parser documents how ca…
wmadden-electric Oct 6, 2026
2fc4624
test(cli): pin the db migrate --to targets, the ref name, and the ext…
wmadden-electric Oct 6, 2026
f5095b6
test(cli): split the status and db migrate test files so each stays u…
wmadden-electric Oct 6, 2026
c77ea06
fix(migration-tools): the head of any space with no migrations counts…
wmadden-electric Oct 6, 2026
3e82929
fix(cli): label @db only in the trees whose plan uses the database ma…
wmadden-electric Oct 6, 2026
d619d7c
docs: with an offline --from, extension spaces start from the empty c…
wmadden-electric Oct 6, 2026
5089c88
test(cli): pin db migrate --show refusing a marker on a project with …
wmadden-electric Oct 6, 2026
cfb5c4a
fix(cli): db migrate and db update repeat the user flags in the missi…
wmadden-electric Oct 6, 2026
ad5d932
fix(cli): migration status names the extension head when an extension…
wmadden-electric Oct 6, 2026
3b18f1f
refactor(cli): migration status decides each space's origin once
wmadden-electric Oct 6, 2026
d1d167d
docs: one name for @contract, @db and @empty: reserved reference
wmadden-electric Oct 6, 2026
723545a
docs(cli): the README and help text describe the db migrate and migra…
wmadden-electric Oct 6, 2026
2d0613c
test(cli): two tests say and check what they mean
wmadden-electric Oct 6, 2026
d96f56a
Merge branch 'main' of https://github.com/prisma/prisma into wukong/3…
wmadden-electric Oct 6, 2026
f895e56
test(cli): the db sign reserved-reference test uses the dbSign mock t…
wmadden-electric Oct 6, 2026
858bf5d
fix(cli): migration status labels the empty node @db when --to @db re…
wmadden-electric Oct 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/CLI Style Guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ Concrete examples (from the migration CLI verb refactor, TML-2546). Each entry b
- Writes or updates the marker of every space that verified, in one transaction on PostgreSQL and SQLite: missing marker → insert; same hash → no‑op; different hash → overwrite, reporting the previous hash.
- Then writes each signed space's contract into its snapshot store and advances its `db` ref to the signed hash (`--advance-ref <name>` picks another ref). Unlike `db init` / `db update`, `--db` does not suppress this — signing never mutates the schema, and adoption normally runs against the real database via `--db`. `--no-advance-ref` signs without writing any ref or snapshot; combining it with `--advance-ref` is `CLI.ADVANCE_REF_ARG_CONFLICT` (exit code 2). Human output names the advanced ref and, when it existed, the previous hash; JSON is `{ ok, summary, spaces, advancedRefs }`: one outcome per space (`signed`, `unchanged` or `failed`) and one `{ space, name, hash }` per advanced ref.
- No migration package is written.
- Options: `[contract]` positional or `--contract <ref>` (hash, prefix, ref name, migration dir name, `<dir>^`, or `./path`; the positional accepts only the first four; defaults to the emitted `contract.json`; both together is `CLI.CONTRACT_ARG_CONFLICT`), `--db <url>`, `--advance-ref <name>`, `--no-advance-ref`.
- Options: `[contract]` positional or `--contract <ref>` (hash, prefix, ref name, migration dir name, or `<dir>^`; both accept the same forms; defaults to the emitted `contract.json`; both together is `CLI.CONTRACT_ARG_CONFLICT`), `--db <url>`, `--advance-ref <name>`, `--no-advance-ref`.
- Exit codes: 0 signed; 2 the command could not run (unresolvable contract reference, no emitted contract, unreachable database, conflicting flags); 4 verification failed for at least one space.

## Init Flow
Expand Down
27 changes: 20 additions & 7 deletions docs/architecture docs/subsystems/7. Migration System.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,13 +148,13 @@ Additive structure is covered by core operations: create table, add nullable col

**Default `from` resolution** ([`resolveFromForPlan`](../../../packages/1-framework/3-tooling/cli/src/control-api/operations/plan-resolution.ts)):

1. Explicit `--from <ref-or-hash>` — ref name, full hash, prefix, migration directory, `<dir>^`, or filesystem path.
1. Explicit `--from <contract>` — any [contract reference](#contract-reference-grammar) that names a recorded contract, or `@empty`.
2. No `--from` — resolve the `db` ref via `migrations/app/refs/db.json`.
3. No `db` ref — resolve `from` to the `null` empty-graph sentinel (greenfield). When the graph is also empty, the human output adds a muted notice (`No db ref set — planning from an empty database. Run db init, db update, or db sign if a database already exists.`) and the JSON document carries `fromDefaulted: true`; an explicit `--from @empty` prints neither.

The from-contract always materialises by reading the content-addressed snapshot store entry for the resolved hash — the ref-resolved hash comes from the ref's pointer, the hash-resolved `from` on a graph node from the matching bundle; either way the store is keyed by hash, so no bundle lookup is needed.

**Default `to` resolution:** when `--to` is omitted, the destination is the emitted `contract.json`. When `--to <ref-or-hash>` is supplied, the same [contract-reference grammar](#refs-environment-targets) as `--from` applies (hash / prefix, ref name, migration directory, `<dir>^`, or filesystem path); the resolved contract becomes the planner destination and is written into the snapshot store keyed by its storage hash. Use `--to <migration-dir>^` to plan a reverse (rollback) edge toward a predecessor state.
**Default `to` resolution:** when `--to` is omitted, the destination is the emitted `contract.json`. When `--to <contract>` is supplied, it takes the [contract references](#contract-reference-grammar) that name a recorded contract (`@empty` is an origin only); the resolved contract becomes the planner destination and is written into the snapshot store keyed by its storage hash. Use `--to <migration-dir>^` to plan a reverse (rollback) edge toward a predecessor state.

**Emission cases:**

Expand Down Expand Up @@ -331,6 +331,19 @@ Migrations form a directed graph (not necessarily acyclic) via their `from` / `t

Refs map logical environment names to contract hashes in `migrations/<space>/refs/<name>.json` (e.g., `{ "hash": "...", "invariants": [] }`). They are version-controlled alongside migration artifacts. `db migrate --to production` uses the ref hash as the target instead of the current contract. `migration status --to staging` reports state relative to that ref. Refs are managed via `prisma migration ref set <name> <contract>`, `prisma migration ref list`, and `prisma migration ref delete <name>`. See [ADR 169 — On-disk migration persistence](../adrs/ADR%20169%20-%20On-disk%20migration%20persistence.md).

#### Contract-reference grammar

A contract reference names one contract. Five forms name a contract recorded in the migrations directory: a full storage hash, a unique hash prefix, a ref name, a migration directory name (the migration's `to` contract), and `<dir>^` (the migration's `from` contract). Three reserved references, written as tokens, resolve without the migrations directory: `@contract` is the hash of the emitted `contract.json`, `@db` is the database marker (so it needs a connection), or the empty contract when the database has none, and `@empty` is the empty contract. When an input matches more than one form, the command refuses it as ambiguous; a longer or different form, such as a full hash, resolves it.

| Argument | `@contract` | `@db` | `@empty` |
|---|---|---|---|
| `migration status --from`, `--to` | yes | yes | yes |
| `db migrate --to`, `db migrate --show --from`, `--to` | yes | yes | yes |
| `migration plan --from` | no | no | yes |
| `migration plan --to`, `migration ref set`, `db update --to`, `db sign` | no | no | no |

`db update --to` and `db sign` refuse the reserved references with `MIGRATION.REF_WRONG_GRAMMAR`, as `migration plan --to` refuses `@empty`. In `migration status`, `db migrate` and `db migrate --show`, `--from` and `--to` apply to the app space. Each extension space goes to its own head: from its own marker when the command reads the database for the origin (no `--from`, or `--from @db`), and from the empty contract when `--from` names a contract.

#### Contract resolution through the snapshot store

A ref is only its pointer file — `{ hash, invariants }`. It carries no contract copy of its own; the contract it names resolves through the shared content-addressed store at `migrations/snapshots/<hex>/contract.{json,d.ts}` by that hash, the same store every graph node resolves through. See [ADR 218 — Refs with paired contract snapshots and universal graph-node invariant](../adrs/ADR%20218%20-%20Refs%20with%20paired%20contract%20snapshots%20and%20universal%20graph-node%20invariant.md) (its paired-snapshot part is superseded — see the ADR's Status note) and [ADR 240 — Contract snapshots live in a content-addressed store](../adrs/ADR%20240%20-%20Contract%20snapshots%20live%20in%20a%20content-addressed%20store.md).
Expand Down Expand Up @@ -539,7 +552,7 @@ The remedy in every case is the same: edit the slots in `migration.ts`, run the

Top-level verbs:

- `prisma db migrate --db <url> [--to <contract>] [--advance-ref <name>]` — execute pending migrations against a live database. Ref advancement is **opt-in only** via `--advance-ref`; plain `db migrate` does not advance any ref. The `<contract>` argument accepts the full [contract-reference grammar](#refs-environment-targets): hash / prefix, ref name, migration directory name, `<dir>^`, or filesystem path.
- `prisma db migrate --db <url> [--to <contract>] [--advance-ref <name>]` — execute pending migrations against a live database. Ref advancement is **opt-in only** via `--advance-ref`; plain `db migrate` does not advance any ref. The `<contract>` argument accepts every [contract reference](#contract-reference-grammar), including `@contract`, `@db` and `@empty`.
- `prisma db init --db <url> [--advance-ref <name>]` — bootstrap a database under contract control. When run against the default `--db` URL (no explicit `--db`), implicitly advances the `db` ref (write-if-absenting its contract into the snapshot store, then writing the pointer). With any explicit `--db` (even one naming the default URL), ref advancement is suppressed unless `--advance-ref` is explicit.
- `prisma db update --db <url> [--to <contract>] [--advance-ref <name>]` — reconcile a live database to the named (or emitted) contract via live introspection. Same implicit `db` ref default and `--db` opt-out as `db init`. Off-graph; dev-only.
- `prisma db sign --db <url> [<contract>] [--advance-ref <name>] [--no-advance-ref]` (or `--contract <contract>`) — sign the marker of every contract space the live DB already satisfies: the application's (with no argument, the emitted `contract.json`) and each extension's. Each space is verified without strict mode; the markers of the spaces that verified are written in one transaction on PostgreSQL and SQLite, and a space that fails is reported with its drift and makes the command exit 4. After signing, writes each signed space's contract into its snapshot store and advances its `db` ref (or `--advance-ref <name>`) to the signed hash; an existing ref is overwritten and the previous hash is reported in the human output (the JSON `advancedRefs` lists `{ space, name, hash }` per advanced ref). Unlike `db init` / `db update`, `--db` does not suppress this — sign never mutates the schema, and adoption is normally done against the real database via `--db`. `--no-advance-ref` is the opt-out: it signs without writing any ref or snapshot (JSON `advancedRefs` is empty), and combining it with `--advance-ref` is refused with `CLI.ADVANCE_REF_ARG_CONFLICT`. No migration package is written. After `--no-advance-ref` there is no `db` ref, so the next default `migration plan` starts from the empty contract (with the muted `No db ref set` notice) on an empty graph, or refuses with `MIGRATION.PLAN_ORIGIN_UNKNOWN` on a non-empty graph.
Expand All @@ -548,9 +561,9 @@ Top-level verbs:

Migration namespace (artifacts and graph):

- `prisma migration plan [--from <contract>] [--to <contract>] --name <slug>` — diff contracts and write a fully attested package (`migration.ts` + `migration.json` + `ops.json`) offline. Defaults `--from` to the `db` ref and `--to` to the emitted contract; when the `db` ref is absent, planning proceeds from greenfield only on an empty graph (with a muted `No db ref set` notice and `fromDefaulted: true` in JSON) — with existing migrations on disk the command refuses (`MIGRATION.PLAN_ORIGIN_UNKNOWN`) unless `--from @empty` names the empty-database origin explicitly. Both flags accept the full [contract-reference grammar](#refs-environment-targets). Re-run `./migration.ts` after filling any `placeholder(...)` slots to rewrite `ops.json` and the `migrationHash`.
- `prisma migration plan [--from <contract>] [--to <contract>] --name <slug>` — diff contracts and write a fully attested package (`migration.ts` + `migration.json` + `ops.json`) offline. Defaults `--from` to the `db` ref and `--to` to the emitted contract; when the `db` ref is absent, planning proceeds from greenfield only on an empty graph (with a muted `No db ref set` notice and `fromDefaulted: true` in JSON) — with existing migrations on disk the command refuses (`MIGRATION.PLAN_ORIGIN_UNKNOWN`) unless `--from @empty` names the empty-database origin explicitly. Both flags take [contract references](#contract-reference-grammar); `--from` also accepts `@empty`. Re-run `./migration.ts` after filling any `placeholder(...)` slots to rewrite `ops.json` and the `migrationHash`.
- `prisma migration new [--from <hash>] --name <slug>` — scaffold an empty `migration.ts` for hand-authoring.
- `prisma migration status [--db <url>] [--to <contract>] [--from <contract>]` — path/pending question. Live (uses marker) or offline (uses `--from`).
- `prisma migration status [--db <url>] [--to <contract>] [--from <contract>]` — path/pending question. Reads the database marker by default; `--from` names the origin instead and runs offline, unless `--from` or `--to` is `@db`, which reads the database.
- `prisma migration log --db <url>` — applied execution history (reads the marker; offline reading of the ledger is also supported).
- `prisma migration list` — enumerate migrations on disk in topological order. Offline.
- `prisma migration graph` — render the migration graph (ASCII tree by default; `--json` / `--dot` for other formats). Offline.
Expand Down Expand Up @@ -596,7 +609,7 @@ Structured diagnostics from plan-time and apply-time checks suggest concrete rec
|---|---|---|
| `MIGRATION.HASH_NOT_IN_GRAPH` | `migration plan` or `migration ref set`: resolved hash not in graph | `migration plan --from <reachable-ref>` (e.g. `--from production`) |
| `MIGRATION.SNAPSHOT_MISSING` | `migration plan`: a named ref has no pointer file, and the hash being resolved isn't a graph node either | `migration ref set <name> <hash>` to create the ref, `db update --advance-ref <name>` to advance it, or pass a hash that is a graph node |
| `MIGRATION.MARKER_MISMATCH` | `db migrate`: live marker hash not a graph node (pre-DDL check) | `migration plan --from <graph-tip>`, or `migration ref set db <marker-hash>` if on-disk graph is canonical |
| `MIGRATION.MARKER_MISMATCH` | `db migrate` (pre-DDL check) and `db migrate --show`: live marker hash not a graph node | `migration plan --from <graph-tip>`, or `migration ref set db <marker-hash>` if on-disk graph is canonical |
| `MIGRATION.PATH_UNREACHABLE` | `db migrate`: no path from marker to target in on-disk graph | Plan the missing edge with `migration plan --from <marker-or-reachable> --to <target> --name <slug>`, then apply with `db migrate --to <target>`. For a rollback, use `--to <migration-dir>^` in both steps — the planned reverse edge applies and moves the marker back without editing contract source. Review destructive (`DROP`) ops in the plan before applying. When the space has no on-disk migrations yet, omit `--from` and use `migration plan --to <target> --name <slug>` first. |

After plain `db migrate`, refresh a stale `db` ref with `db update` (no-op on DB when marker matches) or `db migrate --advance-ref db` in the same invocation.
Expand Down Expand Up @@ -776,7 +789,7 @@ Errors use the stable category/code envelope (see [ADR 027 — Error Envelope &
- `MIGRATION.SAME_SOURCE_AND_TARGET` — migration edge has `from === to` (graph invariant violation)
- `MIGRATION.HASH_NOT_IN_GRAPH` — resolved hash is not a node in the on-disk graph (plan-time / `migration ref set`)
- `MIGRATION.SNAPSHOT_MISSING` — a named ref has no pointer file, and the hash being resolved isn't a graph node either (plan-time)
- `MIGRATION.MARKER_MISMATCH` — live DB marker hash is not a graph node (apply-time, pre-DDL)
- `MIGRATION.MARKER_MISMATCH` — live DB marker hash is not a graph node (`db migrate` before any DDL, and `db migrate --show`)
- `MIGRATION.PATH_UNREACHABLE` — no migration path from marker to target (apply-time; improved `fix` payload)

**Authoring errors** (`PN-MIG-*`):
Expand Down
4 changes: 2 additions & 2 deletions docs/design/10-domains/migration/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -378,8 +378,8 @@ The choices below are the load-bearing ones — the ones that, if reversed, woul
- **`db init` vs `db sign`.** Distinct: init lays down structure (live, mutates); sign verifies + writes marker (no structural mutation, refuses if DB doesn't already satisfy the contract).
- **"Freeze"** rejected. `migration plan` is the verb for the freeze-and-promise act.
- **Dev/deploy split** rejected. The safety semantics belong to the DB URL, not the verb.
- **Contract references and migration references.** Two parallel grammars sharing forms but resolving in different namespaces. `<contract>` resolves to a contract storage hash (accepts: hash, ref name, migration directory name → to-contract, `<dir>^` → from-contract, filesystem path). `<migration>` resolves to a migration (accepts: migration hash or directory name). The command's argument type determines which grammar applies — same hash-shaped input resolves in different namespaces depending on whether the command expects a `<contract>` or a `<migration>`. **In CLI argument syntax the placeholder is `<contract>` or `<migration>`** — no umbrella shorthand. A **ref** is a specific kind of contract reference (named, persisted, file-backed); the umbrella is **contract reference**.
- **Directory names are user-controlled.** The default `<timestamp>T<HHMM>_<slug>` is convention, not invariant. Ambiguity between a directory name and a hash prefix is an explicit ambiguity error with candidate listing — same Git rule for short SHAs that collide with branch names. Disambiguate with `./<path>` for filesystem paths or with a longer / different form.
- **Contract references and migration references.** Two parallel grammars sharing forms but resolving in different namespaces. `<contract>` resolves to a contract storage hash (accepts: hash or hash prefix, ref name, migration directory name → to-contract, `<dir>^` → from-contract, and the reserved references `@contract`, `@db` and `@empty` where the command allows them; see the [contract-reference grammar](../../../architecture%20docs/subsystems/7.%20Migration%20System.md#contract-reference-grammar)). `<migration>` resolves to a migration (accepts: migration hash or directory name). The command's argument type determines which grammar applies — same hash-shaped input resolves in different namespaces depending on whether the command expects a `<contract>` or a `<migration>`. **In CLI argument syntax the placeholder is `<contract>` or `<migration>`** — no umbrella shorthand. A **ref** is a specific kind of contract reference (named, persisted, file-backed); the umbrella is **contract reference**.
- **Directory names are user-controlled.** The default `<timestamp>T<HHMM>_<slug>` is convention, not invariant. Ambiguity between a directory name and a hash prefix is an explicit ambiguity error with candidate listing — same Git rule for short SHAs that collide with branch names. Disambiguate with a longer or different form, such as a full hash.
- **`db sign [<contract>]` (positional) or `db sign --contract <contract>` (explicit).** The argument names *the thing being signed* — neither `--to` (movement) nor `--at` (position) carries the right meaning. Defaults to the current `contract.json` when omitted.
- **`ref set <name> <contract>`** is the direct-ref-write verb. `move` was rejected because refs are stored values, not entities that traverse the graph — the spatial-movement vocabulary is reserved for `migrate`.
- **`head` ref dropped.** Refs are exclusively environment-named (`production`, `staging`, ...). The emitted `contract.json` already plays the role of "what the repo is working toward"; a `head` ref would have been redundant.
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/error-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1612,7 +1612,7 @@ A ref name resolves to nothing: no pointer file with that name exists, and the f

### MIGRATION.REF_WRONG_GRAMMAR

A reference parsed, but as the wrong kind for the argument position, e.g. a migration-only reference where a contract reference is required (raised by the shared ref-resolution mapper). The message and fix come from the resolver's own diagnosis. Payload: `input`, `expectedGrammar`.
A reference parsed, but as the wrong kind for the argument position, e.g. a migration-only reference where a contract reference is required (raised by the shared ref-resolution mapper). The message and fix come from the resolver's own diagnosis. `db sign` and `db update --to` raise it for the reserved references `@contract`, `@db`, and `@empty`, which they do not accept, and `migration plan --to @empty` raises it because `@empty` is only valid as an origin. Payload: `input`, `expectedGrammar`.

### MIGRATION.RUNNER_FAILED

Expand Down
Loading
Loading