Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions .gitleaksignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@
# secrets, and a later commit in the same PR replaced those lines (squash-merge PR).
f86694b619c360078040116a01fc8826d7e7dfcc:test/integration/test/uuid-defaults/uuid-defaults.integration.test.ts:generic-api-key:348
f86694b619c360078040116a01fc8826d7e7dfcc:test/integration/test/uuid-defaults/uuid-defaults.integration.test.ts:generic-api-key:349
873554568f3d4bfd48a8149b158fa8c0a9708ab3:projects/migration-statements/manual-qa-reports/2026-10-07-qa-opus.md:generic-api-key:113
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# ADR 001 — Migrations as Edges

> **Update — 2026-10:** the planner hints this ADR anticipates for renames are not built. A rename is stated on the command line as a statement, `--rename <old>:<new>`, which never enters the contract or the migration edge. See [Migration System § Statements](../subsystems/7.%20Migration%20System.md#statements).

## Context

Traditional migration systems rely on the order of files on disk to decide what to run next. That creates coupling to file sequencing, makes squashing brittle, and complicates collaboration when branches diverge. Agents and CI need deterministic, machine-verifiable answers to basic questions like “is it safe to apply this change” and “how do I get from here to there.”
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# ADR 028 — Migration structure & operations

> **Update — 2026-10:** edges do not carry the `hints` field shown below. A rename is stated on the command line as a statement, `--rename <old>:<new>`, and the planner turns it into rename operations in the edge's `ops`. See [Migration System § Statements](../subsystems/7.%20Migration%20System.md#statements).

## Context

Prisma Next models migrations as edges between data contract hashes rather than ordered files on disk. The system needs a well-defined migration structure that supports graph reconstruction, deterministic pathfinding, and operational transformations like squashing. Teams want to squash old edges into a baseline to speed fresh environment bootstrap while guaranteeing production safety.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

Status: **Accepted**

> **Note (2026-10-05).** Dropping an index is now classed `widening`, not `destructive`, because an index holds no stored data. Where this ADR says an index is dropped under a destructive policy, read a policy that allows `widening`.

Related: [ADR 234 — Content-addressed wire names for Postgres-normalized objects](<./ADR 234 - Content-addressed wire names for Postgres-normalized objects.md>) (extended here), [ADR 235 — The schema differ walks two derived schema IRs](<./ADR 235 - The schema differ walks two derived schema IRs.md>), [ADR 009 — Deterministic Naming Scheme](<./ADR 009 - Deterministic Naming Scheme.md>), [ADR 161 — Explicit foreign key constraint and index configuration](<./ADR 161 - Explicit foreign key constraint and index configuration.md>), [ADR 210 — Index-type registry](<./ADR 210 - Index-type registry.md>), [ADR 224 — Control Policy](<./ADR 224 - Control Policy — framework-locked vocabulary and family-owned dispatch.md>).

## Decision
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

Status: **Accepted**

> **Note (2026-10-05).** Dropping a check constraint is now classed `widening`, not `destructive`, because a check holds no stored data. Where this ADR says a check is dropped, or a database converges, under a policy that allows `destructive`, read a policy that allows `widening`.

Related: [ADR 234 — Content-addressed wire names for Postgres-normalized objects](<./ADR 234 - Content-addressed wire names for Postgres-normalized objects.md>) (extended here), [ADR 243 — Name-identified indexes and exact-name adoption](<./ADR 243 - Name-identified indexes and exact-name adoption.md>) (its constraint carve-out no longer covers checks), [ADR 156 — Storage sets and check constraints](<./ADR 156 - Storage sets and check constraints.md>) (its check-constraint half is superseded by this ADR), [ADR 235 — The schema differ walks two derived schema IRs](<./ADR 235 - The schema differ walks two derived schema IRs.md>), [ADR 224 — Control Policy](<./ADR 224 - Control Policy — framework-locked vocabulary and family-owned dispatch.md>).

## Decision
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,9 @@ A schema that declares no `@@map` and whose storage was created under the loweri
- **Keep the lowering rule and write `@@map` from `contract infer` whenever the model name does not round-trip.** This repairs infer but leaves a default that matches no convention, contradicts every other authoring surface, and surprises anyone reading a schema without one.
- **Infer a rename from a drop and a create whose columns match.** Two unrelated tables can have the same columns, and a wrong guess moves rows into the wrong table. A guess that cannot be verified is worse than a refusal that names the fix.
- **State the rename on the command line, for example `migration plan --rename old=new`.** The command line belongs to the framework CLI, which serves every family, so a flag naming tables puts one family's vocabulary in a shared surface; MongoDB accepted such a flag and planned a collection drop. It also duplicates the planner hint below.

> **Update — 2026-10:** renames are now stated on the command line after all, as `--rename <old>:<new>` on `migration plan` and `db update`. A statement names models and fields, not tables, so no family vocabulary enters the shared CLI, and a statement that resolves to nothing is an error. It drives the rename operation this ADR introduces. See [Migration System § Statements](../subsystems/7.%20Migration%20System.md#statements).

- **A planner hint in the contract source.** An annotation such as `@hint(was: "userProfile")` would let `migration plan` and `db update` rename without a hand-written migration. It is the intended direction and is recorded in the Data Contract and Migration System subsystem documents and in [ADR 001](ADR%20001%20-%20Migrations%20as%20Edges.md). It needs its own decisions first: the syntax in PSL and in TypeScript authoring, whether the hint names the old model or the old storage, how a hint reaches an offline planner without entering `contract.json` or its hashes, and what a hint that matches nothing means. The rename operation is the part a hint would drive, so it is built first.

> **Update — 2026-10:** a contract-source hint is no longer the direction. Renames are stated on the command line as statements (see the note under the previous alternative), and nothing in the contract source records them. See [Migration System § Statements](../subsystems/7.%20Migration%20System.md#statements).
13 changes: 6 additions & 7 deletions docs/architecture docs/subsystems/1. Data Contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,12 @@ If provenance is needed for error UX, it must be carried via diagnostics (CLI/ed

A model names its table, or its MongoDB collection, exactly as the model is written; a field names its column by its own name. A different storage name is stated with `@@map` or `@map`. Nothing transforms identifier case implicitly, on any authoring surface: PSL, TypeScript authoring, and a Prisma 7 schema read as a contract source all behave this way, and `contract infer` writes `@@map` whenever the model name it derives differs from the name it read. Changing a model's storage name therefore renames existing storage, which a migration states with `this.renameTable(...)`. See [ADR 264 — A model names its storage verbatim, and a rename is an operation](../adrs/ADR%20264%20-%20A%20model%20names%20its%20storage%20verbatim,%20and%20a%20rename%20is%20an%20operation.md).

### Canonical contract vs planner hints
### Canonical contract vs migration statements

The canonical contract IR and its `storageHash`/`executionHash`/`profileHash` capture only **application expectations** (models, relations, storage layout, capabilities) and extension-owned metadata. Planner‑specific inputs such as rename hints or migration strategies are **not** part of the canonical contract and **do not** participate in hashing.
The canonical contract IR and its `storageHash`/`executionHash`/`profileHash` capture only **application expectations** (models, relations, storage layout, capabilities) and extension-owned metadata. Migration intent, such as that a model or field was renamed rather than dropped and replaced, is **not** part of the canonical contract and **does not** participate in hashing.

- Authoring layers (PSL or TypeScript contract authoring) may expose annotations like `@hint(was: "old_name")` to guide migration planning.
- These annotations are consumed by the migration planner alongside contracts and are recorded in **migration edges** as structured metadata (see the Migration System subsystem and [ADR 028 — Migration Structure & Operations](../adrs/ADR%20028%20-%20Migration%20Structure%20&%20Operations.md)).
- Downstream contract consumers such as the runtime, lanes, and query DSL treat `contract.json` as planner‑agnostic; they do not rely on or interpret planner hints.
- The user states that intent on the command line as statements, for example `prisma migration plan --rename Profile:User` or `prisma db update --rename User.name:User.fullName`. The planner takes them beside the two contracts; see [Migration System § Statements](./7.%20Migration%20System.md#statements).
- Downstream contract consumers such as the runtime, lanes, and query DSL treat `contract.json` as planner‑agnostic.

## Authoring Modes

Expand Down Expand Up @@ -326,7 +325,7 @@ The following rules ensure that extension content is encoded and hashed consiste
Contract changes are classified to guide safe evolution, planning, and verification without surprising production behavior.

- **Additive changes** (add table/column, nullable field, new index) → compatible; produce new `storageHash`
- **Renames/drops/type changes** → require explicit planner hints (expressed in the authoring layer) and a migration plan; not auto‑compatible
- **Renames/drops/type changes** → need a migration plan, and a rename needs a statement on the command line ([Migration System § Statements](./7.%20Migration%20System.md#statements)); not auto‑compatible
- **Non‑null column addition** → needs default/backfill task; planner should fail fast without a task
- **Capability changes** (e.g., enabling `jsonAgg`) → affect `profileHash`; do not change meaning
- **Deprecation policy:** contract schema (`schemaVersion`) evolves under semver; breaking changes bump major
Expand Down Expand Up @@ -656,7 +655,7 @@ Determinism and safety are enforced through a layered test strategy that covers
- **Schema conformance:** arktype schema validation; property tests for allowed/denied shapes
- **Determinism:** golden tests for PSL/TS → `contract.json` + `storageHash`/`executionHash`/`profileHash`
- **Round‑trip:** PSL→contract→PSL (rendered) sanity checks for information preservation (where applicable)
- **Diff tests:** known change sets (add column, add FK, rename with hint) → expected diffs
- **Diff tests:** known change sets (add column, add FK, rename with a statement) → expected diffs
- **Adapter conformance:** per‑dialect suite validating codecs, capabilities, and validation hooks

## References
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ RLS policies and Postgres roles are **target-only entity kinds** — the framewo
- **Authoring, both surfaces.** PSL: per-operation blocks (`policy_select` / `policy_insert` / `policy_update` / `policy_delete` / `policy_all`) plus the `@@rls` model attribute, contributed declaratively via block descriptors and the target-contributed model-attribute SPI ([ADR 236](../adrs/ADR%20236%20-%20Target-contributed%20model%20attributes.md)). TypeScript: top-level helpers on `@internal/postgres/contract-builder` (`policySelect(model, …)`, `rlsEnabled(model)`, `role(name)`) lowered through the generic pack-entity channel. Both surfaces share one predicate matrix and one hash assembly in the target, so the same policy authored either way emits byte-identical `contract.json`.
- **Naming and equivalence.** A policy is either **wire-named** or **exact-named**. Wire-named (the default): the authored name is a prefix, the target appends an 8-hex SHA-256 suffix over the canonical content tuple, and equivalence is wire-name identity — the verifier never compares predicate bodies ([ADR 234](../adrs/ADR%20234%20-%20Content-addressed%20wire%20names%20for%20Postgres-normalized%20objects.md)). Exact (`@@map("physical name")` on the policy block): the name is adopted verbatim — no prefix, no hash, and no wire-prefix cap (the name is still a physical identifier, so the target's own limits apply; Postgres truncates identifiers to 63 bytes) — and equivalence becomes content comparison (structured attributes strict, `using`/`withCheck` byte-for-byte), reliable precisely when the body text is a Postgres reprint; hand-authoring a body under `@@map` warns (`PN_EXACT_NAME_BODY_COMPARISON`, shared per-build batch with index `map:`).
- **Diff and verify.** Policies and roles are nodes in the Postgres schema-IR tree (database → namespace → table → policy; roles on the database root) walked by the one generic differ ([ADR 235](../adrs/ADR%20235%20-%20The%20schema%20differ%20walks%20two%20derived%20schema%20IRs.md)). `@@rls` enablement diffs as a table attribute (`pg_class.relrowsecurity`); removing a marked table's last policy leaves RLS enabled (deny-all, fail-closed). Roles verify existence-only against `pg_roles`: a declared-but-absent role fails verify even under `control: 'external'`; an undeclared live role is tolerated; roles produce zero migration operations.
- **Planning.** Diff issues map to `CREATE POLICY` / `DROP POLICY` / `ALTER POLICY … RENAME TO` and `ENABLE`/`DISABLE ROW LEVEL SECURITY`, graded per table by its control policy ([ADR 224](../adrs/ADR%20224%20-%20Control%20Policy%20—%20framework-locked%20vocabulary%20and%20family-owned%20dispatch.md)). Renames pair in two phases: matching hash suffix under a different prefix (phase 1), then remaining wire-named-missing policies against extras of any name shape by verbatim content (phase 2 — how replacing `@@map` with the plain head converges as one rename). An exact-named policy whose live body drifted is a `not-equal` issue: drop + create under a destructive-allowed policy, a planner conflict otherwise.
- **Planning.** Diff issues map to `CREATE POLICY` / `DROP POLICY` / `ALTER POLICY … RENAME TO` and `ENABLE`/`DISABLE ROW LEVEL SECURITY`, graded per table by its control policy ([ADR 224](../adrs/ADR%20224%20-%20Control%20Policy%20—%20framework-locked%20vocabulary%20and%20family-owned%20dispatch.md)). Renames pair in two phases: matching hash suffix under a different prefix (phase 1), then remaining wire-named-missing policies against extras of any name shape by verbatim content (phase 2 — how replacing `@@map` with the plain head converges as one rename). An exact-named policy whose live body drifted is a `not-equal` issue: drop + create under a policy that allows `widening`, a planner conflict otherwise.

## Mongo family (document targets)

Expand Down
Loading
Loading