Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
c8bb518
The review-thread rule and its command live in one place in the Drive…
wmadden-electric Oct 30, 2026
7281c49
MongoDB PSL and TypeScript merge identical and redundant indexes befo…
wmadden-electric Oct 30, 2026
8c268be
The Prisma 6 MongoDB reader refuses two indexes on the same fields in…
wmadden-electric Oct 30, 2026
c5621f0
The docs and upgrade notes say MongoDB PSL and TypeScript merge index…
wmadden-electric Oct 30, 2026
17db0bf
The Prisma 6 MongoDB reader refuses exactly the index pairs Prisma 6 …
wmadden-electric Oct 30, 2026
bac0a14
The MongoDB builder and the index merge share one check of which inde…
wmadden-electric Oct 30, 2026
dee4b0a
The Prisma 6 reader names both indexes when MongoDB cannot hold two i…
wmadden-electric Oct 30, 2026
0450a0d
The Prisma 6 journey verifies a database holding a unique and a plain…
wmadden-electric Oct 30, 2026
6c10711
Merge remote-tracking branch 'origin/main' into tml-3774-mongo-index-…
wmadden-electric Oct 30, 2026
4b323c5
The Prisma 6 reader's index messages name the other index and show a …
wmadden-electric Oct 30, 2026
cf30885
Merge remote-tracking branch 'origin/main' into tml-3774-mongo-index-…
wmadden-electric Oct 30, 2026
7b58659
The rc.22 upgrade note stays as released, and this change gets its ow…
wmadden-electric Oct 30, 2026
4d912d0
The Prisma 6 index upgrade note detects schemas with MongoDB indexes
wmadden-electric Oct 30, 2026
b2ea35e
The MongoDB builder assembles each collection in one object instead o…
wmadden-electric Oct 30, 2026
a95a6d2
The released rc.22 detection keeps its test, the new upgrade notes ge…
wmadden-electric Oct 30, 2026
aca224c
The Prisma 6 index upgrade note says to keep the unique index of a cl…
wmadden-electric Nov 2, 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
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,14 @@ The builder checks the assembled contract last. Those refusals (kind `validator`
- **PSL.** The interpreter reads the schema into the definition. It keeps the rules about how a schema is written, such as attribute arguments, `@id` placement, back-relation pairing and unresolved references. It reports each refusal as a diagnostic at the place its key names, with PSL's codes and wording.
- **Prisma 6.** The reader builds with `writesCollectionValidators: false`, because a Prisma 6 database has none. It reports each refusal in its own diagnostics.

**The builder builds the definition it is given.** It checks the definition and refuses what is invalid, but it does not merge, drop or tidy what a source wrote. Tidying is a convenience of the authoring surface the user writes in. For example, Prisma 8 PSL and TypeScript `defineContract` merge an index the user wrote twice without a name to tell the two apart, and they do it before they call the builder. A source that reads an earlier Prisma version's schema exists to produce the contract of the database that version built, so `db sign` accepts it unchanged (ADR 252). It tidies nothing, and the builder keeps everything it describes, even an index the database holds that is redundant, such as an index on `[email, name]` beside one on `[email]`. Prisma 6 itself refuses two indexes on the same fields, so a Prisma 6 database never holds such a pair. MongoDB's builder still merges identical and redundant indexes itself, for every source, the Prisma 6 reader included; TML-3774 moves the merge into PSL and TypeScript and makes the Prisma 6 reader refuse two indexes on the same fields, as Prisma 6 does. SQL's builder still merges duplicate and redundant unnamed indexes itself, and the Prisma 7 reader escapes that only by naming every index; TML-3770 moves the merge into SQL's authoring sources.
**The builder builds the definition it is given.** It checks the definition and refuses what is invalid, but it does not merge, drop or tidy what a source wrote. Tidying is a convenience of the authoring surface the user writes in. For example, Prisma 8 PSL and TypeScript `defineContract` merge an index the user wrote twice without a name to tell the two apart, and they do it before they call the builder. A source that reads an earlier Prisma version's schema exists to produce the contract of the database that version built, so `db sign` accepts it unchanged (ADR 252). It tidies nothing, and the builder keeps everything it describes, even an index the database holds that is redundant, such as an index on `[email, name]` beside one on `[email]`. Prisma 6 itself refuses two indexes of one model, of any kind, whose field paths are the same, in the same order, with the same written sort (an unwritten sort is not `sort: Asc`), so a Prisma 6 database never holds such a pair. The Prisma 6 reader refuses the same pairs, as Prisma 6 does, and merges nothing. SQL's builder still merges duplicate and redundant unnamed indexes itself, and the Prisma 7 reader escapes that only by naming every index; TML-3770 moves the merge into SQL's authoring sources.

## Consequences

- One rule, one implementation. A rule added to the builder applies to every source.
- A source can report every refusal at once, at its own position, because refusals are data, not thrown errors.
- Sources cannot differ in what they accept. A parity suite writes each case in TypeScript and in PSL and checks that the two emitted `contract.json` files are byte-identical.
- Index order is the source's own. The builder does not reorder indexes, because the storage hash depends on their order. When two indexes in one collection are identical, or one is a non-unique index made redundant by a unique index on the same keys and otherwise identical options, the builder removes one and keeps the survivor where it was written. That merge leaves the builder with TML-3774.
- Index order is the source's own. The builder does not reorder indexes, because the storage hash depends on their order. MongoDB PSL and TypeScript `defineContract` merge indexes before they call the builder, with `mergeRedundantIndexes`: when two indexes in one collection are identical, or one is a non-unique index made redundant by a unique index on the same keys and otherwise identical options, they remove one and keep the survivor where it was written. The builder itself refuses two identical indexes in one collection.

SQL does not follow this ADR fully yet. Its builder throws its first refusal, so SQL PSL places only the errors it catches and matches (TML-3730). SQL PSL also adds polymorphism to the contract after the builder runs, instead of carrying it in the definition (TML-2228).

Expand Down
4 changes: 3 additions & 1 deletion docs/architecture docs/subsystems/10. MongoDB Family.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ The builder decides these behaviours the same way for every source:

| Behaviour | Every source |
| --- | --- |
| Identical or redundant indexes in one collection, after variant scoping | Merged into one. Two indexes are identical when they have the same keys and options, counting an option set to `false` as unset (`indexLookupKey`, which the planner also uses). A non-unique index is redundant beside a unique one that is otherwise identical. The unique one survives, else the first, and it keeps the position where it was written. `@unique` and `@@index` on one field give one unique index. The merge runs before the second text index is refused |
| Two identical indexes in one collection, after variant scoping | Refused. Two indexes are identical when they have the same keys and options, counting an option set to `false` as unset (`indexLookupKey`, which the planner also uses). A unique and a plain index on the same keys are both kept |
| `extensions` | The stack's extension packs |
| Roots | Explicit roots when the source gives them, else one per model with a collection, no owner and no base |
| A variant's collection | Its base's; another collection is refused |
Expand All @@ -268,6 +268,8 @@ The builder decides these behaviours the same way for every source:
| Generated values | As on SQL: an optional generated field is accepted; a list that generates a value is refused |
| Index order | Each source's own; the builder does not reorder, because the storage hash depends on the order |

MongoDB PSL and TypeScript `defineContract` merge indexes before they call the builder, with `mergeRedundantIndexes` from `@internal/mongo-contract-ts`. Of two identical indexes in one collection the first is kept. A non-unique index beside a unique one that is otherwise identical is removed, so `@unique` and `@@index` on one field give one unique index. The index that is kept stays where it was written. The Prisma 6 reader merges nothing: it refuses two indexes of one model with the same fields and the same written sort, as Prisma 6 does (`PSL.PRISMA6_MONGO_INDEX_ALREADY_EXISTS`).

A parity suite, `test/integration/test/authoring/mongo-parity.test.ts`, writes each case in TypeScript and in PSL and checks that the two emitted `contract.json` files are byte-identical.

## Contract
Expand Down
13 changes: 12 additions & 1 deletion docs/reference/error-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -535,7 +535,7 @@ The contract module at the configured path loads but exposes neither a `default`

### CONTRACT.NAME_DUPLICATE

Two declarations claim the same name: duplicate namespace entries, model names, value objects, relations, tables (two models mapping to one table, two table nodes for one table, or duplicate table in a namespace), column mappings (two fields to one column, a field and a table node's column node for one column, or two column nodes for one column), stored names (two fields of one MongoDB model stored under one name with `.mongo({ field })`: `Model "<Model>" stores both "<a>" and "<b>" as "<name>".`, `kind: 'field'`), value-sets (enum and pack entity minting the same value-set), or pack entities of the same kind and name in one namespace. A Postgres extension is unique across the whole contract: one declared in two namespaces is refused while building a contract and again when a contract is read (`kind: 'extension'`, `namespaces`). Payload: `kind`, `name`, `namespaceId`, `tableName` (for a column), `first`, `second`. A MongoDB variant field stored under a name a field of its base is stored under, the discriminator included, is refused too (`reason: 'stored-name-shared-with-base'`, with `first` the base's field and `second` the variant's): a variant's documents hold its base's fields, so the two would overwrite each other. Mongo PSL reports it as `PSL_DUPLICATE_STORED_NAME`.
Two declarations claim the same name: duplicate namespace entries, model names, value objects, relations, tables (two models mapping to one table, two table nodes for one table, or duplicate table in a namespace), column mappings (two fields to one column, a field and a table node's column node for one column, or two column nodes for one column), stored names (two fields of one MongoDB model stored under one name with `.mongo({ field })`: `Model "<Model>" stores both "<a>" and "<b>" as "<name>".`, `kind: 'field'`), indexes (two identical indexes in one MongoDB collection: `kind: 'index'`, with `collection`; TypeScript `defineContract` merges identical indexes before the build, so only a definition passed to `buildMongoContractFromDefinition` directly reaches this), value-sets (enum and pack entity minting the same value-set), or pack entities of the same kind and name in one namespace. A Postgres extension is unique across the whole contract: one declared in two namespaces is refused while building a contract and again when a contract is read (`kind: 'extension'`, `namespaces`). Payload: `kind`, `name`, `namespaceId`, `tableName` (for a column), `first`, `second`. A MongoDB variant field stored under a name a field of its base is stored under, the discriminator included, is refused too (`reason: 'stored-name-shared-with-base'`, with `first` the base's field and `second` the variant's): a variant's documents hold its base's fields, so the two would overwrite each other. Mongo PSL reports it as `PSL_DUPLICATE_STORED_NAME`.

### CONTRACT.NAMESPACE_INVALID

Expand Down Expand Up @@ -715,6 +715,17 @@ The model's `@id` is not a required `String @db.ObjectId` stored as `_id`. Decla

An `@ignore`d field is used by `@unique`, `@@unique`, `@@index`, `@@fulltext`, or a relation's `fields:` or `references:`, and Prisma 6 still creates that index or reads that key. Remove `@ignore` from the field, which adds it to the Prisma 6 client, or remove what uses it: removing an index makes Prisma 6 `db push` drop it, and removing a relation field removes that relation from the Prisma 6 client. Reported by the Prisma 6 MongoDB contract source (`prisma6Schema`) during `contract emit`, as a finding in the `diagnostics` list of `CONTRACT.SOURCE_LOAD_FAILED`, never on its own. `summary` is `<file>:<line>:<column> <message>`, with only the file when there is no position (the terminal prints the code before it), and `where` carries `path` and, when known, `line`. Payload: none.

### PSL.PRISMA6_MONGO_INDEX_ALREADY_EXISTS

A Prisma 6 schema holds an index that already exists, in one of two senses. Remove one of the two indexes.

- **Prisma 6 refuses the pair.** Two indexes of one model, of any kind (`@unique`, `@@unique`, `@@index`, `@@fulltext`), whose fields have, position by position, the same names and the same written sort, such as `@unique` and `@@index([email])` on one field. An unwritten sort is not the same as `sort: Asc`. Prisma 6 refuses such a schema with "Index already exists in the model.", so no Prisma 6 database holds the pair. The code is reported at each index of the pair; the message names the other index, written as the schema writes it with any sort, and quotes Prisma 6.
- **MongoDB cannot hold the pair.** Two indexes that Prisma 6 accepts but that are identical in MongoDB, with the same keys and options, in one collection: `@@index([name])` beside `@@index([name(sort: Asc)])`, or the same index on two models stored in one collection. The code is reported at the second index, and the message names both.

The same fields in another order or with another sort are accepted. So is `@unique` beside `@@index([email(sort: Asc)])`, which gives a unique and a plain index on the same keys, as Prisma 6 `db push` creates them.

Reported by the Prisma 6 MongoDB contract source (`prisma6Schema`) during `contract emit`, as a finding in the `diagnostics` list of `CONTRACT.SOURCE_LOAD_FAILED`, never on its own. `summary` is `<file>:<line>:<column> <message>`, with only the file when there is no position (the terminal prints the code before it), and `where` carries `path` and, when known, `line`. Payload: none.

### PSL.PRISMA6_MONGO_INDEX_ARGUMENT_UNSUPPORTED

An index argument a Mongo contract index cannot carry, such as `length`, an unknown argument, or a dotted path whose first segment is not a composite-type field (`title.first`), which Prisma 6 refuses as an unknown field. `sort` is read, and `map` and `name` are accepted and dropped, since Mongo verify matches indexes by keys and options. Remove the argument. Reported by the Prisma 6 MongoDB contract source (`prisma6Schema`) during `contract emit`, as a finding in the `diagnostics` list of `CONTRACT.SOURCE_LOAD_FAILED`, never on its own. `summary` is `<file>:<line>:<column> <message>`, with only the file when there is no position (the terminal prints the code before it), and `where` carries `path` and, when known, `line`. Payload: none.
Expand Down
2 changes: 1 addition & 1 deletion drive/calibration/dod.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ In addition to the canonical slice DoD:

- [ ] After merging `main`: if the merge resolved conflicts with a change `main` made to the same code, run a review round on `git diff <main commit> <merge commit>` before asking for review ([`failure-modes.md § F51`](./failure-modes.md#f51-a-merge-of-main-that-resolves-a-large-overlapping-change-goes-to-review-as-if-it-were-not-new-work)); if `main` had a release bump, `git diff <release commit> HEAD -- upgrade-instructions/releases/` is empty ([`F52`](./failure-modes.md#f52-a-release-bump-archives-pending-upgrade-instructions-and-the-branchs-later-edits-land-in-the-released-copy)). (Added 2026-10-28, datetime-convenience-types.)
- [ ] If the slice changes what an existing type name means: every PSL and TypeScript source under `test/` and `examples/` that writes the name is checked, and the integration files that emit them are run ([`F53`](./failure-modes.md#f53-a-name-changes-meaning-and-a-hand-written-source-that-fixturescheck-does-not-emit-keeps-the-old-meaning)). (Added 2026-10-28, datetime-convenience-types.)
- [ ] Before enabling auto-merge: every review thread on the PR is resolved, by a fix or by a reply that says why not. List the open ones with `gh api graphql --paginate -F number=<PR number> -f query='query($number:Int!,$endCursor:String){repository(owner:"prisma",name:"orm"){pullRequest(number:$number){reviewThreads(first:100,after:$endCursor){pageInfo{hasNextPage endCursor} nodes{id isResolved path}}}}}' --jq '.data.repository.pullRequest.reviewThreads.nodes[]|select(.isResolved==false)|"\(.id) \(.path)"'`, which prints nothing when every thread is resolved; bot reviewers count. (Added 2026-10-27, mongo-authoring-structure: slice 1 merged with four open CodeRabbit threads, found only at close-out.)
- [ ] Before enabling auto-merge: every review thread on the PR is resolved, by a fix or by a reply that says why not; bot reviewers count. The command that lists open threads is in [`pr/README.md § Merge queue needs every review thread resolved`](../pr/README.md#merge-queue-needs-every-review-thread-resolved). (Added 2026-10-27, mongo-authoring-structure: slice 1 merged with four open CodeRabbit threads, found only at close-out.)
- [ ] If the slice changes code that `defineContract`, a pack descriptor or the runtime reaches: run `pnpm size:build` in `examples/bundle-size` (it builds the workspace, then bundles) on the branch and on the PR base ref (`main`, or the base branch of a stacked PR), compare the metafiles, and explain any growth in the PR description before CI's size report asks. (Added 2026-10-27, mongo-authoring-structure: both slices grew a client bundle and the cause was traced only after CI reported it.)

### QA-side items
Expand Down
2 changes: 1 addition & 1 deletion drive/pr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ A PR open against the wrong branch is recoverable but visible; this gate costs n

## Merge queue needs every review thread resolved

The `main` ruleset requires conversation resolution. A PR with auto-merge enabled and every check green still sits at `BLOCKED` while any review thread is unresolved, including CodeRabbit's. Before enabling auto-merge, list unresolved threads: `reviewThreads` takes no `isResolved` filter, so query `reviewThreads(first: 100) { pageInfo { hasNextPage endCursor } nodes { id isResolved } }` through the GraphQL API, page with `after:` while `hasNextPage` is true, and keep the nodes whose `isResolved` is false. Then reply to each with the fix commit or the reason for no change, and resolve them. Seen on #30350 and #30380 (2026-09-22/23).
The `main` ruleset requires conversation resolution. A PR with auto-merge enabled and every check green still sits at `BLOCKED` while any review thread is unresolved, including CodeRabbit's. Before enabling auto-merge, list the unresolved threads. `reviewThreads` takes no `isResolved` filter, so page through every thread and keep the unresolved ones: `gh api graphql --paginate -F number=<PR number> -f query='query($number:Int!,$endCursor:String){repository(owner:"prisma",name:"orm"){pullRequest(number:$number){reviewThreads(first:100,after:$endCursor){pageInfo{hasNextPage endCursor} nodes{id isResolved path}}}}}' --jq '.data.repository.pullRequest.reviewThreads.nodes[]|select(.isResolved==false)|"\(.id) \(.path)"'`. It prints nothing when every thread is resolved. Reply to each open thread with the fix commit or the reason for no change, and resolve it. Seen on #30350 and #30380 (2026-09-22/23).

## PR title convention

Expand Down
Loading
Loading