From 0a53f13548467d9c000f63960d77fbb960741709 Mon Sep 17 00:00:00 2001 From: willbot Date: Mon, 14 Sep 2026 16:49:34 +0200 Subject: [PATCH 01/15] docs: conventions for the data-modeling plain-language pass Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- .../c21-data-modeling/conventions.md | 24 +++++++++++++++++++ .../c21-data-modeling/page-conventions.md | 15 ++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 docs/orm-docs-audit/c21-data-modeling/conventions.md create mode 100644 docs/orm-docs-audit/c21-data-modeling/page-conventions.md diff --git a/docs/orm-docs-audit/c21-data-modeling/conventions.md b/docs/orm-docs-audit/c21-data-modeling/conventions.md new file mode 100644 index 00000000000..75c20b9c746 --- /dev/null +++ b/docs/orm-docs-audit/c21-data-modeling/conventions.md @@ -0,0 +1,24 @@ +# Shared conventions for the C21 plain-language pass (carried from the fundamentals and reference sections) + +Verified facts (rc.10 source at wip/prisma-src/, commit cfccb09be2). Use these words on every page; do not restate them differently. + +- "contract": on first use on a page, say: "your contract, the `contract.prisma` file that replaced `schema.prisma`". After that, "contract" alone. +- `db`: "`db` is the client you create once in `src/prisma/db.ts`; `prisma orm init` writes that file." Examples import it as `import { db } from "./prisma/db"` from a file directly inside `src/`. +- `db.orm.public.User`: "`db.orm` holds your models by model name; `public` is the PostgreSQL schema (the namespace your tables live in, `public` unless you set one)." On MongoDB there is no schema segment and the accessor is the collection name: `db.orm.users`. +- `db.sql.public.user`: the SQL query builder, keyed by table name. The table name is the model name with a lowercase first letter unless the model sets `@@map`. Column names are the field names unless `@map`. Say this once where `db.sql` first appears. +- `db.raw.sql`: raw SQL as a template string. `db.runtime()` is the connection that runs a built query: `db.runtime().query(plan)` for rows, `db.runtime().execute(plan)` for a count. On PostgreSQL `db.runtime()` is synchronous; on MongoDB it must be awaited: `(await db.runtime()).query(plan)`. State this where `db.runtime()` first appears on a page. +- "plan": avoid the word. Say "the built query" or show the code. Every raw or builder query ends with `.build()` and is then passed to `query()` or `execute()`. +- `.all()`, `.first()`, `.create(...)`: "you `await` the whole chain; the last call says what you want back". Do not use "terminal". +- `.first()` returns `null` when nothing matches. `.all().firstOrThrow()` throws an error with code `RUNTIME.NO_ROWS`. +- `emit`: "`prisma contract emit`, which you run after every change to the contract". Never a bare "emit". +- Scaffold command: `npm create prisma@latest -- my-app` (never `npx create-prisma@latest` or the bare package name). Bun: `bun create prisma@latest my-app`. +- Skills note at the bottom of pages: keep, but say "Projects created with `npm create prisma@latest` include the Prisma ORM skills for your coding agent". +- `conflictOn: { email: "..." }` takes a unique column with its value; if a row with that value exists, `update` is applied to it. Leave it out to use the primary key. +- Nested writes that work: `create`, `connect`, `disconnect` (`posts: (p) => p.create([...])`). `connectOrCreate`, nested update/upsert/delete, and `set` do not exist; link /orm/coming-from-prisma-orm-7#not-available-yet. +- Transaction options (`isolationLevel`, `timeout`, `maxWait`) and nested transactions do not exist. Prisma ORM 7's interactive `$transaction(async (tx) => ...)` maps directly to `db.transaction(async (tx) => ...)`. +- `create-prisma` mentions of "skills": "Prisma ORM skills, instruction files for coding agents". +- Type names in raw SQL (`pg/int4@1`): "the PostgreSQL type plus a version, always `@1` today"; prefer taking the type from the table: `db.sql.public.user.columns.id`. There is no published list; the raw-queries page shows the ids a bare JavaScript value gets. +- Errors: "an error with code `X`" (it is `error.code`). +- Do not use: terminal, codec, envelope, facade, lane, junction model/table (say "join table" / "model for the join table"), surface, plan, "rides", "fan out", "drop one level", "field proxy" (say "the `p` argument"), "lambda" (say "callback"). +- `npx prisma skills sync` exists in the published CLI (8.0.0-rc.13, newer than the rc.9 source tree): "in an existing project, run `npx prisma skills sync`". Do not drop it because the rc.9 tree lacks it. +- The serverless client (`postgresServerless(...)` from `@prisma/orm-postgres/serverless`) has no `db.orm`, `runtime()`, or `transaction()`; it runs SQL builder and raw queries only, reading rows 100 at a time. Never show `db.orm` on it. diff --git a/docs/orm-docs-audit/c21-data-modeling/page-conventions.md b/docs/orm-docs-audit/c21-data-modeling/page-conventions.md new file mode 100644 index 00000000000..e4a3f507dac --- /dev/null +++ b/docs/orm-docs-audit/c21-data-modeling/page-conventions.md @@ -0,0 +1,15 @@ +# Conventions for the orm/data-modeling pages (in addition to conventions.md) + +- These are explanation pages, not reference pages: Overview (models, primary keys, scalar fields, relations), Relational data modeling, MongoDB data modeling. Each one answers "how do I describe my data and why this way". Keep the heading order. Do not renumber, rename, or reorder headings; if a heading must change because it contains a banned term, pin the old anchor with `[#old-anchor]`. +- No commentary about the docs inside the docs. Cut sentences such as "This page introduces", "This page shows", "it is what this page focuses on", "the guides cover that in depth". Write the thing itself. +- Never interleave PostgreSQL and MongoDB sentence by sentence. Where both databases matter, give PostgreSQL its own sentences or block and then MongoDB its own. Tabs already on a page stay. +- The word "contract" is the subject of the contract-authoring section, not this one. On first use on a page: "your contract, the `contract.prisma` file that replaced `schema.prisma`". After that, "contract" alone. Do not use "schema" for the contract; "schema" alone means the PostgreSQL schema (`public`). +- "Join table": the model that holds one record per connected pair is "the model for the join table" on first use and "the join model" after. Never "junction", "junction model", "junction table", "junction record". +- Relation fields: "a relation field, typed as the other model" and "the field that stores the other record's primary key" (the foreign key column on SQL, the field holding the `_id` on MongoDB). "Back-relation" and "virtual" only with the plain meaning beside them ("stores nothing in the database"). +- `prisma contract emit` is the command that checks the contract. Say "`npx prisma contract emit` rejects it with an error whose `code` is `X`". Never "emit rejects", "the schema compiler", "the emitter lowers", "lowered". Referential actions: "become the foreign key's `ON DELETE` and `ON UPDATE` clauses". +- Banned on these pages in addition to the list: gate / gated; capability in prose; codec; canonical; deterministic; pure; "source of truth"; "escape hatch"; "purpose-built"; "first-class"; "wiring" / "wired"; "hangs off"; "consult"; "authoritative"; "physical" (write "in the database"); "idiomatic" (write "the usual choice" or "what MongoDB expects"); "denormalize" only with its meaning beside it ("keep a small copy of the fields a read needs"); "hot paths" (write "reads that run often"); "opaque"; "polymorphism" as a bare noun (write "a base model with variants"); "lifecycle"; "injected" (write "filled in for you"); "narrow" as a verb for `.variant()` only with "limit the query to one variant" beside it; "at the storage level" (write "in the table"); "rides along"; "lives on" / "lives inside" (write "is on", "is stored inside"). +- Types: the scalar table names Prisma types (`String`, `Int`, ...) and says what PostgreSQL column each becomes only where it matters (`Decimal`, `Bytes`, `Json`, `Jsonb`). Type ids such as `pg/text@1` do not belong on these pages. Version references to a pre-release ("before 0.17") do not belong on these pages; say what the type is now. +- Query examples on these pages are short and use the page's own models. The ORM client is `db.orm.public.User` on PostgreSQL and `db.orm.users` on MongoDB (see conventions.md). Link to `/orm/fundamentals/relations-and-joins` for querying; do not restate it. +- "Prompt your coding agent" sections: keep, but the first sentence reads "Projects created with `npm create prisma@latest` include the Prisma ORM skills for your coding agent. In an existing project, run `npx prisma skills sync`." Keep the example prompts. +- The source of truth is prisma/orm at 8.0.0-rc.10 (`cfccb09be2`), exported at `wip/prisma-src/`. rc.11 changed nothing in the contract language, so rc.10 facts hold for rc.11. +- The contract-authoring pages under `/orm/contract-authoring/` (PSL syntax, TypeScript builder, the artifacts) and the CLI pages under `/cli/` are out of scope; link to them, do not restate attribute lists or flags. From 7cdfd4bcd19cf6d9ccea74a68bd05c211db789c4 Mon Sep 17 00:00:00 2001 From: willbot Date: Mon, 14 Sep 2026 16:56:09 +0200 Subject: [PATCH 02/15] docs(orm): data-modeling banned-term pass and fact check against rc.10 Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- .../content/docs/orm/data-modeling/index.mdx | 62 +++++++------- .../docs/orm/data-modeling/mongodb.mdx | 22 ++--- .../data-modeling/relational-databases.mdx | 44 +++++----- .../c21-data-modeling/factcheck1.index.md | 80 ++++++++++++++++++ .../c21-data-modeling/factcheck1.mongodb.md | 53 ++++++++++++ .../factcheck1.relational-databases.md | 83 +++++++++++++++++++ .../c21-data-modeling/page-conventions.md | 2 +- 7 files changed, 281 insertions(+), 65 deletions(-) create mode 100644 docs/orm-docs-audit/c21-data-modeling/factcheck1.index.md create mode 100644 docs/orm-docs-audit/c21-data-modeling/factcheck1.mongodb.md create mode 100644 docs/orm-docs-audit/c21-data-modeling/factcheck1.relational-databases.md diff --git a/apps/docs/content/docs/orm/data-modeling/index.mdx b/apps/docs/content/docs/orm/data-modeling/index.mdx index 374d6709f1b..639161b9dc6 100644 --- a/apps/docs/content/docs/orm/data-modeling/index.mdx +++ b/apps/docs/content/docs/orm/data-modeling/index.mdx @@ -10,18 +10,16 @@ Data modeling is the process of describing the data your application needs and h For example, a blog has users, posts, and comments. A user has fields like an email and a name. A post has fields like a title and content. These models also relate to each other: a user can write many posts, and a post can have many comments. -In Prisma ORM, you define this structure in a `contract.prisma` file. This file becomes the shared contract between your application code, your database migrations, and your developer tools. +In Prisma ORM, you define this structure in your contract, the `contract.prisma` file that replaced `schema.prisma`. Prisma ORM derives your TypeScript types, your migrations, and the client's configuration from it. -If you are coming from Prisma ORM 7, `contract.prisma` plays a similar role to the `schema.prisma` file you used before. - -This page introduces the four building blocks of every Prisma ORM contract: +Every contract is built from four things: - [Models](#models): the things your application works with - [Primary keys](#primary-keys): how each record is identified - [Scalar fields](#scalar-fields): the values a model stores - [Relations](#relations): how models connect to each other -These building blocks apply to every database. Where databases differ is in how you model relations, and the [relational](/orm/data-modeling/relational-databases) and [MongoDB](/orm/data-modeling/mongodb) guides cover that in depth. +All four apply to every database. How you model relations is where databases differ, so continue with [relational data modeling](/orm/data-modeling/relational-databases) or [MongoDB data modeling](/orm/data-modeling/mongodb). ## Models @@ -35,13 +33,13 @@ model User { } ``` -Every record of a model has its own identity and its own lifecycle. Two posts with the same title are still two different posts, and a user who changes their email address is still the same user. +Every record of a model has its own identity and its own history. Two posts with the same title are still two different posts, and a user who changes their email address is still the same user. On a relational database, a model becomes a table. On MongoDB, it becomes a collection. ## Primary keys -A primary key is the field that uniquely identifies each record. Every model needs one. Mark it with `@id`: +A primary key is the field that uniquely identifies each record. Give every model one, because Prisma ORM needs a primary key to update or delete a record. Mark it with `@id`: ```prisma tab="PostgreSQL" model User { @@ -58,7 +56,7 @@ model User { } ``` -On MongoDB, the primary key maps to the document's mandatory `_id` field, and `ObjectId` is the idiomatic type for it. +On MongoDB, the primary key is the document's mandatory `_id` field, and it must be an `ObjectId`. ### Natural keys @@ -73,7 +71,7 @@ model Country { } ``` -Records that point at `Country` now store a readable value (`US`, `DE`) instead of an opaque number. Currencies (`USD`, `EUR`) and similar lookup tables work the same way. +Records that point at `Country` now store a readable value (`US`, `DE`) instead of a meaningless number. Currencies (`USD`, `EUR`) and similar lookup tables work the same way. ### Surrogate keys @@ -121,7 +119,7 @@ model ApiToken { A UUID is wider than an integer and random UUIDs index a little worse, but independent services never collide and nothing is leaked. ```prisma -// ObjectId: the idiomatic MongoDB key. Generated without +// ObjectId: the MongoDB key. Generated without // coordination and embeds its own creation time. model Post { id ObjectId @id @map("_id") @@ -142,25 +140,27 @@ model UserTag { } ``` -A duplicate `(userId, tagId)` pair would be meaningless, so the pair is the key. Avoid composite keys on ordinary models: everything that points at the model then has to carry all of the key's fields. +A duplicate `(userId, tagId)` pair would be meaningless, so the pair is the key. Avoid composite keys for ordinary models: everything that points at the model then has to store all of the key's fields. ## Scalar fields A scalar field holds a single value. The common types: -| Type | Stores | -| ---------- | --------------------------------------------------------------------------- | -| `String` | Text | -| `Int` | 32-bit integer | -| `BigInt` | 64-bit integer | -| `Float` | Floating-point number | -| `Decimal` | Exact decimal, PostgreSQL `numeric`. The runtime value is a decimal string, so binary floating point never loses digits. Write the type as `Numeric(10, 2)` when you want an explicit precision and scale; PostgreSQL then rounds stored values to that scale and rejects values that exceed the precision | -| `Boolean` | `true` / `false` | -| `DateTime` | Timestamp | -| `Bytes` | Binary data, PostgreSQL `bytea`. The runtime value is a `Uint8Array` | -| `Json` | Arbitrary JSON value, stored in PostgreSQL's native `json` | -| `Jsonb` | Arbitrary JSON value, stored in PostgreSQL's `jsonb`. This is what `field.json()` and value objects use, and what a `Json` field meant before 0.17 | -| `ObjectId` | MongoDB document identifier | +| Type | Stores | +| ---------- | ------------------------------------------------ | +| `String` | Text | +| `Int` | 32-bit integer | +| `BigInt` | 64-bit integer | +| `Float` | Floating-point number | +| `Decimal` | Exact decimal, PostgreSQL `numeric` | +| `Boolean` | `true` / `false` | +| `DateTime` | Timestamp, PostgreSQL `timestamptz` | +| `Bytes` | Binary data, PostgreSQL `bytea` | +| `Json` | Arbitrary JSON value, PostgreSQL's native `json` | +| `Jsonb` | Arbitrary JSON value, PostgreSQL's `jsonb` | +| `ObjectId` | MongoDB document identifier | + +A `Decimal` value reaches your code as a decimal string, so binary floating point never loses digits. Write the type as `Numeric(10, 2)` when you want an explicit precision and scale. A `DateTime` value reaches your code as a `Temporal.Instant`, and a `Bytes` value as a `Uint8Array`. `Jsonb` is also the storage the TypeScript builder's `field.json()` and value objects use. Filtering on a `Jsonb` field is whole-value equality only: you can ask whether the column equals a given document, not whether a key inside it matches. A `Json` field offers no comparison at all in the ORM filter. Reach into the document with [raw SQL](/orm/reference/raw-queries) when you need a path query. @@ -202,7 +202,7 @@ model Post { } ``` -When you are unsure between two sizes, lean toward the wider one (`BigInt` over `Int` for a counter that could grow). Widening a type later is a migration; the wider type today is free. +When you are unsure between two sizes, lean toward the wider one (`BigInt` over `Int` for a counter that could grow). Widening a type later is a migration. The wider type today is free. Mark a field optional (`?`) only when "absent" means something different from a sensible default. A required field with a default is often the clearer model. @@ -212,8 +212,8 @@ A relation connects two models: a user has many posts, a post belongs to one use Two kinds of field describe a relation: -- A field that stores the connection. This is a real column or document field, like `authorId`, holding the other record's primary key. -- A relation field, typed as the other model, like `author User`. It stores nothing itself; it tells Prisma ORM how to navigate the connection in queries. +- The field that stores the other record's primary key, like `authorId`. On a relational database this is the foreign key column, and on MongoDB it is the field holding the other document's `_id`. +- A relation field, typed as the other model, like `author User`. It stores nothing in the database. It tells Prisma ORM how to follow the connection in a query. The `@relation` attribute ties them together: `fields` names the local field that stores the link, and `references` names the field it points at on the other model. @@ -233,12 +233,12 @@ Relations come in three shapes: How each shape is stored, and which model should hold the connecting field, is where relational and document databases differ. Continue with the guide for your database: -- [Relational data modeling](/orm/data-modeling/relational-databases) for PostgreSQL and other SQL databases: foreign keys, junction tables, and which side owns the key. +- [Relational data modeling](/orm/data-modeling/relational-databases) for PostgreSQL and other SQL databases: foreign keys, the model for the join table, and which side owns the key. - [MongoDB data modeling](/orm/data-modeling/mongodb): embedding versus referencing, and polymorphic collections. ## Prompt your coding agent -Projects scaffolded with `create-prisma@latest` install [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8) for your coding agent; the `prisma-8` skill covers contract authoring. Prompts that map to each section: +Projects created with `npm create prisma@latest` include the [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8) for your coding agent. In an existing project, run `npx prisma skills sync`. The `prisma-8` skill covers contract authoring. Prompts that map to each section: - "Using the prisma-8 skill, add a Product model with a surrogate id and a unique sku field." - "Add a Country reference table keyed by its ISO code." @@ -248,5 +248,5 @@ Projects scaffolded with `create-prisma@latest` install [Prisma ORM skills](/ai/ ## Next steps - [Model relational data](/orm/data-modeling/relational-databases): one-to-one, one-to-many, many-to-many, and polymorphic relations on SQL databases. -- [Model MongoDB data](/orm/data-modeling/mongodb): embed or reference, and single-collection polymorphism. -- [Query your models](/orm/fundamentals/reading-data) once the schema is in place. +- [Model MongoDB data](/orm/data-modeling/mongodb): embed or reference, and polymorphic collections. +- [Query your models](/orm/fundamentals/reading-data) once your contract is in place. diff --git a/apps/docs/content/docs/orm/data-modeling/mongodb.mdx b/apps/docs/content/docs/orm/data-modeling/mongodb.mdx index 0f535271eb1..c9d5945d994 100644 --- a/apps/docs/content/docs/orm/data-modeling/mongodb.mdx +++ b/apps/docs/content/docs/orm/data-modeling/mongodb.mdx @@ -8,11 +8,11 @@ metaDescription: 'How to model data in Prisma ORM for MongoDB, including documen MongoDB stores data as documents grouped into collections. A document can nest objects and arrays directly, which gives you a real choice for related data: keep it inside the document (embed it) or store it in its own collection and link to it (reference it). -Making that choice well is the core of MongoDB data modeling, and it is what this page focuses on. If you are new to models and keys, start with the [data modeling overview](/orm/data-modeling). +Making that choice well is the core of MongoDB data modeling. If you are new to models and keys, start with the [data modeling overview](/orm/data-modeling). ## Documents, collections, and the _id key -A Prisma ORM model maps to a collection, and each record is a document. Every document has an `_id` field as its primary key. `ObjectId` is the idiomatic type for it: +A Prisma ORM model maps to a collection, and each record is a document. Every document has an `_id` field as its primary key. `ObjectId` is the type MongoDB expects for it: ```prisma model User { @@ -23,13 +23,13 @@ model User { } ``` -`@map("_id")` maps the model's `id` field to MongoDB's mandatory `_id` field, and `@@map("users")` names the collection. +`@map("_id")` maps the model's `id` field to MongoDB's mandatory `_id` field, and `@@map("users")` names the collection. `@@map` is optional. Without it the collection takes the model name with a lowercase first letter, so `user`. ## Embed or reference Two pieces of related data can live together in one document (embedded) or in separate collections linked by an id (referenced). -Embedding nests the data inside its parent: an order and its line items in one document. One read returns everything, and a write updates parent and children together, atomically. The trade-off is that embedded data has no independent life. You cannot query it on its own, and it rides along on every read of the parent. +Embedding nests the data inside its parent: an order and its line items in one document. One read returns everything, and a write updates parent and children together, atomically. The trade-off is that embedded data has no independent life. You cannot read it without its parent, and it comes back on every read of the parent. Referencing keeps the data in its own collection and stores the linked document's `_id`. Loading both takes a second lookup, but each record stands on its own: it can be queried, listed, and updated independently, and it is not duplicated when several parents point at it. @@ -48,7 +48,7 @@ A blog post's comments make it concrete. If you always render them with the post ## Embedded documents -Describe the shape of embedded data with a `type` block, then use it as a field. An embedded type has no collection of its own; it exists only inside its parent document. +Describe the shape of embedded data with a `type` block, then use it as a field. An embedded type has no collection of its own. It exists only inside its parent document. ```prisma type Address { @@ -82,9 +82,9 @@ model Cart { } ``` -`Cart.items` lives inside the cart document. There is no separate collection and no join to load the items; every read of a cart returns them. +`Cart.items` is stored inside the cart document. There is no separate collection and no join to load the items. Every read of a cart returns them. -An embedded `Address` has no `_id` and cannot be queried on its own. It is loaded, updated, and deleted with the document that holds it. That is the right behavior for values that belong to their parent, and the wrong one for records with their own life. +An embedded `Address` has no `_id` and no collection of its own, so you cannot read it without its parent. It is loaded, updated, and deleted with the document that holds it. You can still match on its fields through the parent, with a path such as `address.city` in the [pipeline builder](/orm/fundamentals/advanced-queries#mongodb-pipeline-builder). That is the right behavior for values that belong to their parent, and the wrong one for records with their own life. :::warning @@ -115,11 +115,11 @@ model Post { This is a one-to-many by reference: many posts point at one user, and each post is queryable on its own. When you [include the relation in a query](/orm/fundamentals/relations-and-joins), Prisma ORM resolves it with a `$lookup` aggregation. -A `$lookup` is a real cost on hot paths, so document models often denormalize: embed a small copy of the fields a read needs (a comment stores its author's name) while the full record stays in its own collection. You trade some duplication, and the work of keeping the copy in sync, for reads that touch one document. +A `$lookup` is a real cost on reads that run often, so document models often keep a small copy of the fields a read needs inside the parent. A comment stores its author's name, while the full author record stays in its own collection. You trade some duplication, and the work of keeping the copy in sync, for reads that touch one document. ## Polymorphic collections -MongoDB collections do not enforce a single shape, so it is idiomatic to store documents of different kinds in one collection and tell them apart with a discriminator field. +MongoDB collections do not enforce a single shape. The usual choice is to store documents of different kinds in one collection and tell them apart with a discriminator field. Prisma ORM models this with a base model plus variant models, all sharing one collection: @@ -147,13 +147,13 @@ model Tutorial { } ``` -`@@discriminator(kind)` names the field that records each document's variant. `@@base(Post, "article")` declares `Article` as the `"article"` variant of `Post`. A query for posts returns articles and tutorials together, and you can narrow to one variant in queries. +`@@discriminator(kind)` names the field that records each document's variant. `@@base(Post, "article")` declares `Article` as the `"article"` variant of `Post`. A query for posts returns articles and tutorials together. `db.orm.posts.variant("Article")` limits the query to one variant. Use one polymorphic collection when the variants are handled together far more than separately: a notifications collection of email, SMS, and push messages read as one stream. Prefer separate collections when the types rarely appear in the same query or need very different indexes. ## Prompt your coding agent -Projects scaffolded with `create-prisma@latest` install [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8) for your coding agent; the `prisma-8` skill covers document modeling. Prompts that map to each section: +Projects created with `npm create prisma@latest` include the [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8) for your coding agent. In an existing project, run `npx prisma skills sync`. The `prisma-8` skill covers document modeling. Prompts that map to each section: - "Using the prisma-8 skill, add an embedded Address type to the User model." - "Cart items are always read with the cart. Model them as an embedded list." diff --git a/apps/docs/content/docs/orm/data-modeling/relational-databases.mdx b/apps/docs/content/docs/orm/data-modeling/relational-databases.mdx index 2c925ac076e..c2e3ea434ae 100644 --- a/apps/docs/content/docs/orm/data-modeling/relational-databases.mdx +++ b/apps/docs/content/docs/orm/data-modeling/relational-databases.mdx @@ -8,11 +8,11 @@ metaDescription: 'How to model one-to-one, one-to-many, many-to-many, and polymo In a relational database, each model becomes a table and each scalar field becomes a column. Models connect through foreign keys: a column in one table that holds the primary key of a row in another table. -This page shows how to model each relation shape and, for each one, where the foreign key belongs. If you are new to models and keys, start with the [data modeling overview](/orm/data-modeling). +If you are new to models and keys, start with the [data modeling overview](/orm/data-modeling). ## How relations are declared -The model that stores the connection carries two fields: a scalar field for the foreign key column, and a relation field that navigates it. `@relation(fields:, references:)` ties them together: +The model that stores the connection has two fields. One is a scalar field for the foreign key column. The other is a relation field, typed as the other model. `@relation(fields:, references:)` ties them together: ```prisma model Post { @@ -22,7 +22,7 @@ model Post { } ``` -The side that carries the `fields` argument owns the foreign key. Which side that should be is the main decision in the shapes below. +The side that declares the `fields` argument owns the foreign key. Which side that should be is the main decision in the shapes below. ## One-to-many @@ -43,11 +43,11 @@ model Post { } ``` -The foreign key lives on the "many" side, `Post.authorId`, because each post points at exactly one user, while a user points at an open-ended list of posts. The `posts Post[]` field on `User` is the back-relation: it stores nothing in the database and is inferred from the foreign key on `Post`. +The foreign key is on the "many" side, `Post.authorId`, because each post points at exactly one user, while a user points at an open-ended list of posts. The `posts Post[]` field on `User` is the back-relation: it stores nothing in the database and is matched to the foreign key on `Post`. Use a one-to-many whenever a record belongs to one parent and a parent has many children: comments on a post, line items on an order, employees in a department. -Because the list field is virtual, you never write to it. To connect a post to a user, set the foreign key: +To connect a post to a user, set the foreign key when you create the post: ```typescript const post = await db.orm.public.Post.create({ @@ -78,13 +78,13 @@ model Profile { } ``` -Declare the relation on the side that holds the foreign key (`Profile` here). You can add the mirror field on the other model as well (`profile Profile?` on `User`); a bare optional model-typed field is enough, and it needs no `@relation` of its own. The foreign key it mirrors must be unique, or emit rejects the field with `PSL_NON_UNIQUE_BACKRELATION`. With the mirror field in place, [the relation reads from either side](/orm/fundamentals/relations-and-joins#one-to-one). +Declare the relation on the side that holds the foreign key, `Profile` here. You can add the mirror field on the other model as well: `profile Profile?` on `User`. A bare field typed as the other model is enough, and it needs no `@relation` of its own. It has to be optional, because nothing in the database guarantees a matching profile row. The foreign key it mirrors must be unique, or `npx prisma contract emit` rejects the field with an error whose `code` is `PSL_NON_UNIQUE_BACKRELATION`. With the mirror field in place, [the relation reads from either side](/orm/fundamentals/relations-and-joins#one-to-one). ### Which side owns the foreign key Put the foreign key on the dependent side: the record that cannot exist on its own. -A profile needs a user. A user does not need a profile. So `Profile` carries `userId`: +A profile needs a user. A user does not need a profile. So `Profile` holds `userId`: ```prisma model Profile { @@ -108,7 +108,7 @@ If neither side is clearly dependent, put the key on the side you query from les A many-to-many relation (`m:n`) connects many records on each side: a post has many tags, and a tag applies to many posts. -A single foreign key cannot express this, because each side needs to point at many records. The answer is a junction model: a third model that holds one record per connected pair. +A single foreign key cannot express this, because each side needs to point at many records. The answer is the model for the join table: a third model that holds one record per connected pair. ```prisma model Post { @@ -133,7 +133,7 @@ model PostTag { } ``` -`PostTag` is the model for the join table, and it is two one-to-many relations back to back. Its composite primary key, `@@id([postId, tagId])`, enforces one record per pair. The list fields on `Post` and `Tag` name the far side (`Tag[]`, `Post[]`), and the schema compiler connects them through `PostTag`, so queries read `post.tags` directly. +`PostTag` is the model for the join table, and it is two one-to-many relations back to back. Its composite primary key, `@@id([postId, tagId])`, enforces one record per pair. The list fields on `Post` and `Tag` name the far side (`Tag[]`, `Post[]`), and `npx prisma contract emit` connects them through `PostTag`, so queries read `post.tags` directly. Connect a post to a tag with a nested write on the list field, or create the `PostTag` record yourself. Both write the same row, so pick one per pair: running both inserts the same composite key twice, and the second fails. @@ -143,11 +143,11 @@ await db.orm.public.Post.where({ id: post.id }).update({ tags: (t) => t.connect([{ id: tag.id }]), }); -// Or, equivalently, create the junction record yourself +// Or create the join record yourself await db.orm.public.PostTag.create({ postId: post.id, tagId: tag.id }); ``` -The junction is an ordinary model, so it can carry data about the pairing itself. When the tag was added, who added it, a sort order: add the field to the junction model. +`PostTag` is an ordinary model, so it can hold data about the pairing itself. When the tag was added, who added it, a sort order: add the field to `PostTag`. ```prisma model PostTag { @@ -161,11 +161,11 @@ model PostTag { } ``` -An implicit many-to-many (list fields on both sides with no junction model, as in Prisma ORM 7) is not supported yet: the schema compiler rejects it and asks for an explicit join model. Model the junction explicitly; [Relations and joins](/orm/fundamentals/relations-and-joins#many-to-many) shows how to traverse it in one query. +An implicit many-to-many, with list fields on both sides and no model for the join table, as in Prisma ORM 7, is not supported. `npx prisma contract emit` rejects it with an error whose `code` is `PSL_ORPHANED_BACKRELATION`, and the message tells you to add an explicit model for the join table. Declare that model yourself. [Relations and joins](/orm/fundamentals/relations-and-joins#many-to-many) shows how to read across it in one query. ## Polymorphic relations -Sometimes several kinds of record share a common core but each carries its own extra fields: every `Task` has a title, but a `Bug` also has a severity and a `Feature` has a target release. +Sometimes several kinds of record share a common core but each has its own extra fields: every `Task` has a title, but a `Bug` also has a severity and a `Feature` has a target release. Prisma ORM models this with a base model plus variant models. A discriminator field records which variant each row is: @@ -195,32 +195,32 @@ model Feature { `@@discriminator(type)` marks the field that records the variant. `@@base(Task, "bug")` declares that `Bug` is the `"bug"` variant of `Task`. -Variants appear as their own models in queries (`db.orm.public.Bug` alongside `db.orm.public.Task`), and a query on the base model returns every variant. To write a variant, narrow the base collection with `.variant(...)` and create through it; the discriminator is injected for you: +Variants appear as their own models in queries, `db.orm.public.Bug` alongside `db.orm.public.Task`. A query on the base model returns every variant. To write a variant, limit the query to one variant with `.variant(...)` and create through it. The discriminator value is filled in for you: ```typescript await db.orm.public.Task.variant("Bug").create({ title: "Crash on save", severity: "high" }); ``` -Chaining `.variant(...)` on a read narrows the same way, applying the discriminator filter and giving the collection the variant's row shape. +On a read, `.variant(...)` works the same way. It limits the query to one variant by filtering on the discriminator, and the rows come back with that variant's fields. Each variant picks one of two storage layouts: -- A variant with no `@@map`, like `Bug`, shares the base table. Its extra columns live alongside the base columns and must be nullable at the storage level, because a `Feature` row has no severity. -- A variant with its own `@@map`, like `Feature`, gets its own table holding only its extra columns, linked back to the base table's primary key. Its columns keep their constraints, at the cost of a join to read a full variant. +- A variant with no `@@map`, like `Bug`, shares the base table. Its extra columns are added to the base table and have to be nullable there, because a `Feature` row has no severity. +- A variant with its own `@@map`, like `Feature`, gets its own table. That table holds its extra columns plus the base model's primary key, which links it back to the base table. Its columns keep their constraints, at the cost of a join to read a full variant. -Use polymorphism when you query the variants together (one feed of tasks, one stream of events) and each variant carries real structure of its own. If you never query them together, separate models are simpler. If the variants differ by one nullable field, a single model is simpler still. +Use a base model with variants when you query the variants together (one feed of tasks, one stream of events) and each variant has real structure of its own. If you never query them together, separate models are simpler. If the variants differ by one nullable field, a single model is simpler still. ## Prompt your coding agent -Projects scaffolded with `create-prisma@latest` install [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8) for your coding agent; the `prisma-8` skill covers relation modeling. Prompts that map to each section: +Projects created with `npm create prisma@latest` include the [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8) for your coding agent. In an existing project, run `npx prisma skills sync`. The `prisma-8` skill covers relation modeling. Prompts that map to each section: - "Using the prisma-8 skill, add a one-to-many between User and Post with the foreign key on Post." - "Make the Profile relation one-to-one by adding a unique constraint to its foreign key." -- "Model a many-to-many between Post and Tag with an explicit junction model and an addedAt timestamp." +- "Model a many-to-many between Post and Tag with an explicit model for the join table and an addedAt timestamp." - "Split Task into Bug and Feature variants with a discriminator field." ## Next steps -- [Query relations](/orm/fundamentals/relations-and-joins) with `.include(...)`, relation predicates, and junction traversal. +- [Query relations](/orm/fundamentals/relations-and-joins) with `.include(...)`, filters on relation data, and reads across the join table. - To model for MongoDB, see the [MongoDB data modeling guide](/orm/data-modeling/mongodb). -- Referential actions work today: add `onDelete` and `onUpdate` to `@relation` with `Cascade`, `Restrict`, `SetNull`, `SetDefault`, or `NoAction`, as in `@relation(fields: [userId], references: [id], onDelete: Cascade)`, and the emitter lowers them to the foreign key's `ON DELETE` / `ON UPDATE` clause. +- Referential actions: add `onDelete` and `onUpdate` to `@relation` with `Cascade`, `Restrict`, `SetNull`, `SetDefault`, or `NoAction`, as in `@relation(fields: [userId], references: [id], onDelete: Cascade)`. They become the foreign key's `ON DELETE` and `ON UPDATE` clauses. diff --git a/docs/orm-docs-audit/c21-data-modeling/factcheck1.index.md b/docs/orm-docs-audit/c21-data-modeling/factcheck1.index.md new file mode 100644 index 00000000000..29e6fabb95e --- /dev/null +++ b/docs/orm-docs-audit/c21-data-modeling/factcheck1.index.md @@ -0,0 +1,80 @@ +Done. Page: `/Users/will/Projects/prisma/web/.claude/worktrees/pr-conflicts-review-fb93f7/apps/docs/content/docs/orm/data-modeling/index.mdx` + +## 1. Wording changes (banned terms + page conventions) + +| Line | Old | New | +|---|---|---| +| 13 | "you define this structure in a `contract.prisma` file. This file becomes the shared contract between your application code, your database migrations, and your developer tools." | "you define this structure in your contract, the `contract.prisma` file that replaced `schema.prisma`. Prisma ORM derives your TypeScript types, your migrations, and the client's configuration from it." | +| 15 | "If you are coming from Prisma ORM 7, `contract.prisma` plays a similar role to the `schema.prisma` file you used before." | deleted (the required first-use phrasing now covers it) | +| 17 | "This page introduces the four building blocks of every Prisma ORM contract:" | "Every contract is built from four things:" | +| 24 | "...the [relational] and [MongoDB] guides cover that in depth." | "All four apply to every database. How you model relations is where databases differ, so continue with [relational data modeling] or [MongoDB data modeling]." | +| 38 | "its own lifecycle" | "its own history" | +| 61 | "`ObjectId` is the idiomatic type for it" | "it must be an `ObjectId`" | +| 76 | "an opaque number" | "a meaningless number" | +| 124 (code comment) | "the idiomatic MongoDB key" | "the MongoDB key" | +| 143 | "Avoid composite keys on ordinary models ... has to carry all of the key's fields" | "Avoid composite keys for ordinary models ... has to store all of the key's fields" (`keys on` and `carry` both hit the checker) | +| 205 | "Widening a type later is a migration; the wider type today is free." | split into two sentences | +| 215–216 | "a real column or document field"; "It stores nothing itself; it tells Prisma ORM how to navigate" | "The field that stores the other record's primary key..."; "It stores nothing in the database. It tells Prisma ORM how to follow the connection in a query." | +| 236 | "junction tables" | "the model for the join table" | +| 241 | "Projects scaffolded with `create-prisma@latest` install..." | "Projects created with `npm create prisma@latest` include the [Prisma ORM skills](...) for your coding agent. In an existing project, run `npx prisma skills sync`." | +| 251 | "single-collection polymorphism" | "polymorphic collections" (matches the target page's heading) | +| 252 | "once the schema is in place" | "once your contract is in place" | + +No heading was renamed, so no anchors were pinned. + +## 2. Claims checked + +| Line | Claim | Source | Verdict | +|---|---|---|---| +| 13 | contract is the shared contract for app code, migrations, tools | `skills/prisma-8/references/contract.md:5` ("derives types, migrations, and runtime configuration") | **Corrected** → now names TypeScript types, migrations, client configuration | +| all | examples need `// use prisma-8` | `packages/1-framework/3-tooling/language-server/src/schema-directive.ts:1`; no reference in `psl-parser/src/` or either `contract-psl/src/` | **Confirmed** (not needed). It is a language-server marker. The contract-authoring pages show it only on whole-file blocks (`psl-syntax.mdx:15,59`); this page's blocks are fragments, matching that convention. No change. | +| 32,48 | `Int @id @default(autoincrement())` | `cli/src/commands/init/templates/code-templates.ts:106,172` | Confirmed | +| 32 | `autoincrement()` exists as a default function | `3-targets/6-adapters/postgres/src/core/control-mutation-defaults.ts:131-137` | Confirmed | +| — | what `autoincrement()` becomes on PostgreSQL | `postgres/src/core/migrations/planner-ddl-builders.ts:61-71` → `SERIAL` (int4), `BIGSERIAL`, `SMALLSERIAL`; no `DEFAULT` clause | Confirmed. Page's "the database assigns the value, so you only know it after the insert" holds. | +| 34 | `String?` optional modifier | `code-templates.ts:172-180` | Confirmed | +| 55,127 | `ObjectId @id @map("_id")` | `2-mongo-family/2-authoring/contract-psl/src/interpreter.ts:1322-1335` (model *must* declare it) | Confirmed | +| 57,129 | `@@map("users")` for the collection | `mongo-attribute-specs.ts:143,249`; `code-templates.ts:196-210` | Confirmed | +| 44 | "Every model needs one" | SQL: `contract-psl/src/interpreter.ts:763` allows no primary key, but `sql-orm-client/src/collection.ts:2422` — update/delete require a primary key or unique. MongoDB: required. | **Corrected** → "Give every model one, because Prisma ORM needs a primary key to update or delete a record." | +| 61 | MongoDB primary key is `_id`, type `ObjectId` | `mongo interpreter.ts:1327-1335` | Confirmed (strengthened from "idiomatic" to "must be") | +| 71 | `String @id` (natural key) | `contract-psl/src/psl-field-resolution.ts:642,651` (only lists and optionals are rejected) | Confirmed | +| 89,94 | `String @unique` | `code-templates.ts:173` | Confirmed | +| 117 | `String @id @default(uuid())` | `control-mutation-defaults.ts:140-141` (`uuid()`, `uuid(4)`, `uuid(7)`); `1-framework/2-authoring/ids/src/index.ts:62-64` — `uuidv4` applies to `pg/text@1` | Confirmed | +| 117 | UUID "generated before the insert" | `control-mutation-defaults.ts:73-80` returns an *execution* generator, not a column default | Confirmed | +| 141 | `@@id([userId, tagId])` | `interpreter.ts:944-953`; `examples/prisma-8-demo/.../contract.prisma` `PostTag` | Confirmed | +| 172 | `String[]` scalar list on PostgreSQL | `psl-field-resolution.ts:497-500` requires the `scalarList` capability; `3-targets/6-adapters/postgres/src/core/adapter.ts:38` and `descriptor-meta.ts:192` report `scalarList: true` | Confirmed | +| 151 | `String` → text | `control-mutation-defaults.ts:166-169` | Confirmed | +| 152 | `Int` 32-bit | same, `pg/int4@1` / `int4` | Confirmed | +| 153 | `BigInt` 64-bit | same, `pg/int8@1` / `int8` | Confirmed | +| 154 | `Float` | same, `pg/float8@1` / `float8` | Confirmed | +| 155 | `Decimal` = PostgreSQL `numeric`, value is a decimal string | `control-mutation-defaults.ts:184-187`; `postgres/src/core/codecs.ts:896-901` (app type `string`) | Confirmed | +| 155 | `Numeric(10, 2)` written as the type | `control-mutation-defaults.ts:228-244` (`Numeric` native type, `precision`/`scale` args) | Confirmed | +| 155 | PostgreSQL rounds to scale and rejects over precision | Not in the Prisma source (PostgreSQL behaviour) | **Q1** — kept, see below | +| 156 | `Boolean` | `control-mutation-defaults.ts:170-173` (`pg/bool@1` / `bool`) | Confirmed | +| 157 | `DateTime` = timestamp | `control-mutation-defaults.ts:188-191` → `pg/timestamptz-temporal@1` / `timestamptz`; `temporal-codecs.ts:139-143` app type `Temporal.Instant` | **Corrected** → cell now says "Timestamp, PostgreSQL `timestamptz`" and the note says the value reaches your code as a `Temporal.Instant` | +| 158 | `Bytes` = `bytea`, value is `Uint8Array` | `codecs.ts:1164-1178` | Confirmed | +| 159 | `Json` = native `json` | `control-mutation-defaults.ts:192-195` | Confirmed | +| 160 | `Jsonb` = `jsonb`, used by `field.json()` and value objects | `control-mutation-defaults.ts:196-199`; `postgres/src/core/authoring.ts:721-726` (`json` field preset → `pg/jsonb@1`); `6-adapters/postgres/src/exports/control.ts:15` `valueObjectStorageType: 'Jsonb'` | Confirmed | +| 160 | "what a `Json` field meant before 0.17" | page-conventions bans pre-release version references | **Corrected** → clause removed | +| 161 | `ObjectId` = MongoDB document identifier | `mongo contract-psl/src/interpreter.ts:981` | Confirmed | +| 165 | `Jsonb` filters by whole-value equality; `Json` has no comparison | `postgres/src/core/codecs.ts:1417-1421` (`Jsonb` traits `['equality']`) and `1373-1377` (`Json` traits `[]`); `sql-orm-client/src/types.ts:361-395` maps `eq`/`neq`/`in`/`notIn` to `equality`, `gt`/`lt`/… to `order`, `like` to `textual`; `sql-orm-client/src/filters.ts:81-85` throws without the trait | Confirmed | +| 167 | extensions add types such as vectors or geometry | `examples/prisma-8-demo/.../contract.prisma` `pgvector.Vector(1536)`; `examples/prisma-8-postgis-demo/.../contract.prisma` `postgis.Geometry(4326)` | Confirmed | +| 171 | `?` optional | `code-templates.ts:173-175` | Confirmed | +| 218,224 | `@relation(fields:, references:)` and what the two arguments mean | `code-templates.ts:186-188`; `2-sql/2-authoring/contract-psl/src/psl-relation-resolution.ts:431-432` | Confirmed | +| 228-232 | relations come in three shapes, incl. many-to-many | `psl-relation-resolution.ts:308-323,400-418` — many-to-many is recognised, but only through an explicit join model whose `@@id([...])` lists exactly its two foreign-key columns; implicit many-to-many is rejected with `PSL_ORPHANED_BACKRELATION` ("or use an explicit join model for many-to-many") | Confirmed as a shape. The requirement is carried by the wording change on line 236 ("the model for the join table") and belongs to the relational page. | +| 241 | `create-prisma@latest` scaffolds and installs skills | conventions.md mandates `npm create prisma@latest`; `ai/tools/skills.mdx:104` | **Corrected** to the mandated sentence | +| 241 | `prisma-8` skill covers contract authoring | `wip/prisma-src/skills/` contains exactly one skill, `prisma-8`, with `references/contract.md` | Confirmed | +| 241 | `/ai/tools/skills#available-skills-for-prisma-8` | `apps/docs/content/docs/ai/tools/skills.mdx:100` declares `[#available-skills-for-prisma-8]` | Confirmed | +| 24,236,250 | `/orm/data-modeling/relational-databases` | file exists, `url:` matches | Confirmed | +| 24,237,251 | `/orm/data-modeling/mongodb` | file exists, `url:` matches | Confirmed | +| 165 | `/orm/reference/raw-queries` | file exists, `url:` matches | Confirmed | +| 252 | `/orm/fundamentals/reading-data` | file exists, `url:` matches | Confirmed | + +## 3. Q list + +**Q1 (line 155, now in the note under the table):** "PostgreSQL then rounds stored values to that scale and rejects values that exceed the precision." This is PostgreSQL server behaviour, not Prisma behaviour, and the Prisma source neither states nor contradicts it. Searches run inside `wip/prisma-src`: `grep -rn "precision" packages/3-targets/6-adapters/postgres/src/core/control-mutation-defaults.ts` (only the arg schema, `minimum: 1`/`minimum: 0`), `grep -rn "class PgNumericCodec" -A 14 packages/3-targets/3-targets/postgres/src/core/codecs.ts` (encode passes the string through unchanged, no rounding), `grep -rn "numeric" packages/3-targets/3-targets/postgres/src/core/psl-infer/postgres-type-map.ts`. Left in the page for the operator to decide. + +Also worth flagging, not a Q: `Decimal` and `DateTime` have no entry in `postgres-type-map.ts`, which is the *introspection* direction only (`numeric` → `Numeric`, `timestamptz` → `Timestamptz`). The authoring direction in `control-mutation-defaults.ts` does define both, which is what the page describes. + +## 4. Line count + +252 before, 252 after. `check-plain.sh` prints `plain-language check: clean`, and the extra page-conventions word list has no hits. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/factcheck1.mongodb.md b/docs/orm-docs-audit/c21-data-modeling/factcheck1.mongodb.md new file mode 100644 index 00000000000..bf8e62407b2 --- /dev/null +++ b/docs/orm-docs-audit/c21-data-modeling/factcheck1.mongodb.md @@ -0,0 +1,53 @@ +Done. Page: `/Users/will/Projects/prisma/web/.claude/worktrees/pr-conflicts-review-fb93f7/apps/docs/content/docs/orm/data-modeling/mongodb.mdx` + +## 1. Wording replacements + +| Line | Old | New | +| --- | --- | --- | +| 11 | "…is the core of MongoDB data modeling, and it is what this page focuses on." | "…is the core of MongoDB data modeling." (commentary about the docs cut) | +| 15 | "`ObjectId` is the idiomatic type for it" | "`ObjectId` is the type MongoDB expects for it" | +| 32 | "You cannot query it on its own, and it rides along on every read of the parent." | "You cannot read it without its parent, and it comes back on every read of the parent." | +| 51 | semicolon joining two clauses | split into two sentences | +| 85 | "`Cart.items` lives inside the cart document… items; every read" | "`Cart.items` is stored inside the cart document… items. Every read" | +| 118 | "a real cost on hot paths, so document models often denormalize: embed a small copy…" | "a real cost on reads that run often, so document models often keep a small copy of the fields a read needs inside the parent. A comment stores its author's name, while the full author record stays in its own collection." | +| 122 | "so it is idiomatic to store documents of different kinds…" | "The usual choice is to store documents of different kinds…" | +| 150 | "you can narrow to one variant in queries" | "`db.orm.posts.variant(\"Article\")` limits the query to one variant." | +| 156 | "Projects scaffolded with `create-prisma@latest` install…" | "Projects created with `npm create prisma@latest` include the [Prisma ORM skills](…) for your coding agent. In an existing project, run `npx prisma skills sync`." | + +No headings renamed, no anchors changed. + +## 2. Claims checked + +| Line | Claim | Source | Verdict | +| --- | --- | --- | --- | +| 15–24 | PSL example needs no `// use prisma-8` or provider marker | `apps/docs/content/docs/orm/contract-authoring/psl-syntax.mdx:16,60` puts the marker only on the two full-contract tab examples, not on fragments | Confirmed | +| 19 | `id ObjectId @id @map("_id")` | `wip/prisma-src/examples/mongo-demo/src/contract.prisma`; `psl-syntax.mdx:157-165` | Confirmed | +| 22 | `@@map("users")` names the collection | `2-mongo-family/2-authoring/contract-psl/src/interpreter.ts:240-254` | Confirmed | +| 26 | `@@map` requirement / default collection name | same, `return name ?? lowerFirst(model.name)`; `psl-helpers.ts:6-9` | **Corrected** — now says "`@@map` is optional. Without it the collection takes the model name with a lowercase first letter, so `user`." | +| 32 | embedded data cannot be read on its own | see line 87 evidence | Corrected (wording; see below) | +| 51–67 | `type Address` used as `address Address?` | `examples/mongo-demo/src/contract.prisma`, identical model | Confirmed | +| 71–83 | `type CartItem` used as `items CartItem[]` | `contract-psl/test/derive-json-schema.test.ts` "handles value object array field"; `mongo-attribute-specs.ts` allows list value-object fields | Confirmed | +| 87 | "An embedded `Address` has no `_id` and cannot be queried on its own" | no `_id`: `derive-json-schema.test.ts:373` asserts `expect(nestedProps).not.toHaveProperty('_id')`. Filtering: the pipeline builder **does** accept paths into embedded documents — `5-query-builders/query-builder/test/field-accessor.test-d.ts:70,108,216` (`f('address.city').eq('London')`); the ORM `.where()` object form keys are top-level model fields only (`orm/src/types.ts:188-202`) | **Corrected** — "has no `_id` and no collection of its own, so you cannot read it without its parent… You can still match on its fields through the parent, with a path such as `address.city` in the pipeline builder." | +| 91 | 16 MB document limit | general MongoDB fact, attributed to MongoDB not Prisma ORM | Confirmed (kept) | +| 97, 111 | `@relation(fields:, references:)` on MongoDB | `2-mongo-family/2-authoring/contract-psl/src/mongo-attribute-specs.ts:148-156` | Confirmed | +| 100–113 | `authorId ObjectId`, `posts Post[]` back-relation | `examples/mongo-blog-leaderboard/src/contract.prisma` — same models, same field names | Confirmed | +| 116 | include resolves with `$lookup` | `2-mongo-family/5-query-builders/orm/src/collection.ts:97` ("Adds a `$lookup` for a reference relation"); `skills/prisma-8/references/queries-mongo.md:21` | Confirmed | +| 118 | denormalization advice | general advice; does not contradict the ORM | Confirmed (reworded only) | +| 127–148 | `@@discriminator(kind)`, `@@base(Post, "article")`, `kind String` on the base | `examples/mongo-blog-leaderboard/src/contract.prisma` and `mongo-demo` carry exactly this shape | Confirmed | +| 124, 150 | all variants share one collection | `interpreter.ts:472-482` sets each variant's `storage.collection` to the base's; `interpreter.ts:462-468` rejects a variant `@@map` naming a different collection with `PSL_MONGO_VARIANT_SEPARATE_COLLECTION` | Confirmed | +| 150 | a base query returns all variants | `collection.ts:203-232` — the discriminator filter is added only by `.variant()`, never by the base collection | Confirmed | +| 150 | `.variant(...)` exists, takes the variant **model name** | `collection.ts:83-86,203-232`; `orm/test/integration/polymorphism.test.ts:100` (`orm.tasks.variant('Bug').all()`); `queries-mongo.md:57` (`db.orm.posts.variant('Article').all()`) | Confirmed | +| — | does `@@map` on a variant mean anything on MongoDB | `interpreter.ts:462-468` — only permitted if it names the base's collection, otherwise an error. The page's variants carry no `@@map`, matching the real contracts | Confirmed (no page change needed) | +| 156 | `create-prisma@latest` and the skills link | `conventions.md` scaffold command; `apps/docs/content/docs/ai/tools/skills.mdx:100` pinned anchor `[#available-skills-for-prisma-8]` | **Corrected** — scaffold command and first sentence per page-conventions | +| 156 | "the `prisma-8` skill covers document modeling" | `skills/prisma-8/references/queries-mongo.md`, `contract.md` | Confirmed | +| 165–167 | link targets | all files exist: `orm/data-modeling/index.mdx`, `fundamentals/relations-and-joins.mdx`, `fundamentals/reading-data.mdx`, `fundamentals/advanced-queries.mdx` (heading `## MongoDB: Pipeline builder` → `#mongodb-pipeline-builder`), `data-modeling/relational-databases.mdx` | Confirmed | + +## 3. Q list + +None. Every claim resolved against the source. + +## 4. Line count + +167 before, 167 after. + +`check-plain.sh` prints "clean". The extra page-conventions word grep is clear (the single hit is "Aggregate", a false positive on the `gate` substring). \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/factcheck1.relational-databases.md b/docs/orm-docs-audit/c21-data-modeling/factcheck1.relational-databases.md new file mode 100644 index 00000000000..f6ad8c6fac2 --- /dev/null +++ b/docs/orm-docs-audit/c21-data-modeling/factcheck1.relational-databases.md @@ -0,0 +1,83 @@ +Done. Both passes are complete; `check-plain.sh` prints **clean** and the page-conventions extra-word grep is empty. + +## 1. Banned-term and wording replacements + +No headings changed, so no anchors pinned. Line numbers are the final file. + +| Line | Old | New | +|---|---|---| +| 11 | "This page shows how to model each relation shape and, for each one, where the foreign key belongs. If you are new to models and keys, start with the [data modeling overview]…" | "If you are new to models and keys, start with the [data modeling overview](/orm/data-modeling)." (docs-about-docs commentary cut) | +| 15 | "…carries two fields: a scalar field for the foreign key column, and a relation field that navigates it." | "…has two fields. One is a scalar field for the foreign key column. The other is a relation field, typed as the other model." ("carries" is banned as a verb) | +| 25 | "The side that carries the `fields` argument…" | "The side that declares the `fields` argument…" | +| 46 | "The foreign key lives on the "many" side" | "The foreign key is on the "many" side" ("lives on" banned) | +| 81 | "…a bare optional model-typed field is enough…; …or emit rejects the field with `PSL_NON_UNIQUE_BACKRELATION`." | semicolon split into sentences; "emit rejects" → "`npx prisma contract emit` rejects the field with an error whose `code` is `PSL_NON_UNIQUE_BACKRELATION`" | +| 87 | "So `Profile` carries `userId`" | "So `Profile` holds `userId`" | +| 111 | "The answer is a junction model" | "The answer is the model for the join table" | +| 136 | "…and the schema compiler connects them through `PostTag`" | "…and `npx prisma contract emit` connects them through `PostTag`" | +| 146 (comment) | "// Or, equivalently, create the junction record yourself" | "// Or create the join record yourself" | +| 150 | "The junction is an ordinary model, so it can carry data… add the field to the junction model." | "`PostTag` is an ordinary model, so it can hold data… add the field to `PostTag`." | +| 164 | "…with no junction model…: the schema compiler rejects it and asks for an explicit join model. Model the junction explicitly; [Relations and joins]… traverse it" | rewritten (see Corrected table); "junction" gone, error code named, semicolon removed | +| 168 | "each carries its own extra fields" | "each has its own extra fields" | +| 198 | "narrow the base collection with `.variant(...)`…; the discriminator is injected for you" | "limit the query to one variant with `.variant(...)`… The discriminator value is filled in for you" ("narrow" as a bare verb and "injected" banned) | +| 204 | "Chaining `.variant(...)` on a read narrows the same way, applying the discriminator filter and giving the collection the variant's row shape." | "On a read, `.variant(...)` works the same way. It limits the query to one variant by filtering on the discriminator, and the rows come back with that variant's fields." | +| 208 | "Its extra columns live alongside the base columns and must be nullable at the storage level" | "Its extra columns are added to the base table and have to be nullable there" | +| 211 | "Use polymorphism when…" | "Use a base model with variants when…" ("polymorphism" as a bare noun banned) | +| 215 | "Projects scaffolded with `create-prisma@latest` install [Prisma ORM skills]…; the `prisma-8` skill…" | "Projects created with `npm create prisma@latest` include the [Prisma ORM skills]… In an existing project, run `npx prisma skills sync`. The `prisma-8` skill…" (required first-sentence rule) | +| 219 | "an explicit junction model" | "an explicit model for the join table" | +| 224 | "relation predicates, and junction traversal" | "filters on relation data, and reads across the join table" | +| 226 | "the emitter lowers them to the foreign key's `ON DELETE` / `ON UPDATE` clause" | "They become the foreign key's `ON DELETE` and `ON UPDATE` clauses." | + +Note on a conventions conflict: `page-conventions.md` says to write "the join model" after first use, but `references/banned-terms.md` bans "join model" and `check-plain.sh` fails on it. I used "the model for the join table", "the join table", and the model's own name `PostTag` instead, so the check passes. + +## 2. Claims checked + +All paths under `wip/prisma-src/`. + +| Line | Claim | Deciding source | Verdict | +|---|---|---|---| +| 17–23, 31–44, 67–79, 89–95, 113–134, 152–162, 172–194 | PSL examples need no `// use prisma-8` line | `apps/docs/content/docs/orm/contract-authoring/psl-syntax.mdx:147,159,175,222,235,262` — fragment snippets carry no marker; only whole-file `title="prisma/contract.prisma"` blocks do (lines 15–16, 59–60) | Confirmed, no change | +| 9 | Model → table, scalar field → column | `packages/2-sql/2-authoring/contract-psl/src/interpreter.ts` (`buildModelNodeFromPsl`, `storage.table` / `storage.fields`) | Confirmed | +| 19, 33, 39, 69, 74, 91, 115, 121, 174 | `Int @id @default(autoincrement())` is valid PSL on PostgreSQL | `packages/3-targets/3-targets/postgres/src/core/psl-infer/infer-model-blocks.ts:283-290`, `core/migrations/planner-ddl-builders.ts:61,151` | Confirmed | +| 25 | "The side that declares the `fields` argument owns the foreign key" | `packages/1-framework/2-authoring/contract/src/relation-nullability.ts:20-31` (`ownsReference`), `psl-relation-resolution.ts:102-150` (`indexFkRelations` builds `N:1` from the declaring side) | Confirmed | +| 46 | `posts Post[]` is a back-relation, stores nothing, matched to the FK on `Post` | `psl-relation-resolution.ts:380-430` (`applyBackrelationCandidates` matches by `fkRelationPairKey`, emits `1:N` with no storage of its own) | Confirmed | +| 50 | (was) "Because the list field is virtual, you never write to it." | `test/integration/test/sql-orm-client/nested-mutations.test.ts:19-30` (`users.create({ posts: (posts) => … })`) and `mn-nested-write.test.ts:59,95` — list fields **do** accept nested writes, and this page itself shows one at line 142 | **Corrected** — sentence removed; the paragraph now reads "To connect a post to a user, set the foreign key when you create the post:" | +| 52–57 | `db.orm.public.Post.create({ title, authorId })` — `create` takes a foreign key scalar directly, and the page's `Post` has both fields | `test/integration/test/sql-orm-client/create.test.ts:187-190` (`createAll([{ id, name, email, invitedById: null }])` sets an FK scalar in a create input); page's `Post` (lines 38–43) declares `title` and `authorId` | Confirmed | +| 65, 76, 92 | `@unique` on the foreign key makes it one-to-one | `psl-relation-resolution.ts:352-366` (`fkColumnsAreUnique`: the FK columns must exactly match a unique column set) | Confirmed | +| 81 | Mirror field `profile Profile?` needs no `@relation` | `psl-relation-resolution.ts:487-525` (`validateBackrelationFieldAttributes` skips `relation` and rejects any other attribute; a bare field is accepted) | Confirmed | +| 81 | The mirror field must be optional | `packages/1-framework/2-authoring/psl-parser/src/relation-backrelations.ts:37-51` — a required singular back-relation gets `PSL_REQUIRED_ONE_TO_ONE_BACKRELATION`; `psl-relation-resolution.ts:461-474` calls it | **Corrected** (addition): the page now states "It has to be optional, because nothing in the database guarantees a matching profile row." | +| 81 | "emit rejects the field with `PSL_NON_UNIQUE_BACKRELATION`" — exact code and what it says | `psl-relation-resolution.ts:451-458`: code `PSL_NON_UNIQUE_BACKRELATION`, message "Backrelation field … is singular, but the matching FK on … is not unique… add @unique (or @@unique([…])) to the FK fields, or make … a list." | Confirmed (code exact); wording changed to the required "`npx prisma contract emit` rejects … with an error whose `code` is …" | +| (checked, not on page) | Required relation field over an optional FK rejected with `PSL_RELATION_NULLABILITY_MISMATCH` in rc.10 | `packages/2-sql/2-authoring/contract-psl/src/interpreter.ts:702-717` (code and both message directions), `relation-nullability.ts:26-33`; tests `contract-psl/test/interpreter.relations.test.ts:167,195,271` | Confirmed — the coming-from-prisma-orm-7 page is right, and every relation field on this page pairs a required relation field with a required FK scalar, so all examples are consistent | +| 113–136 | `Post.tags Tag[]` + `Tag.posts Post[]` + `PostTag` with `@@id([postId, tagId])` resolves to many-to-many in rc.10 (not `PSL_ORPHANED_BACKRELATION`) | `psl-relation-resolution.ts:225-275` (`findJunctionFkPairs`) and `:404-420` — a bare list back-relation with no direct FK match is resolved through a model whose `@@id` covers exactly the two FK columns and whose target-side FK references the target's full `@id`. Both hold here. Corroborated by `apps/docs/content/docs/orm/contract-authoring/psl-syntax.mdx:235-256` and `examples/retail-store/src/contract.prisma` (`Post.tags Tag[]` / `Tag.posts Post[]`) | Confirmed — the example is valid | +| 136 | `@@id([postId, tagId])` enforces one record per pair, and emit connects the list fields through `PostTag` so `post.tags` reads directly | `psl-relation-resolution.ts:155-175` (`idColumnsAreExactlyFkPair`), `:277-310` (`manyToManyRelationNode` emits `cardinality: 'N:M'` with `through`) | Confirmed | +| 138 | Both writes hit the same row; running both inserts the same composite key twice and the second fails | `test/integration/test/sql-orm-client/mn-nested-write.test.ts:145-170` — "connect to an already-linked tag rejects", `rejects.toThrow(/violated a unique constraint on junction "user_tags"/)` | Confirmed | +| 142–144 | `.where({ id }).update({ tags: (t) => t.connect([{ id }]) })` — `update` takes a relation callback and `connect` exists through a join model | `mn-nested-write.test.ts:124-130` (`users.where({ id: 1 }).…update({ tags: (t) => t.connect({ id: TAG_RUST }) })`); array form at `:95` | Confirmed | +| 147 | `db.orm.public.PostTag.create({ postId, tagId })` | join model is an ordinary model in `domain.namespaces..models`; `packages/3-extensions/sql-orm-client/src/orm.ts:70-80` keys `db.orm.` by model name | Confirmed | +| 152–162 | `addedAt DateTime @default(now())` on the join model | `packages/3-targets/3-targets/postgres/src/core/default-normalizer.ts:12,33,41`; `examples/retail-store/src/contract.prisma` uses `createdAt DateTime @default(now())` | Confirmed | +| 164 | Implicit many-to-many is rejected, with an explicit join model asked for | `psl-relation-resolution.ts:428-436`: code `PSL_ORPHANED_BACKRELATION`, message "Backrelation field "X.y" has no matching FK-side relation on model "Z". Add @relation(fields: […], references: […]) on the FK-side relation **or use an explicit join model for many-to-many**." | **Corrected** — the page now names the code and drops "yet": "…is not supported. `npx prisma contract emit` rejects it with an error whose `code` is `PSL_ORPHANED_BACKRELATION`, and the message tells you to add an explicit model for the join table." Nothing in the source says "not supported *yet*"; `skills/prisma-8/references/contract.md:424` likewise states it is rejected | +| 178 | `@@discriminator(type)` | `packages/2-sql/2-authoring/contract-psl/src/sql-attribute-specs.ts:381-383` — `modelAttribute('discriminator', { positional: [{ key: 'field', type: fieldRef() }] })` | Confirmed | +| 184, 190 | `@@base(Task, "bug")` | `sql-attribute-specs.ts:384-389` — positional `entityRef()` then `str()` | Confirmed | +| 192 | `@@map("features")` on a variant | `interpreter.ts:1864-1875, 2562-2572` — an explicit `@@map` on a variant is the multi-table signal | Confirmed | +| 198 | `db.orm.public.Bug` exists alongside `db.orm.public.Task` | emitted fixture `test/integration/test/sql-orm-client/fixtures/polymorphism/generated/contract.json` lists `Bug`, `Epic`, `Feature`, `Task` as separate models in `domain.namespaces.public.models`; `orm.ts:70-80` | Confirmed | +| 198 | A query on the base model returns every variant | `test/integration/test/sql-orm-client/polymorphism.test.ts:74-128` — a bare `Task` query returns both bug and feature rows, each with its own variant field | Confirmed | +| 198, 201 | `.variant("Bug").create({...})` fills in the discriminator | `packages/3-extensions/sql-orm-client/src/collection.ts:394-449` (doc comment "Insert under a variant — discriminator is injected automatically", and `LiteralExpr.of(variantEntry.value)`) | Confirmed | +| 204 | `.variant(...)` on a read applies the discriminator filter and gives the variant's row shape | `collection.ts:394-470`; `polymorphism.test.ts:137-200` | Confirmed | +| 208 | Shared-table variant: extra columns in the base table, nullable | `interpreter.ts:1975-1995` (`materializeStiVariantStorageColumns`: "The materialised columns are always nullable in storage"); `polymorphism.test.ts:31-40` (`severity text` nullable in `tasks`) | Confirmed | +| 209 | Own-table variant: own table linked by the base primary key, constraints kept, at the cost of a join | `interpreter.ts:1890-1935` (`materializeMtiVariantStorageLinks`, "joins that table to the base on the shared primary key"); `polymorphism.test.ts:43-47` (`create table features (id integer primary key references tasks(id), priority integer not null, …)`) and `:179` ("INNER JOINs") | **Corrected** (precision) — "holding only its extra columns, linked back to the base table's primary key" became "That table holds its extra columns plus the base model's primary key, which links it back to the base table", because the variant table does carry the base PK column | +| 215 | `create-prisma@latest` / skills link | `skills/prisma-8/SKILL.md` exists; conventions fix the scaffold command to `npm create prisma@latest` | **Corrected** to the conventions wording | +| 215 | "the `prisma-8` skill covers relation modeling" | `skills/prisma-8/references/contract.md:95-142` ("Workflow — Edit a model / field / relation (PSL)"), `:213-245` (polymorphism) | Confirmed | +| 224 | `.include(...)` and filters on relation data exist | `test/integration/test/sql-orm-client/include.test.ts`, `mn-filter.test.ts` | Confirmed | +| 226 | `onDelete` / `onUpdate` accept `Cascade`, `Restrict`, `SetNull`, `SetDefault`, `NoAction` | `sql-attribute-specs.ts:418-425` (`referentialActionArgument`), `psl-relation-resolution.ts:24-34` (`REFERENTIAL_ACTION_MAP`) | Confirmed — exactly these five | +| 226 | They become the FK's `ON DELETE` / `ON UPDATE` clauses | `packages/3-targets/3-targets/postgres/src/core/migrations/operations/constraints.ts:22-45` — `sql += \`\nON DELETE ${action}\`` and the same for `ON UPDATE` | Confirmed | +| 11, 59, 81, 164, 215, 224, 225 | Every link target and anchor | `apps/docs/content/docs/orm/data-modeling/index.mdx` (`url: /orm/data-modeling`), `orm/data-modeling/mongodb.mdx`, `orm/fundamentals/relations-and-joins.mdx` headings `## One-to-one`, `## One-to-many`, `## Many-to-many`, `ai/tools/skills.mdx:100` `## Available skills for Prisma ORM 8 [#available-skills-for-prisma-8]` | Confirmed — all resolve | + +## 3. Q list + +None. Every claim on the page resolved for or against in the source. + +Two notes rather than Qs: +- No test covers the `PSL_ORPHANED_BACKRELATION` implicit-many-to-many path specifically from a docs-shaped contract; the code at `psl-relation-resolution.ts:428-436` is what the page now states. +- `PSL_REQUIRED_ONE_TO_ONE_BACKRELATION` (the required-mirror-field rejection) is a different code from `PSL_NON_UNIQUE_BACKRELATION`. I stated the optionality requirement in plain words without naming the second code, to keep the page from growing. + +## 4. Line count + +226 before, 226 after. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/page-conventions.md b/docs/orm-docs-audit/c21-data-modeling/page-conventions.md index e4a3f507dac..1adb7341579 100644 --- a/docs/orm-docs-audit/c21-data-modeling/page-conventions.md +++ b/docs/orm-docs-audit/c21-data-modeling/page-conventions.md @@ -4,7 +4,7 @@ - No commentary about the docs inside the docs. Cut sentences such as "This page introduces", "This page shows", "it is what this page focuses on", "the guides cover that in depth". Write the thing itself. - Never interleave PostgreSQL and MongoDB sentence by sentence. Where both databases matter, give PostgreSQL its own sentences or block and then MongoDB its own. Tabs already on a page stay. - The word "contract" is the subject of the contract-authoring section, not this one. On first use on a page: "your contract, the `contract.prisma` file that replaced `schema.prisma`". After that, "contract" alone. Do not use "schema" for the contract; "schema" alone means the PostgreSQL schema (`public`). -- "Join table": the model that holds one record per connected pair is "the model for the join table" on first use and "the join model" after. Never "junction", "junction model", "junction table", "junction record". +- "Join table": the model that holds one record per connected pair is "the model for the join table" on first use and the model's own name (`PostTag`) or "the join table" after. Never "junction", "junction model", "junction table", "junction record", or "join model" (the checker fails on it). - Relation fields: "a relation field, typed as the other model" and "the field that stores the other record's primary key" (the foreign key column on SQL, the field holding the `_id` on MongoDB). "Back-relation" and "virtual" only with the plain meaning beside them ("stores nothing in the database"). - `prisma contract emit` is the command that checks the contract. Say "`npx prisma contract emit` rejects it with an error whose `code` is `X`". Never "emit rejects", "the schema compiler", "the emitter lowers", "lowered". Referential actions: "become the foreign key's `ON DELETE` and `ON UPDATE` clauses". - Banned on these pages in addition to the list: gate / gated; capability in prose; codec; canonical; deterministic; pure; "source of truth"; "escape hatch"; "purpose-built"; "first-class"; "wiring" / "wired"; "hangs off"; "consult"; "authoritative"; "physical" (write "in the database"); "idiomatic" (write "the usual choice" or "what MongoDB expects"); "denormalize" only with its meaning beside it ("keep a small copy of the fields a read needs"); "hot paths" (write "reads that run often"); "opaque"; "polymorphism" as a bare noun (write "a base model with variants"); "lifecycle"; "injected" (write "filled in for you"); "narrow" as a verb for `.variant()` only with "limit the query to one variant" beside it; "at the storage level" (write "in the table"); "rides along"; "lives on" / "lives inside" (write "is on", "is stored inside"). From 9e3cdf6c162b496d486550a5867a27b82f2b02eb Mon Sep 17 00:00:00 2001 From: willbot Date: Mon, 14 Sep 2026 17:03:43 +0200 Subject: [PATCH 03/15] docs(orm): data-modeling reader review round one Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- .../content/docs/orm/data-modeling/index.mdx | 56 ++++++++------- .../docs/orm/data-modeling/mongodb.mdx | 70 +++++++++--------- .../data-modeling/relational-databases.mdx | 72 ++++++++----------- .../c21-data-modeling/index.fix1.md | 48 +++++++++++++ .../c21-data-modeling/index.round1.md | 67 +++++++++++++++++ .../c21-data-modeling/mongodb.fix1.md | 54 ++++++++++++++ .../c21-data-modeling/mongodb.round1.md | 65 +++++++++++++++++ .../relational-databases.fix1.md | 56 +++++++++++++++ .../relational-databases.round1.md | 69 ++++++++++++++++++ 9 files changed, 459 insertions(+), 98 deletions(-) create mode 100644 docs/orm-docs-audit/c21-data-modeling/index.fix1.md create mode 100644 docs/orm-docs-audit/c21-data-modeling/index.round1.md create mode 100644 docs/orm-docs-audit/c21-data-modeling/mongodb.fix1.md create mode 100644 docs/orm-docs-audit/c21-data-modeling/mongodb.round1.md create mode 100644 docs/orm-docs-audit/c21-data-modeling/relational-databases.fix1.md create mode 100644 docs/orm-docs-audit/c21-data-modeling/relational-databases.round1.md diff --git a/apps/docs/content/docs/orm/data-modeling/index.mdx b/apps/docs/content/docs/orm/data-modeling/index.mdx index 639161b9dc6..29f40d6bcc6 100644 --- a/apps/docs/content/docs/orm/data-modeling/index.mdx +++ b/apps/docs/content/docs/orm/data-modeling/index.mdx @@ -10,7 +10,7 @@ Data modeling is the process of describing the data your application needs and h For example, a blog has users, posts, and comments. A user has fields like an email and a name. A post has fields like a title and content. These models also relate to each other: a user can write many posts, and a post can have many comments. -In Prisma ORM, you define this structure in your contract, the `contract.prisma` file that replaced `schema.prisma`. Prisma ORM derives your TypeScript types, your migrations, and the client's configuration from it. +In Prisma ORM, you define this structure in your contract, the `contract.prisma` file that replaced `schema.prisma`. You still describe your models in it. Prisma ORM derives your TypeScript types and your migrations from it. If you are coming from Prisma ORM 7, [what changed](/orm/coming-from-prisma-orm-7) covers the rename and the rest of the differences. Every contract is built from four things: @@ -19,7 +19,7 @@ Every contract is built from four things: - [Scalar fields](#scalar-fields): the values a model stores - [Relations](#relations): how models connect to each other -All four apply to every database. How you model relations is where databases differ, so continue with [relational data modeling](/orm/data-modeling/relational-databases) or [MongoDB data modeling](/orm/data-modeling/mongodb). +All four apply to both databases Prisma ORM supports today, PostgreSQL and MongoDB. How you model relations is where the two differ, so continue with [relational data modeling](/orm/data-modeling/relational-databases) or [MongoDB data modeling](/orm/data-modeling/mongodb). ## Models @@ -56,13 +56,13 @@ model User { } ``` -On MongoDB, the primary key is the document's mandatory `_id` field, and it must be an `ObjectId`. +On MongoDB, the primary key is the document's mandatory `_id` field, and it must be an `ObjectId`. `@map("_id")` gives the field that name in the database, and `@@map("users")` names the collection the model is stored in. Leave `id` out of the `create` call and MongoDB assigns the `ObjectId` for you. ### Natural keys A natural key is a value that already identifies the record in the real world. Use one when the value is stable, unique, and assigned outside your application: a standardized code you receive rather than invent. -Reference tables are the classic fit. A country is its ISO code, and the code never changes: +A reference table, one that holds a fixed set of values the rest of your data points at, is the classic fit. A country is its ISO code, and the code never changes: ```prisma model Country { @@ -71,7 +71,7 @@ model Country { } ``` -Records that point at `Country` now store a readable value (`US`, `DE`) instead of a meaningless number. Currencies (`USD`, `EUR`) and similar lookup tables work the same way. +Records that point at `Country` now store a readable value (`US`, `DE`) instead of a meaningless number. Currencies (`USD`, `EUR`) and other reference tables work the same way. ### Surrogate keys @@ -98,9 +98,8 @@ model Product { Pick by how the record is created and where its id travels: ```prisma -// Auto-incrementing integer: smallest and fastest to index, -// ordered by insertion. Good for internal records whose id -// never leaves your system. +// Auto-incrementing integer: smallest and fastest to index. +// Good for internal records whose id never leaves your system. model Invoice { id Int @id @default(autoincrement()) } @@ -116,16 +115,9 @@ model ApiToken { } ``` -A UUID is wider than an integer and random UUIDs index a little worse, but independent services never collide and nothing is leaked. +A UUID takes more space than an integer, but independent services never produce the same one and the value tells nobody how many records you have. `@default(uuid())` gives you a random UUID. Write `@default(uuid(7))` instead for a UUID that starts with the time it was created, so new ids sort after older ones. -```prisma -// ObjectId: the MongoDB key. Generated without -// coordination and embeds its own creation time. -model Post { - id ObjectId @id @map("_id") - @@map("posts") -} -``` +On MongoDB the choice is made for you. The key is the `ObjectId` shown above, which MongoDB generates without asking any other server and which carries its own creation time. ### Composite keys @@ -160,17 +152,23 @@ A scalar field holds a single value. The common types: | `Jsonb` | Arbitrary JSON value, PostgreSQL's `jsonb` | | `ObjectId` | MongoDB document identifier | -A `Decimal` value reaches your code as a decimal string, so binary floating point never loses digits. Write the type as `Numeric(10, 2)` when you want an explicit precision and scale. A `DateTime` value reaches your code as a `Temporal.Instant`, and a `Bytes` value as a `Uint8Array`. `Jsonb` is also the storage the TypeScript builder's `field.json()` and value objects use. +The column names in the table are PostgreSQL columns. MongoDB has no such mapping, and `ObjectId` is the only type in the table that is specific to it. + +A `Decimal` value reaches your code as a decimal string, so binary floating point never loses digits. Write `price Decimal` when any precision will do, and `price Numeric(10, 2)` when you want an explicit precision and scale. Both become a PostgreSQL `numeric` column. + +A `DateTime` value reaches your code as a `Temporal.Instant`, the standard JavaScript type for a point in time. [Raw queries](/orm/reference/raw-queries) says which Node.js versions have `Temporal` built in and how to add it to older ones. A `Bytes` value reaches your code as a `Uint8Array`. Filtering on a `Jsonb` field is whole-value equality only: you can ask whether the column equals a given document, not whether a key inside it matches. A `Json` field offers no comparison at all in the ORM filter. Reach into the document with [raw SQL](/orm/reference/raw-queries) when you need a path query. -Prisma ORM's type system is extensible, so extensions can add more types, such as vectors or geometry. +[Extensions](/orm/extensions) add more types, such as vectors or geometry. -Two modifiers change a field's shape: +Two modifiers change a field: - A trailing `?` makes the field optional: `name String?` - A trailing `[]` makes it a list: `tags String[]` +A field can also hold one of a fixed set of values, which you declare as an [enum](/orm/contract-authoring/psl-syntax#enums). + ### How to pick a data type Match the type to what the value means, not to what it looks like. @@ -184,7 +182,7 @@ model Address { } ``` -Money is not a `Float`. Floating-point numbers cannot represent decimal amounts exactly, so sums drift by fractions of a cent. Store an integer number of the smallest unit: +Money is not a `Float`. Floating-point numbers cannot represent decimal amounts exactly, so sums drift by fractions of a cent. Store an integer number of the smallest unit instead. `Decimal` is exact as well, so use it when your amounts need more decimal places than the currency's smallest unit, and use `Int` otherwise because a plain JavaScript number is easier to work with than a decimal string: ```prisma model Product { @@ -202,7 +200,7 @@ model Post { } ``` -When you are unsure between two sizes, lean toward the wider one (`BigInt` over `Int` for a counter that could grow). Widening a type later is a migration. The wider type today is free. +When you are unsure between two sizes, lean toward the wider one. For a counter that could grow past two billion, that means `BigInt` over `Int`. Widening a type later is a migration, so picking the wider type now saves you one. `BigInt` does cost you something in your code: the value reaches you as a JavaScript `bigint`, which `JSON.stringify` refuses to serialize. Convert it to a string before you send it anywhere as JSON. Mark a field optional (`?`) only when "absent" means something different from a sensible default. A required field with a default is often the clearer model. @@ -210,7 +208,7 @@ Mark a field optional (`?`) only when "absent" means something different from a A relation connects two models: a user has many posts, a post belongs to one user. -Two kinds of field describe a relation: +Two kinds of field describe a relation, and each model in it needs the fields for its own side: - The field that stores the other record's primary key, like `authorId`. On a relational database this is the foreign key column, and on MongoDB it is the field holding the other document's `_id`. - A relation field, typed as the other model, like `author User`. It stores nothing in the database. It tells Prisma ORM how to follow the connection in a query. @@ -218,6 +216,11 @@ Two kinds of field describe a relation: The `@relation` attribute ties them together: `fields` names the local field that stores the link, and `references` names the field it points at on the other model. ```prisma +model User { + id Int @id @default(autoincrement()) + posts Post[] +} + model Post { id Int @id @default(autoincrement()) authorId Int @@ -225,13 +228,15 @@ model Post { } ``` -Relations come in three shapes: +`posts Post[]` on `User` is the other end of the same relation, and like `author` it stores nothing in the database. + +Relations come in three kinds: - One-to-one: a user has at most one profile. - One-to-many: a user has many posts. - Many-to-many: a post has many tags, and a tag appears on many posts. -How each shape is stored, and which model should hold the connecting field, is where relational and document databases differ. Continue with the guide for your database: +How each kind is stored, and which model should hold the connecting field, is where relational and document databases differ. Continue with the guide for your database: - [Relational data modeling](/orm/data-modeling/relational-databases) for PostgreSQL and other SQL databases: foreign keys, the model for the join table, and which side owns the key. - [MongoDB data modeling](/orm/data-modeling/mongodb): embedding versus referencing, and polymorphic collections. @@ -249,4 +254,5 @@ Projects created with `npm create prisma@latest` include the [Prisma ORM skills] - [Model relational data](/orm/data-modeling/relational-databases): one-to-one, one-to-many, many-to-many, and polymorphic relations on SQL databases. - [Model MongoDB data](/orm/data-modeling/mongodb): embed or reference, and polymorphic collections. +- [Generate a migration](/orm/migrations/generating-a-migration) to get the models you wrote into the database: run `npx prisma contract emit` after every change to the contract, then plan and apply a migration. - [Query your models](/orm/fundamentals/reading-data) once your contract is in place. diff --git a/apps/docs/content/docs/orm/data-modeling/mongodb.mdx b/apps/docs/content/docs/orm/data-modeling/mongodb.mdx index c9d5945d994..f216f55ff7f 100644 --- a/apps/docs/content/docs/orm/data-modeling/mongodb.mdx +++ b/apps/docs/content/docs/orm/data-modeling/mongodb.mdx @@ -6,9 +6,7 @@ metaTitle: MongoDB data modeling in Prisma ORM metaDescription: 'How to model data in Prisma ORM for MongoDB, including documents and collections, embedded documents versus references, and polymorphic collections.' --- -MongoDB stores data as documents grouped into collections. A document can nest objects and arrays directly, which gives you a real choice for related data: keep it inside the document (embed it) or store it in its own collection and link to it (reference it). - -Making that choice well is the core of MongoDB data modeling. If you are new to models and keys, start with the [data modeling overview](/orm/data-modeling). +MongoDB stores data as documents grouped into collections. A document can nest objects and arrays directly, which gives you a real choice for related data: keep it inside the document (embed it) or store it in its own collection and link to it (reference it). Making that choice well is the core of MongoDB data modeling. If you are new to models and keys, start with the [data modeling overview](/orm/data-modeling). ## Documents, collections, and the _id key @@ -23,15 +21,23 @@ model User { } ``` -`@map("_id")` maps the model's `id` field to MongoDB's mandatory `_id` field, and `@@map("users")` names the collection. `@@map` is optional. Without it the collection takes the model name with a lowercase first letter, so `user`. +`@map("_id")` maps the model's `id` field to MongoDB's mandatory `_id` field, and `@@map("users")` names the collection. `@@map` is optional. Without it the collection takes the model name with a lowercase first letter, so `user`, and you reach it in code as `db.orm.user`. + +`ObjectId` is the type of the field. There is no `@default(auto())` and no `@db.ObjectId`. Leave `id` out of the values you pass to `create`, and MongoDB assigns the `ObjectId`: + +```ts +const user = await db.orm.users.create({ name: "Ada", email: "ada@example.com" }); +``` + +`db` is the client you create once in `src/prisma/db.ts`, and `prisma orm init` writes that file. `db.orm` holds your models, and on MongoDB the accessor is the collection name, so `db.orm.users`. The values go straight in, without the `data:` wrapper Prisma ORM 7 used. Models themselves go in your contract, the `contract.prisma` file that replaced `schema.prisma`. Run `npx prisma contract emit` after every change to it. MongoDB has migrations like any other database: `npx prisma migration plan` writes one and `npx prisma db migrate` applies it, creating the collections and indexes. See [the data contract](/orm/contract-authoring/the-data-contract). ## Embed or reference Two pieces of related data can live together in one document (embedded) or in separate collections linked by an id (referenced). -Embedding nests the data inside its parent: an order and its line items in one document. One read returns everything, and a write updates parent and children together, atomically. The trade-off is that embedded data has no independent life. You cannot read it without its parent, and it comes back on every read of the parent. +Embedding nests the data inside its parent: an order and its line items in one document. One read returns everything, and a write updates parent and children together, atomically. The trade-off is that you cannot read embedded data without its parent, and it comes back on every read of the parent. -Referencing keeps the data in its own collection and stores the linked document's `_id`. Loading both takes a second lookup, but each record stands on its own: it can be queried, listed, and updated independently, and it is not duplicated when several parents point at it. +Referencing keeps the data in its own collection and stores the linked document's `_id`. Loading both takes a second read, but each record stands on its own: it can be queried, listed, and updated independently, and it is not duplicated when several parents point at it. A quick way to decide: @@ -54,7 +60,6 @@ Describe the shape of embedded data with a `type` block, then use it as a field. type Address { street String city String - zip String? country String } @@ -66,32 +71,25 @@ model User { } ``` -A single embedded value models a one-to-one. A list of embedded values models a one-to-many: +A single embedded value models a one-to-one. A list of embedded values models a one-to-many: `items CartItem[]` on a `Cart` model stores every line item inside the cart document, with no second collection and no join to load them. -```prisma -type CartItem { - productId String - name String - amount Int -} +An embedded value has no `_id` and no collection of its own. It is created, loaded, updated, and deleted with the document that holds it. Pass it to `create` as a plain nested object: -model Cart { - id ObjectId @id @map("_id") - items CartItem[] - @@map("carts") -} +```ts +const address = { street: "1 Main St", city: "Berlin", country: "DE" }; +await db.orm.users.create({ name: "Ada", email: "ada@example.com", address }); ``` -`Cart.items` is stored inside the cart document. There is no separate collection and no join to load the items. Every read of a cart returns them. +To change one field inside an embedded value, give `update` the path to it: -An embedded `Address` has no `_id` and no collection of its own, so you cannot read it without its parent. It is loaded, updated, and deleted with the document that holds it. You can still match on its fields through the parent, with a path such as `address.city` in the [pipeline builder](/orm/fundamentals/advanced-queries#mongodb-pipeline-builder). That is the right behavior for values that belong to their parent, and the wrong one for records with their own life. +```ts +await db.orm.users.where({ email: "ada@example.com" }).update((u) => [u("address.city").set("Hamburg")]); +``` -:::warning +`.where()` filters on top-level model fields only. To match on a field inside an embedded document, use the pipeline builder, the API for MongoDB aggregation pipelines, with a path such as `address.city`. See [the pipeline builder](/orm/fundamentals/advanced-queries#mongodb-pipeline-builder). Embedded arrays are only a good idea while they stay bounded. An array that grows forever slows every read of the parent and pushes the document toward MongoDB's 16 MB limit. When a list has no natural cap, reference it instead. -::: - ## References across collections When related records need their own collection, store one document's `_id` on the other and declare the relation with the same `@relation(fields:, references:)` used everywhere in Prisma ORM: @@ -113,15 +111,17 @@ model Post { } ``` -This is a one-to-many by reference: many posts point at one user, and each post is queryable on its own. When you [include the relation in a query](/orm/fundamentals/relations-and-joins), Prisma ORM resolves it with a `$lookup` aggregation. +This is a one-to-many by reference: many posts point at one user, and each post is queryable on its own. To load a post together with its author, chain `.include("author")`, which adds the related record to every row you get back: -A `$lookup` is a real cost on reads that run often, so document models often keep a small copy of the fields a read needs inside the parent. A comment stores its author's name, while the full author record stays in its own collection. You trade some duplication, and the work of keeping the copy in sync, for reads that touch one document. +```ts +const posts = await db.orm.posts.include("author").all(); +``` -## Polymorphic collections +See [relations and joins](/orm/fundamentals/relations-and-joins) for the rest of the query API. Resolving a reference costs a second read. If a read runs constantly, you can copy the few fields it needs into the parent document, such as a comment storing its author's name. -MongoDB collections do not enforce a single shape. The usual choice is to store documents of different kinds in one collection and tell them apart with a discriminator field. +## Polymorphic collections -Prisma ORM models this with a base model plus variant models, all sharing one collection: +Prisma ORM models a collection that holds more than one kind of document with a base model plus variant models, all sharing one collection. MongoDB collections do not enforce a single shape, so this is the usual way to keep the kinds together and tell them apart by a discriminator field, a field whose value says which kind each document is. ```prisma model Post { @@ -147,13 +147,19 @@ model Tutorial { } ``` -`@@discriminator(kind)` names the field that records each document's variant. `@@base(Post, "article")` declares `Article` as the `"article"` variant of `Post`. A query for posts returns articles and tutorials together. `db.orm.posts.variant("Article")` limits the query to one variant. +`@@discriminator(kind)` takes the name of the field that records each document's variant, so write `kind` and not `"kind"`. `@@base(Post, "article")` declares `Article` as the `"article"` variant of `Post`. A variant has every field of its base plus its own, so an `Article` has `id`, `title`, `kind`, and `summary`. + +A query for posts returns articles and tutorials together. `db.orm.posts.variant("Article")` limits the query to one variant. `.variant()` takes the variant's model name, `"Article"`, and not the discriminator value `"article"`. Creating through a variant fills in `kind` for you: + +```ts +const article = await db.orm.posts.variant("Article").create({ title: "Hello", summary: "A short post" }); +``` -Use one polymorphic collection when the variants are handled together far more than separately: a notifications collection of email, SMS, and push messages read as one stream. Prefer separate collections when the types rarely appear in the same query or need very different indexes. +Use one collection when you almost always query the variants together, for example email, SMS, and push notifications that you read as a single feed. Prefer separate collections when the types rarely appear in the same query or need very different indexes. Indexes are declared on the model with `@@index([authorId])`, which is covered in [PSL syntax](/orm/contract-authoring/psl-syntax). ## Prompt your coding agent -Projects created with `npm create prisma@latest` include the [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8) for your coding agent. In an existing project, run `npx prisma skills sync`. The `prisma-8` skill covers document modeling. Prompts that map to each section: +Projects created with `npm create prisma@latest` include the [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8), instruction files for coding agents. In an existing project, run `npx prisma skills sync`. The `prisma-8` skill covers document modeling. Prompts that map to each section: - "Using the prisma-8 skill, add an embedded Address type to the User model." - "Cart items are always read with the cart. Model them as an embedded list." diff --git a/apps/docs/content/docs/orm/data-modeling/relational-databases.mdx b/apps/docs/content/docs/orm/data-modeling/relational-databases.mdx index c2e3ea434ae..5bc1673f5c0 100644 --- a/apps/docs/content/docs/orm/data-modeling/relational-databases.mdx +++ b/apps/docs/content/docs/orm/data-modeling/relational-databases.mdx @@ -24,6 +24,12 @@ model Post { The side that declares the `fields` argument owns the foreign key. Which side that should be is the main decision in the shapes below. +Run `npx prisma contract emit` after every change to your contract, the `contract.prisma` file that replaced `schema.prisma`. It checks the contract and writes `contract.json` and `contract.d.ts` beside it. To create the tables themselves, run `npx prisma migration plan --name add-posts` and then `npx prisma db migrate`, as described in [Generating a migration](/orm/migrations/generating-a-migration). + +To let a post have no author, make the foreign key field and the relation field both optional: `authorId Int?` and `author User?`. Making one optional and the other required is rejected with an error whose `code` is `PSL_RELATION_NULLABILITY_MISMATCH`. + +Two relations between the same pair of models need a name on both sides, such as `@relation("Authored", fields: [authorId], references: [id])` on one side and `@relation("Authored")` on the other. Without the name, `npx prisma contract emit` reports an error whose `code` is `PSL_AMBIGUOUS_BACKRELATION`. + ## One-to-many A one-to-many relation (`1:n`) connects one record to many: one user writes many posts, and each post belongs to one user. This is the most common relation shape. @@ -43,11 +49,13 @@ model Post { } ``` -The foreign key is on the "many" side, `Post.authorId`, because each post points at exactly one user, while a user points at an open-ended list of posts. The `posts Post[]` field on `User` is the back-relation: it stores nothing in the database and is matched to the foreign key on `Post`. +The foreign key is on the "many" side, `Post.authorId`, because each post points at exactly one user, while a user points at an open-ended list of posts. + +`posts Post[]` on `User` is the mirror field: the field on the other model, which stores nothing in the database. `npx prisma contract emit` matches it to the foreign key on `Post`. Mirror fields are optional to declare. Leave it out and the relation still works, and you read it from the foreign key side only. Use a one-to-many whenever a record belongs to one parent and a parent has many children: comments on a post, line items on an order, employees in a department. -To connect a post to a user, set the foreign key when you create the post: +To connect a post to a user, set the foreign key when you create the post. `db.orm` holds your models by model name, and `public` is the PostgreSQL schema your tables live in, which is `public` unless you set another one. The `data:` wrapper from Prisma ORM 7 is gone, so the fields go straight into `create`: ```typescript const post = await db.orm.public.Post.create({ @@ -78,37 +86,27 @@ model Profile { } ``` -Declare the relation on the side that holds the foreign key, `Profile` here. You can add the mirror field on the other model as well: `profile Profile?` on `User`. A bare field typed as the other model is enough, and it needs no `@relation` of its own. It has to be optional, because nothing in the database guarantees a matching profile row. The foreign key it mirrors must be unique, or `npx prisma contract emit` rejects the field with an error whose `code` is `PSL_NON_UNIQUE_BACKRELATION`. With the mirror field in place, [the relation reads from either side](/orm/fundamentals/relations-and-joins#one-to-one). +Declare the relation on the side that holds the foreign key, `Profile` here. -### Which side owns the foreign key +You can add the mirror field to the other model as well: `profile Profile?` on `User`. A bare field typed as the other model is enough, and it needs no `@relation` of its own. It has to be optional, because nothing in the database guarantees a matching profile row. The foreign key it mirrors has to be `@unique`. If it is not, `npx prisma contract emit` reports an error whose `code` is `PSL_NON_UNIQUE_BACKRELATION`. With the mirror field in place, [the relation reads from either side](/orm/fundamentals/relations-and-joins#one-to-one). -Put the foreign key on the dependent side: the record that cannot exist on its own. +### Which side owns the foreign key -A profile needs a user. A user does not need a profile. So `Profile` holds `userId`: - -```prisma -model Profile { - id Int @id @default(autoincrement()) - userId Int @unique - user User @relation(fields: [userId], references: [id]) -} -``` +Put the foreign key on the dependent side, meaning the record that cannot exist on its own. A profile needs a user. A user does not need a profile. So `Profile` holds `userId`. The other signals all point at the same side. The dependent record is: - The one you create second. Insert the `User`, then its `Profile`. - The one you would delete when the parent goes away. -- The one whose absence is normal. A user without a profile is fine; a profile without a user is broken data. +- The one whose absence is normal. A user without a profile is fine, and a profile without a user is broken data. Placing the key on the dependent side keeps `User` free of profile concerns, lets a user exist with no profile, and guarantees every profile has exactly one user. -If neither side is clearly dependent, put the key on the side you query from less often. - ## Many-to-many A many-to-many relation (`m:n`) connects many records on each side: a post has many tags, and a tag applies to many posts. -A single foreign key cannot express this, because each side needs to point at many records. The answer is the model for the join table: a third model that holds one record per connected pair. +One foreign key column cannot hold many values, so you add a third model, the model for the join table, which holds one record per connected pair. ```prisma model Post { @@ -133,12 +131,12 @@ model PostTag { } ``` -`PostTag` is the model for the join table, and it is two one-to-many relations back to back. Its composite primary key, `@@id([postId, tagId])`, enforces one record per pair. The list fields on `Post` and `Tag` name the far side (`Tag[]`, `Post[]`), and `npx prisma contract emit` connects them through `PostTag`, so queries read `post.tags` directly. +The composite primary key on `PostTag`, `@@id([postId, tagId])`, allows one record per pair. The mirror fields on `Post` and `Tag` name the far side (`Tag[]`, `Post[]`), and `npx prisma contract emit` matches them to the two relations on `PostTag`, so queries read `post.tags` directly. If a second join table connects the same two models, name the relation on both sides. -Connect a post to a tag with a nested write on the list field, or create the `PostTag` record yourself. Both write the same row, so pick one per pair: running both inserts the same composite key twice, and the second fails. +Connect a post to a tag with a nested write on the mirror field, or create the `PostTag` record yourself. Both write the same row, so pick one per pair: running both inserts the same composite key twice, and the second fails. ```typescript -// Nested write on the list field +// Nested write on the mirror field await db.orm.public.Post.where({ id: post.id }).update({ tags: (t) => t.connect([{ id: tag.id }]), }); @@ -147,19 +145,9 @@ await db.orm.public.Post.where({ id: post.id }).update({ await db.orm.public.PostTag.create({ postId: post.id, tagId: tag.id }); ``` -`PostTag` is an ordinary model, so it can hold data about the pairing itself. When the tag was added, who added it, a sort order: add the field to `PostTag`. +`t` is the argument of the nested write callback, and it offers `t.create([...])`, `t.connect([...])`, and `t.disconnect([...])`, each taking an array. [Relations and joins](/orm/fundamentals/relations-and-joins) covers them. -```prisma -model PostTag { - postId Int - tagId Int - addedAt DateTime @default(now()) - post Post @relation(fields: [postId], references: [id]) - tag Tag @relation(fields: [tagId], references: [id]) - - @@id([postId, tagId]) -} -``` +`PostTag` is an ordinary model, so the join table can hold data about the pairing itself. To record when a tag was added, add `addedAt DateTime @default(now())` to `PostTag`. An implicit many-to-many, with list fields on both sides and no model for the join table, as in Prisma ORM 7, is not supported. `npx prisma contract emit` rejects it with an error whose `code` is `PSL_ORPHANED_BACKRELATION`, and the message tells you to add an explicit model for the join table. Declare that model yourself. [Relations and joins](/orm/fundamentals/relations-and-joins#many-to-many) shows how to read across it in one query. @@ -167,7 +155,7 @@ An implicit many-to-many, with list fields on both sides and no model for the jo Sometimes several kinds of record share a common core but each has its own extra fields: every `Task` has a title, but a `Bug` also has a severity and a `Feature` has a target release. -Prisma ORM models this with a base model plus variant models. A discriminator field records which variant each row is: +Prisma ORM models this with a base model plus variant models. One field on the base model, the discriminator, records which variant each row is: ```prisma model Task { @@ -193,20 +181,22 @@ model Feature { } ``` -`@@discriminator(type)` marks the field that records the variant. `@@base(Task, "bug")` declares that `Bug` is the `"bug"` variant of `Task`. +`@@discriminator(type)` marks the field that records the variant. `@@base(Task, "bug")` declares that `Bug` is the `"bug"` variant of `Task`. A variant has every field of its base plus the ones it declares, and it needs no `@id` of its own, because it uses the base model's primary key. -Variants appear as their own models in queries, `db.orm.public.Bug` alongside `db.orm.public.Task`. A query on the base model returns every variant. To write a variant, limit the query to one variant with `.variant(...)` and create through it. The discriminator value is filled in for you: +Read and write variants through the base model. `.variant(...)` limits the query to one variant, and it takes the variant's model name, `"Bug"`, not the discriminator value `"bug"`. On a create, the discriminator value is filled in for you: ```typescript await db.orm.public.Task.variant("Bug").create({ title: "Crash on save", severity: "high" }); ``` -On a read, `.variant(...)` works the same way. It limits the query to one variant by filtering on the discriminator, and the rows come back with that variant's fields. +A read works the same way. `.variant("Bug")` filters on the discriminator, and the rows come back with the base fields plus that variant's fields. A query on `Task` with no `.variant(...)` returns every variant. + +`@@map` decides how a variant is stored: -Each variant picks one of two storage layouts: +- A variant with no `@@map`, like `Bug`, shares the base table. Its extra columns are added to the base table and are nullable there, because a `Feature` row has no severity. +- A variant with its own `@@map`, like `Feature`, gets its own table. That table holds its extra columns plus the base model's primary key, which links it back to the base table. Those columns can be `NOT NULL`, and reading a full variant costs a join. -- A variant with no `@@map`, like `Bug`, shares the base table. Its extra columns are added to the base table and have to be nullable there, because a `Feature` row has no severity. -- A variant with its own `@@map`, like `Feature`, gets its own table. That table holds its extra columns plus the base model's primary key, which links it back to the base table. Its columns keep their constraints, at the cost of a join to read a full variant. +Start with the shared table. Give a variant its own table when its columns have to be `NOT NULL` in the database, or when the variant has many columns of its own. Use a base model with variants when you query the variants together (one feed of tasks, one stream of events) and each variant has real structure of its own. If you never query them together, separate models are simpler. If the variants differ by one nullable field, a single model is simpler still. @@ -223,4 +213,4 @@ Projects created with `npm create prisma@latest` include the [Prisma ORM skills] - [Query relations](/orm/fundamentals/relations-and-joins) with `.include(...)`, filters on relation data, and reads across the join table. - To model for MongoDB, see the [MongoDB data modeling guide](/orm/data-modeling/mongodb). -- Referential actions: add `onDelete` and `onUpdate` to `@relation` with `Cascade`, `Restrict`, `SetNull`, `SetDefault`, or `NoAction`, as in `@relation(fields: [userId], references: [id], onDelete: Cascade)`. They become the foreign key's `ON DELETE` and `ON UPDATE` clauses. +- Referential actions: add `onDelete` and `onUpdate` to `@relation`, as in `@relation(fields: [userId], references: [id], onDelete: Cascade)`. `Cascade` deletes or updates the matching rows, `Restrict` blocks the change, `SetNull` clears the foreign key, `SetDefault` writes its default value, and `NoAction` leaves the decision to the database. They become the foreign key's `ON DELETE` and `ON UPDATE` clauses. diff --git a/docs/orm-docs-audit/c21-data-modeling/index.fix1.md b/docs/orm-docs-audit/c21-data-modeling/index.fix1.md new file mode 100644 index 00000000000..cdd3888ceda --- /dev/null +++ b/docs/orm-docs-audit/c21-data-modeling/index.fix1.md @@ -0,0 +1,48 @@ +Done. Every mark fixed; page is 258 lines (was 252), budget met, `check-plain.sh` clean, no em-dashes. + +**File:** `/Users/will/Projects/prisma/web/.claude/worktrees/pr-conflicts-review-fb93f7/wip/c21/dm/r1/index.mdx` + +## Marks and what I did + +**"contract" (line 13) / "the client's configuration"** — kept the conventions.md wording, added "You still describe your models in it", dropped the configuration clause (the source does not back it), and added the version-7 pointer: "If you are coming from Prisma ORM 7, [what changed](/orm/coming-from-prisma-orm-7) covers the rename and the rest of the differences." That also answers "so what do I type" item 1 (migrating an existing project) with a link rather than a new section. + +**Databases other than PostgreSQL** — "All four apply to both databases Prisma ORM supports today, PostgreSQL and MongoDB." After the type table: "The column names in the table are PostgreSQL columns. MongoDB has no such mapping, and `ObjectId` is the only type in the table that is specific to it." No mention of MySQL or SQLite. + +**MongoDB `_id`, and `@@map` unexplained** — one sentence added to the primary-keys paragraph: "`@map("_id")` gives the field that name in the database, and `@@map("users")` names the collection the model is stored in. Leave `id` out of the `create` call and MongoDB assigns the `ObjectId` for you." + +**Reference / lookup tables** — defined at first use ("A reference table, one that holds a fixed set of values the rest of your data points at"), and the second term is now "other reference tables" instead of "lookup tables". + +**Random UUIDs (line 119)** — index internals cut, the sortable option named: "`@default(uuid())` gives you a random UUID. Write `@default(uuid(7))` instead for a UUID that starts with the time it was created, so new ids sort after older ones." + +**The four-fact paragraph (line 163)** — split into two paragraphs. The `field.json()` and value-objects sentence is gone. `Numeric` now has syntax: "Write `price Decimal` when any precision will do, and `price Numeric(10, 2)` when you want an explicit precision and scale. Both become a PostgreSQL `numeric` column." `Temporal.Instant` is named as "the standard JavaScript type for a point in time" with a clause linking `/orm/reference/raw-queries` for Node.js versions and the polyfill. + +**Extensions (line 167)** — architecture claim cut, one linked sentence: "[Extensions](/orm/extensions) add more types, such as vectors or geometry." + +**Enums** — one sentence after the modifiers: "A field can also hold one of a fixed set of values, which you declare as an [enum](/orm/contract-authoring/psl-syntax#enums)." + +**Decimal versus integer cents** — reconciled in the money paragraph: use `Decimal` when amounts need more decimal places than the currency's smallest unit, `Int` otherwise, because a plain number is easier to work with than a decimal string. + +**"The wider type today is free"** — replaced with the real cost: `BigInt` reaches your code as a JavaScript `bigint`, which `JSON.stringify` refuses to serialize, so convert it to a string first. + +**The other side of a relation** — the example now shows `User` with `posts Post[]` above `Post`, the lead-in reads "each model in it needs the fields for its own side", and one sentence follows: "`posts Post[]` on `User` is the other end of the same relation, and like `author` it stores nothing in the database." + +**After I write the models** — a Next steps bullet: "[Generate a migration](/orm/migrations/generating-a-migration) to get the models you wrote into the database: run `npx prisma contract emit` after every change to the contract, then plan and apply a migration." No flags restated. + +**"skills" (line 241)** — unchanged; the reader accepted it and it is linked. + +## Cuts to stay in budget + +- The `ObjectId` code block in "Which surrogate type to pick" duplicated the MongoDB primary-key tab. It is now one prose sentence pointing at that tab. +- One comment line trimmed in the `Invoice` example. +- Also replaced "shape"/"shapes" for relations with "kinds" (banned unexplained), and "a field's shape" with "a field". + +## Facts looked up + +- `uuid()`, `uuid(4)`, `uuid(7)` accepted: `/Users/will/Projects/prisma/web/.claude/worktrees/pr-conflicts-review-fb93f7/wip/prisma-src/packages/3-targets/6-adapters/postgres/src/core/control-mutation-defaults.ts:141`. The generator table at `wip/prisma-src/packages/1-framework/2-authoring/ids/src/index.ts:57-64` names `uuidv7` and `uuidv4` but says nothing beyond storage width, so the page claims only that a version 7 value starts with its creation time and therefore sorts after older ids. It makes no indexing claim. +- Link targets verified to exist: `apps/docs/content/docs/orm/coming-from-prisma-orm-7.mdx`, `orm/extensions/index.mdx`, `orm/migrations/generating-a-migration.mdx`, and the `## Enums` heading in `orm/contract-authoring/psl-syntax.mdx`. + +## Q list + +None. Every mark was resolvable from the verified facts in the task or the source. + +Line count: 252 before, 258 after. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/index.round1.md b/docs/orm-docs-audit/c21-data-modeling/index.round1.md new file mode 100644 index 00000000000..6b5542923e1 --- /dev/null +++ b/docs/orm-docs-audit/c21-data-modeling/index.round1.md @@ -0,0 +1,67 @@ +Read once, top to bottom, as someone coming from Prisma 6. Report below. + +## Sentences I could not restate after one reading + +**Line 13** — "In Prisma ORM, you define this structure in your contract, the `contract.prisma` file that replaced `schema.prisma`." +Stopped by: "your contract." The page uses it as a noun I already own before it ever says what a contract is. "The file that replaced `schema.prisma`" tells me the filename changed but not whether the contents changed, whether my existing file still works, or whether something renames it for me. Plainest version I'd have understood: "Prisma 8 renames `schema.prisma` to `contract.prisma`. The file is called your contract. It holds the same model definitions you wrote before, plus X." + +**Line 13, second half** — "Prisma ORM derives your TypeScript types, your migrations, and the client's configuration from it." +Stopped by: "the client's configuration." In v6 my `generator` and `datasource` blocks lived in `schema.prisma`. Is "the client's configuration" those blocks? A new thing? The page shows four building blocks and none of them is configuration, so I can't place this. + +**Line 163** — "A `Decimal` value reaches your code as a decimal string, so binary floating point never loses digits. Write the type as `Numeric(10, 2)` when you want an explicit precision and scale. A `DateTime` value reaches your code as a `Temporal.Instant`, and a `Bytes` value as a `Uint8Array`. `Jsonb` is also the storage the TypeScript builder's `field.json()` and value objects use." +Four unrelated facts in one paragraph, and the last one I cannot resolve at all. "The TypeScript builder" is never introduced anywhere on the page and is not linked. "Value objects" likewise. `field.json()` is a method on something I have not been told exists. I got nothing from that sentence. +Also inside it: "Write the type as `Numeric(10, 2)`" — `Numeric` is not in the type table two lines above, which lists `Decimal`. Are they the same type? Two types? Is this `@db.Decimal(10, 2)` renamed? I could not tell. + +**Line 167** — "Prisma ORM's type system is extensible, so extensions can add more types, such as vectors or geometry." +"Extensions" here could mean Prisma Client extensions (which I used in v6 for query middleware, and which had nothing to do with schema types) or something new. No link. I'd cut it or link it. + +**Line 205** — "Widening a type later is a migration. The wider type today is free." +"Is free" I could not restate honestly. `BigInt` is not free in the client — it comes back as a JS `bigint`, which does not `JSON.stringify`. That's the cost I actually hit in v6, and the page says there isn't one. + +**Line 119** — "A UUID is wider than an integer and random UUIDs index a little worse, but independent services never collide and nothing is leaked." +"Random UUIDs" implies non-random ones exist and are better. The example right above is `@default(uuid())`. Which kind do I get? If there's a sortable option, the page tells me to want it and not how to get it. + +## Words and phrases I had to guess + +- **"contract"** — guessed: the new name for the schema, same concept. +- **"the client's configuration"** — guessed: the `generator` block. +- **"the TypeScript builder"** — guessed: some way of defining models in TypeScript instead of `.prisma` files? Genuinely a guess. +- **"value objects"** — guessed: no idea. Some Prisma 8 concept. +- **"`Temporal.Instant`"** — guessed: the TC39 Temporal API. Do I need Node 22+? A polyfill? Not said. +- **"`ObjectId`"** as a Prisma type (line 53) — guessed: replaces v6's `String @db.ObjectId`. +- **"`@@map("users")`"** (line 55) — guessed: table/collection name override. Appears in a code sample with zero mention in prose; it's the only attribute in the page that's shown without being explained. +- **"reference tables" / "lookup tables"** (lines 65, 74) — guessed from context; fine, but they're used as if already defined. +- **"skills"** (line 241) — guessed: agent instruction files. At least it's linked. + +## "So what do I actually type?" + +1. **Migrating an existing project.** The page says `contract.prisma` "replaced `schema.prisma`" and then never says what I do about it. Do I `git mv`? Does a codemod exist? Does Prisma read the old name? This is the single biggest gap for the audience that has a v6 app. +2. **Explicit precision.** "Write the type as `Numeric(10, 2)`" — write it *where*? `price Numeric(10, 2)`? `price Decimal @db.Numeric(10, 2)`? No code sample, and this is the one paragraph that tells me to type something and shows nothing. +3. **After I write the models.** The page ends at "Query your models." Nothing says how the contract reaches the database — no `prisma migrate dev`, no `prisma generate`, not even a link labeled as that step. +4. **MongoDB `_id`.** Line 53 shows `id ObjectId @id @map("_id")` with no `@default`. In v6 I wrote `@default(auto())`. Does Prisma 8 generate the ObjectId for me, or must I supply one on every `create`? The page states `_id` is mandatory and then hands me a model with no way to fill it. +5. **The other side of a relation.** The `Post` example (line 221) has `author User @relation(...)`. v6 required a matching `posts Post[]` on `User` or validation failed. The page never shows `User`, never mentions the back-relation, and says "Two kinds of field describe a relation" — which reads like two fields total, not two per side. I would have typed only the `Post` half and hit an error. +6. **Enums.** Line 245 tells my coding agent to find "fields that should be an enum," and the page never mentions enums — not in the type table, not in the "four things." Where do I declare one? +7. **Databases other than PostgreSQL.** The type table annotates every type with its PostgreSQL mapping only. I'm on MySQL. Does `Decimal` map? Does `Jsonb` exist? The page says "All four apply to every database" and then the only concrete table is PostgreSQL-specific. +8. **Decimal versus integer cents.** Line 155 offers `Decimal` as "exact decimal." Line 187 says "Money is not a `Float`" and tells me to store `priceCents Int`. So when do I use `Decimal`? The page gives me two answers for money and never reconciles them. + +## Internals I did not ask for + +- **Line 163**, `Jsonb` "is also the storage the TypeScript builder's `field.json()` and value objects use." That's a note about how another Prisma feature is implemented. I'm picking a column type. +- **Line 119**, "random UUIDs index a little worse." Useful as a tradeoff, but without a way to act on it it's just index internals. +- **Line 167**, the type system being "extensible." That's a statement about Prisma's architecture; it doesn't tell me to do anything. +- **Line 109**, "it leaks row counts and invites guessing" is fine — it changes my decision. + +## Could I do what the page is for, after one reading? + +Partly. I could write a plain PostgreSQL model with a surrogate key, pick sensible scalar types, and make a decent call between a natural and a surrogate key. The primary-key section is the strongest part and I'd have made the right choice from it. + +I could not: + +- Say what a "contract" is beyond a renamed file, or convert my existing v6 project to one. +- Write a working relation, because I was never shown the `User` side. +- Write a MongoDB model, because the `_id` example has no way to populate the id. +- Declare a decimal with precision, because the instruction has no syntax. +- Get from a finished contract to a database, because the page never names the command. +- Explain `Jsonb`'s relationship to whatever the "TypeScript builder" is — I still don't know what that is. + +The page's own framing is "four things." It covers three of them well enough to act on and leaves relations, the thing I actually came for, with half an example. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/mongodb.fix1.md b/docs/orm-docs-audit/c21-data-modeling/mongodb.fix1.md new file mode 100644 index 00000000000..d53c7d74d2f --- /dev/null +++ b/docs/orm-docs-audit/c21-data-modeling/mongodb.fix1.md @@ -0,0 +1,54 @@ +Done. 173 lines (was 167), budget 175. `check-plain.sh` clean, no em-dashes, no page-conventions banned words. + +## What I did with each mark + +**Sentences the reader could not restate** + +- **Line 87, pipeline builder / embedded filters.** Split into two plain facts and named the thing on first use: "`.where()` filters on top-level model fields only. To match on a field inside an embedded document, use the pipeline builder, the API for MongoDB aggregation pipelines, with a path such as `address.city`." +- **Line 118, `$lookup` denormalization paragraph.** Cut `$lookup` entirely and reduced the paragraph to two sentences with no sync pattern: "Resolving a reference costs a second read. If a read runs constantly, you can copy the few fields it needs into the parent document, such as a comment storing its author's name." +- **Line 150, unexplained `db`.** `db` now first appears in the `_id` section with the conventions wording: "`db` is the client you create once in `src/prisma/db.ts`, and `prisma orm init` writes that file. `db.orm` holds your models, and on MongoDB the accessor is the collection name, so `db.orm.users`." Plus the clause "The values go straight in, without the `data:` wrapper Prisma ORM 7 used." +- **Line 152, colon construction.** Now: "Use one collection when you almost always query the variants together, for example email, SMS, and push notifications that you read as a single feed." + +**Words guessed at** + +- **`ObjectId`**: "`ObjectId` is the type of the field. There is no `@default(auto())` and no `@db.ObjectId`." +- **pipeline builder**: defined on first use (above). +- **`@@discriminator(kind)`**: "takes the name of the field that records each document's variant, so write `kind` and not `"kind"`." +- **`@@base(...)`**: explanation now follows directly after the block, together with the new inheritance sentence. +- **`.include(...)`**: explained where it first appears, with a one-line example: "chain `.include("author")`, which adds the related record to every row you get back". +- **`prisma-8` skill**: now "the Prisma ORM skills, instruction files for coding agents". +- **discriminator field**: defined inline, "a field whose value says which kind each document is". +- **"no independent life" / "own life" metaphors**: both cut for the page's literal wording ("you cannot read embedded data without its parent"; "each record stands on its own"). +- **`@@map` optional but always used**: added the consequence, "and you reach it in code as `db.orm.user`". + +**"So what do I type"** + +1. `_id` generation: one-line `create` with `id` omitted, plus the sentence that MongoDB assigns it. +2. Create with an embedded value: two-line example passing a plain nested object. +3. Update one field inside an embedded value: `u("address.city").set("Hamburg")`. +4. Filter on an embedded field: stated plainly plus the pipeline-builder link. +5. Create a polymorphic document: `db.orm.posts.variant("Article").create({...})`, plus "Creating through a variant fills in `kind` for you" and "A variant has every field of its base plus its own, so an `Article` has `id`, `title`, `kind`, and `summary`". Also stated `.variant()` takes the model name, not the discriminator value. +6. Applying the schema: one sentence naming `npx prisma contract emit`, then `npx prisma migration plan` / `npx prisma db migrate`, with a link. +7. Indexes: "Indexes are declared on the model with `@@index([authorId])`", linked to PSL syntax. +8. Keeping the copy in sync: the paragraph no longer raises maintenance; it is two sentences and stops. + +**Mechanism cut** + +- `$lookup` gone. +- "Prisma ORM models this with a base model plus variant models" is now the first sentence of the polymorphic section, ahead of the MongoDB-shape sentence. +- 16 MB limit kept (converted from a `:::warning` block to a plain paragraph to reclaim lines). + +**Cuts made for the line budget:** the `Cart`/`CartItem` PSL block (folded into one sentence mentioning `items CartItem[]`), the duplicate "`Cart.items` is stored inside the cart document" paragraph, the `zip` field, the warning directive, and several paragraph merges. + +## Facts looked up (all in `wip/prisma-src/`) + +- Embedded value on `create` as a plain nested object, and dot-path update: `packages/2-mongo-family/5-query-builders/orm/test/integration/orm-ergonomics.test.ts:171-177` (`homeAddress: { city: 'SF', country: 'US' }`, then `u('homeAddress.city').set('NYC')`). +- Variant create fills in the discriminator: `packages/2-mongo-family/5-query-builders/orm/src/collection.ts:358` (`#injectDiscriminator`) and `.../test/integration/polymorphism.test.ts:133-153` ("variant create injects discriminator and persists it"). +- `.variant()` takes the model name: `.../test/integration/polymorphism.test.ts:100,143` (`variant('Bug')` against discriminator value `'bug'`). +- `.where()` is keyed on model fields only: `packages/2-mongo-family/5-query-builders/orm/src/types.ts:188-201`. +- `.include(relationName)` for reference relations: `collection.ts:98-101`. +- MongoDB migrations exist and `db migrate` runs `createCollection` / `createIndex`: `skills/prisma-8/references/migrations.md:14,34,48`. + +## Q list + +None. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/mongodb.round1.md b/docs/orm-docs-audit/c21-data-modeling/mongodb.round1.md new file mode 100644 index 00000000000..8fea2eeedaa --- /dev/null +++ b/docs/orm-docs-audit/c21-data-modeling/mongodb.round1.md @@ -0,0 +1,65 @@ +Read once, top to bottom. Here's what tripped me up. + +## Sentences I could not restate after one reading + +**Line 87:** "You can still match on its fields through the parent, with a path such as `address.city` in the [pipeline builder](...)." + +Two problems. I don't know what "the pipeline builder" is — it's linked but never described, and the sentence assumes I already know. Worse, the sentence implies that matching on an embedded field is something you do *in the pipeline builder*, i.e. not in an ordinary query. In the version I use I'd write a normal filter on a composite type field. Is that gone? Plainer: "You can filter on embedded fields in a normal query using X syntax" — and then show X. If the pipeline builder really is the only way, say that outright. + +**Line 118:** "A `$lookup` is a real cost on reads that run often, so document models often keep a small copy of the fields a read needs inside the parent." + +"document models often keep a small copy" — models don't keep copies, people do. And this is three ideas in one sentence: lookups are slow, therefore denormalize, and here's the general practice. Plainer: "Resolving a reference costs an extra query. If a read runs constantly, you can copy the few fields it needs into the parent document." Also, the whole paragraph gives me advice with no code — see below. + +**Line 150:** "`db.orm.posts.variant(\"Article\")` limits the query to one variant." + +I could not restate this because I don't know what `db` is, what `.orm` is, or where either comes from. In the version I've used for two years the client is `prisma.post.findMany()`. This is the only client code on the entire page and it arrives with no introduction. Is `db` something I import? Something generated? `orm` looks like a namespace I've never seen. + +**Line 152:** "Use one polymorphic collection when the variants are handled together far more than separately: a notifications collection of email, SMS, and push messages read as one stream." + +The colon-example construction made me re-read. The example is good; the sentence in front of it is compressed. Plainer: "Use one collection when you almost always query the variants together — for example, email, SMS, and push notifications that you read as a single feed." + +## Words and phrases I had to guess at + +- **`ObjectId`** (line 19) — I guessed this is a new scalar type in the schema language. In the version I know, you write `String @id @default(auto()) @map("_id") @db.ObjectId`. The page shows a bare `ObjectId` with no `@default`, and never says the type is new or that `@db.ObjectId` is gone. I guessed, and I'm not confident. +- **"the pipeline builder"** (lines 87, 166) — guessed: some API for writing MongoDB aggregation pipelines. Never stated on the page. +- **`@@discriminator(kind)`** (line 132) — guessed from the name. The page does explain it on line 150, but the unquoted `kind` next to `@@map("users")`'s quoted string made me guess whether the argument is a field reference or a string, and whether I'd get an error for writing `"kind"`. +- **`@@base(Post, "article")`** (line 139) — guessed: "Article is a subtype of Post, stored with `kind = "article"`." The explanation on line 150 confirms it, but only after two code blocks. +- **`.include(...)`** (line 165) — guessed this is a chained method. In the version I know, `include` is an option object key. The page uses method syntax in passing without flagging the change. +- **`prisma-8` skill** (line 156) — guessed: a named bundle of instructions for an AI coding agent. +- **"discriminator field"** (line 122) — guessed: a field whose value says which kind of document this is. Fine, but it's introduced as if standard vocabulary. +- **"no independent life" / "records with their own life"** (lines 32, 87) — guessed: can't be queried or updated on its own. The literal version is right there in the page (line 34, "each record stands on its own"); the metaphor is the weaker wording. + +## "So what do I actually type?" + +This is the page's biggest problem. There is **not one line of query code on the whole page** except the unexplained `db.orm.posts.variant("Article")`. + +1. **How do I get an `_id` generated?** The schema shows `id ObjectId @id @map("_id")` with no default. When I call create, do I have to supply an ObjectId myself? Does MongoDB fill it in? Does Prisma? The version I know requires `@default(auto())` and I'd get a runtime error without it. The page never says. +2. **How do I create a document with an embedded type?** Section "Embedded documents" defines `Address` and `CartItem` and then never shows a write. Do I pass a plain nested object? Is there a `create:` wrapper like relations have? +3. **How do I update one item inside an embedded array?** Line 32 says writes update "parent and children together, atomically" — great, but what do I type to change `items[2].amount`? +4. **How do I filter on an embedded field in a normal query?** See line 87 above. I'm told a path string and pointed at another page. +5. **How do I create a polymorphic document?** `Article` has `summary` and nothing else. Do I write `db.orm.articles.create({ title, summary })`? Does `kind` get set for me from `@@base`, or do I set it? Does `Article` have `title` and `id`, inherited from `Post`? The page says they "share one collection" but never says the variant model inherits the base model's fields — I'm inferring it. +6. **How do I apply this schema to MongoDB?** There are no migrations for MongoDB in the version I know; you run a push command. The page ends with a polymorphic schema and never tells me the command that makes it real. +7. **How do I declare the indexes line 152 tells me to think about?** "need very different indexes" is the only mention of indexes and there's no syntax anywhere. +8. **How do I keep the denormalized copy in sync** (line 118)? The page raises the technique and the maintenance cost, then stops. Either show the pattern or don't raise it. + +## Explaining the machinery when I wanted instructions + +- **Line 116:** "Prisma ORM resolves it with a `$lookup` aggregation." I don't need to know the MongoDB operator name to write a query. It's only justified if it sets up line 118 — and line 118 is itself advice I can't act on. +- **Line 122:** "MongoDB collections do not enforce a single shape. The usual choice is to store documents of different kinds in one collection..." This is a MongoDB-modeling lecture before I get told what Prisma does about it. Line 124 ("Prisma ORM models this with a base model plus variant models") is what I came for and it's the third sentence. +- **Line 91:** "pushes the document toward MongoDB's 16 MB limit" — this one earns its place; it's a hard limit I'd hit. Keeping it. +- **Line 26:** "Without it the collection takes the model name with a lowercase first letter, so `user`." Useful, but then every single example on the page uses `@@map` anyway, which made me wonder whether it's actually optional or just conventionally required. + +## Could I do what the page is for? + +Partly. I could **write a schema** — models with `ObjectId` ids, `type` blocks for embedded data, `@relation` for references. The embed-vs-reference table on line 38 is the best thing on the page; it answered the actual decision I came with, in one screen, with examples I recognized. + +I could **not** do anything with that schema. After one reading I still don't know: + +- Whether I need `@default(auto())` on the id, or what happens on create if I omit it. +- How to apply the schema to my database (no command anywhere). +- What `db.orm` is, or how to get one. +- How to write or read an embedded document — no client code exists on this page. +- How to filter on an embedded field without learning "the pipeline builder" first. +- Whether `Article` inherits `Post`'s fields, and how to create one. + +The page reads as a schema-design reference that has been labeled a data-modeling guide. Every time it approaches "and here's what you run," it hands off to a link (`/orm/fundamentals/reading-data`, `/orm/fundamentals/relations-and-joins`, the pipeline builder anchor) — five links, and I'd have to open at least three of them before I could run anything I modeled here. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/relational-databases.fix1.md b/docs/orm-docs-audit/c21-data-modeling/relational-databases.fix1.md new file mode 100644 index 00000000000..fe9610fae3c --- /dev/null +++ b/docs/orm-docs-audit/c21-data-modeling/relational-databases.fix1.md @@ -0,0 +1,56 @@ +Done. Page rewritten at `/Users/will/Projects/prisma/web/.claude/worktrees/pr-conflicts-review-fb93f7/wip/c21/dm/r1/relational-databases.mdx`. 226 lines before, 216 after (budget 232). `check-plain.sh` reports clean, no em-dashes, no semicolons in prose. + +## Marks and what I did + +**Sentences the reader could not restate** + +- **Line 81, one-to-one paragraph (six ideas, unexplained command and `PSL`).** Split into one-idea sentences with the rule before the error code: "It has to be optional, because nothing in the database guarantees a matching profile row. The foreign key it mirrors has to be `@unique`. If it is not, `npx prisma contract emit` reports an error whose `code` is `PSL_NON_UNIQUE_BACKRELATION`." +- **Line 46, "matched to the foreign key" (passive, actor hidden).** Now: "`npx prisma contract emit` matches it to the foreign key on `Post`." The two-relations case is answered in a new paragraph under "How relations are declared". +- **Line 136, how the list fields find `PostTag`.** Now names the actor and the target: "`npx prisma contract emit` matches them to the two relations on `PostTag`, so queries read `post.tags` directly. If a second join table connects the same two models, name the relation on both sides." +- **Line 198, `db.orm.public.Bug` vs `Task.variant("Bug")`.** The contradiction is gone. The page now says "Read and write variants through the base model", which is what the source shows. +- **Line 209, "keep their constraints" with an unstated contrast.** Now stated directly: shared-table columns "are nullable there"; own-table columns "can be `NOT NULL`, and reading a full variant costs a join". +- **Line 226, five bare referential-action names.** Each now has a short gloss (`Cascade` deletes or updates the matching rows, `Restrict` blocks the change, `SetNull` clears the foreign key, `SetDefault` writes its default value, `NoAction` leaves the decision to the database). + +**Words the reader had to guess** + +- `npx prisma contract emit`: introduced on first use with the conventions wording, plus the contract definition, in "How relations are declared". +- `db.orm.public`: explained at the first TypeScript example with the conventions wording, and the missing `data:` wrapper is flagged in the same sentence. +- Three names for one field: everything is "the mirror field" now, defined once as "the field on the other model, which stores nothing in the database". "Back-relation" no longer appears in prose (only inside error-code names). +- "the model for the join table": kept on first use, then `PostTag` or "the join table". +- "dependent side": the definition now arrives in the same sentence as the term. +- "discriminator": defined at first use ("One field on the base model, the discriminator, records which variant each row is"). + +**"So what do I type?"** + +1. Migration command added: `npx prisma migration plan --name add-posts` then `npx prisma db migrate`, with a link to `/orm/migrations/generating-a-migration`. +2. `.variant(...)` now says it takes the variant's model name, `"Bug"`, not the discriminator value `"bug"`. +3. Variant `@id`: "it needs no `@id` of its own, because it uses the base model's primary key". +4. Optional relations: new sentence pair with `authorId Int?` / `author User?` and the mismatch error code. +5. Mirror field optional to declare: stated in the one-to-many section. +6. Two relations between the same models: new paragraph with `@relation("Authored", ...)` and `PSL_AMBIGUOUS_BACKRELATION`. +7. "query from less often": cut. +8. The `(t) => ...` callback: "`t` is the argument of the nested write callback, and it offers `t.create([...])`, `t.connect([...])`, and `t.disconnect([...])`, each taking an array", with the link out. +9. `data:` wrapper: covered above. + +**Mechanism the reader did not need** + +- Storage layouts now end with a recommendation: "Start with the shared table. Give a variant its own table when its columns have to be `NOT NULL` in the database, or when the variant has many columns of its own." This follows from the two bullets above it; nothing in the source declares a default. +- "two one-to-many relations back to back": cut. +- The relational-theory sentence at line 111: cut to one sentence about needing a third model. +- Error codes now come after the rule everywhere. + +**Cuts that paid for the additions:** the repeated `Profile` code block under "Which side owns the foreign key", the second `PostTag` code block (the `addedAt` example is one prose sentence now), and the mechanism sentences listed above. + +## Facts looked up (rc.10 at `wip/prisma-src/`) + +- `.variant(...)` takes the variant model name: `VariantNames` resolves to the keys of the model's `variants` record (`packages/3-extensions/sql-orm-client/src/types.ts:554`), and the interpreter keys that record by model name (`packages/2-sql/2-authoring/contract-psl/src/interpreter.ts:1816`). Tests use `.variant('Bug')` / `.variant('Feature')` against `@@base(Task, "bug")` (`test/integration/test/sql-orm-client/polymorphism.test.ts:148,191`). +- Variant has base fields plus its own, and no own `@id`: `stripStorageOnlyDomainFields` with `syntheticPkFieldsByVariant` drops the link column from the variant's create/read input (`interpreter.ts:1878-1884`, comment at `interpreter.ts:1891-1905`); the test rows carry base fields plus the variant field (`polymorphism.test.ts:153-171`). +- `db.orm.public.Bug` vs `Task.variant("Bug")`: `OrmNamespace` exposes every model in the namespace by name (`packages/3-extensions/sql-orm-client/src/orm.ts:70-80`), and variant models stay in the contract, so the accessor type exists. Nothing in `collection.ts` or the integration tests reads or writes a variant through its own accessor: every case goes through the base collection with `.variant(...)`, and `collection.ts:1538` restricts some mutations on a variant-limited collection. So the page states only what the source shows. +- Ambiguous relations: `PSL_AMBIGUOUS_BACKRELATION`, fix is `@relation(name: "...")` or `@relation("...")` on both sides (`psl-relation-resolution.ts:417-441`). +- Nullability: `PSL_RELATION_NULLABILITY_MISMATCH` when the relation field and its foreign key fields disagree (`interpreter.ts:702-717`). +- Mirror field optional to declare: relations are built from the foreign-key side, and there is no missing-back-relation diagnostic anywhere in `contract-psl/src` (grep for `MISSING_BACKRELATION` / `NO_BACKRELATION` returns nothing). +- Shared-table columns are nullable in the table while the variant's own field keeps its declared nullability (`materializeStiVariantStorageColumns`, `interpreter.ts:1975-2030`); own-table variants get the base primary key as a link column with a foreign key back (`materializeMtiVariantStorageLinks`, `interpreter.ts:1890-1970`). + +## Q list + +None. Every fact the reader needed was in the source. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/relational-databases.round1.md b/docs/orm-docs-audit/c21-data-modeling/relational-databases.round1.md new file mode 100644 index 00000000000..73a0474f7bd --- /dev/null +++ b/docs/orm-docs-audit/c21-data-modeling/relational-databases.round1.md @@ -0,0 +1,69 @@ +Read once, top to bottom, as a Prisma 6/7 user seeing version 8 for the first time. + +## Sentences I could not restate after one reading + +**Line 81** — "The foreign key it mirrors must be unique, or `npx prisma contract emit` rejects the field with an error whose `code` is `PSL_NON_UNIQUE_BACKRELATION`." +Stopped by: `npx prisma contract emit` (never introduced anywhere on the page — I know `prisma generate` and `prisma migrate`, not this), and `PSL`. Also the whole paragraph is six ideas in one block: declare on the FK side, you may add a mirror field, it needs no `@relation`, it must be optional, why it must be optional, and the error you get if the FK isn't unique. +Plainest wording I'd have understood: "You can also add `profile Profile?` to `User`. It must be optional, and the `userId` field it points at must be `@unique`." + +**Line 46** — "it stores nothing in the database and is matched to the foreign key on `Post`." +Stopped by: matched *by whom*, and matched *how*? Passive voice hides the actor. If I have two relations between `User` and `Post` (author and editor), how does it know which is which? The page never says, and in the version I know that needed a named `@relation("...")`. + +**Line 136** — "The list fields on `Post` and `Tag` name the far side (`Tag[]`, `Post[]`), and `npx prisma contract emit` connects them through `PostTag`, so queries read `post.tags` directly." +Stopped by: same "how does it know" problem, now harder. `Post.tags` is typed `Tag[]`, but there is no `@relation` on it and `PostTag` is never named. If two join models existed I'd have no idea what happens. Also the unexplained command again. + +**Line 198** — "Variants appear as their own models in queries, `db.orm.public.Bug` alongside `db.orm.public.Task`. A query on the base model returns every variant. To write a variant, limit the query to one variant with `.variant(...)` and create through it." +Stopped by: it tells me `db.orm.public.Bug` exists, then the example never uses it and instead writes through `Task.variant("Bug")`. Can I call `db.orm.public.Bug.create(...)`? The page implies yes and then does the opposite. I could not restate what the rule is. + +**Line 209** — "That table holds its extra columns plus the base model's primary key, which links it back to the base table. Its columns keep their constraints, at the cost of a join to read a full variant." +Stopped by: "keep their constraints" — constraints compared to what? I have to reconstruct it from the previous bullet's "have to be nullable there." Two bullets carrying an implicit contrast that's never stated as a contrast. + +**Line 226** — "Referential actions: add `onDelete` and `onUpdate` to `@relation` with `Cascade`, `Restrict`, `SetNull`, `SetDefault`, or `NoAction`, as in `@relation(fields: [userId], references: [id], onDelete: Cascade)`. They become the foreign key's `ON DELETE` and `ON UPDATE` clauses." +Stopped by: five bare enum values with no hint of what any of them does, crammed into a bullet under "Next steps." I can't restate what `SetDefault` does. Either explain them or link out; a naked list teaches nothing. + +## Words and phrases I had to guess + +- **`npx prisma contract emit`** — guessed it's the version 8 replacement for `prisma generate`, i.e. it validates the schema and writes the typed client. The page uses it three times (lines 81, 136, 164) as the thing that reports schema errors and never once says what it is or when I run it. +- **`PSL`** in `PSL_NON_UNIQUE_BACKRELATION` / `PSL_ORPHANED_BACKRELATION` — guessed "Prisma Schema Language." +- **`db.orm.public`** — guessed `public` is the Postgres schema name and `orm` separates this from some other namespace. Pure guessing; it appears in every TypeScript example and is never explained. +- **"back-relation"** (line 46) vs **"the mirror field"** (line 81) vs **"list fields"** (line 136) — I guessed these are all the same concept: the field that has no column behind it. Three names for one thing in one page. +- **"the model for the join table"** (line 111) — guessed this is what used to be called an explicit many-to-many join model. The phrase is used five times and reads like a mouthful each time. +- **"dependent side"** (line 85) — guessed correctly from the bullets that follow, but the term arrives before the explanation. +- **"discriminator"** (line 171) — guessed: the column storing which variant a row is. The page does define it at line 196, one paragraph after first use. + +## "So what do I actually type?" + +1. **After I edit the schema, what command do I run to get the tables?** The page never mentions a migration step. `npx prisma contract emit` is presented only as the thing that *rejects* bad schemas. I've written a schema and have no idea what to run. +2. **`.variant("Bug")` — is that the model name or the discriminator value?** `@@base(Task, "bug")` declares the string `"bug"`, and then line 201 types `.variant("Bug")`. Two different strings, one capitalized, one not, and no statement of which one `.variant()` wants. This is the single worst spot on the page: I would type the wrong thing. +3. **Does `Bug` need its own `id`?** The `Bug` and `Feature` models have no `@id`. I assume it's inherited from `Task`, but nothing says so, and every other model on the page declares one. +4. **How do I make a relation optional?** A post with no author, `authorId Int?` — the single most common thing after the basic case, and it does not appear anywhere. +5. **Is the back-relation required?** `User` has `posts Post[]` in the one-to-many example. `User` has no `profile` field in the one-to-one example, and the prose says you "can" add it. So is the list field on the one-to-many side optional too, or required? Not stated. +6. **Two relations between the same two models.** Nothing on naming relations. In the version I know this was mandatory syntax; if version 8 changed it, I need to be told. +7. **Line 105: "put the key on the side you query from less often."** No reason given, so I can't judge whether the advice applies to me, and I don't know what "query from" means when both sides are readable. +8. **`tags: (t) => t.connect([{ id: tag.id }])`** — the arrow-function form is brand new to me and appears with no explanation. What else can `t` do? Is there a `disconnect`? Is the array required for one item? +9. **`Post.create({ title, authorId })`** — I expected `data: { ... }`. The wrapper is gone. The page shows it but never flags it as a change, so I'd assume I'd misread. + +## Mechanism where I wanted instructions + +- **Lines 206–209, the two storage layouts.** I asked "how do I add a variant" and got a lecture on table-per-hierarchy vs table-per-type. Worse, the switch between them is `@@map` — a directive that in the version I know just renames a table. Making a table name also decide the physical layout is surprising, and the page presents it as a fact to absorb rather than a choice with a recommendation. Tell me which one to use by default. +- **Line 136, "it is two one-to-many relations back to back."** That's how the tool thinks about it internally. What I needed was: here's the model, here's what you type to connect a pair. +- **Line 111, "A single foreign key cannot express this, because each side needs to point at many records."** Relational-database theory I already have. One sentence about needing a third model would do. +- **The error codes** (`PSL_NON_UNIQUE_BACKRELATION`, `PSL_ORPHANED_BACKRELATION`). Telling me the internal error identifier before telling me the rule inverts the order. State the rule; the code is noise unless I'm already staring at it. +- **Line 164** is the exception and is the best-written passage on the page: it names the thing I'd try from the old version, says it's gone, and says what to do instead. More of that. + +## Could I do what the page is for, after one reading? + +Partly. I could write a one-to-many and a one-to-one correctly, and I could write the many-to-many schema. Everything past the schema, I could not. + +What I would still not know: + +- What command turns my edited schema into actual database tables. The page never names a migration step. +- What `npx prisma contract emit` is, even though it is the page's authority for three separate rules. +- What `db.orm.public` means, so I couldn't adapt any TypeScript example to my own project with confidence. +- Which string `.variant()` takes. I'd get this wrong on the first try. +- How to make any relation optional. +- How to model two relations between the same pair of models. +- Whether to write a variant through `db.orm.public.Bug` or `Task.variant(...)`. +- What any of the five referential actions do. + +The schema half of this page works. The "and then you use it" half assumes I already know the version 8 client, and I don't — that's the whole reason I'm reading a version 8 page. \ No newline at end of file From e47b01cdbc9ee9f54a6fd47e13e16b234f8c02b0 Mon Sep 17 00:00:00 2001 From: willbot Date: Mon, 14 Sep 2026 17:13:05 +0200 Subject: [PATCH 04/15] docs(orm): data-modeling reader review round two Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- .../content/docs/orm/data-modeling/index.mdx | 48 ++++---- .../docs/orm/data-modeling/mongodb.mdx | 62 +++++----- .../data-modeling/relational-databases.mdx | 55 +++++---- .../c21-data-modeling/index.fix2.md | 56 +++++++++ .../c21-data-modeling/index.round2.md | 111 ++++++++++++++++++ .../c21-data-modeling/mongodb.fix2.md | 45 +++++++ .../c21-data-modeling/mongodb.round2.md | 90 ++++++++++++++ .../relational-databases.fix2.md | 54 +++++++++ .../relational-databases.round2.md | 73 ++++++++++++ 9 files changed, 511 insertions(+), 83 deletions(-) create mode 100644 docs/orm-docs-audit/c21-data-modeling/index.fix2.md create mode 100644 docs/orm-docs-audit/c21-data-modeling/index.round2.md create mode 100644 docs/orm-docs-audit/c21-data-modeling/mongodb.fix2.md create mode 100644 docs/orm-docs-audit/c21-data-modeling/mongodb.round2.md create mode 100644 docs/orm-docs-audit/c21-data-modeling/relational-databases.fix2.md create mode 100644 docs/orm-docs-audit/c21-data-modeling/relational-databases.round2.md diff --git a/apps/docs/content/docs/orm/data-modeling/index.mdx b/apps/docs/content/docs/orm/data-modeling/index.mdx index 29f40d6bcc6..daebf874456 100644 --- a/apps/docs/content/docs/orm/data-modeling/index.mdx +++ b/apps/docs/content/docs/orm/data-modeling/index.mdx @@ -10,9 +10,9 @@ Data modeling is the process of describing the data your application needs and h For example, a blog has users, posts, and comments. A user has fields like an email and a name. A post has fields like a title and content. These models also relate to each other: a user can write many posts, and a post can have many comments. -In Prisma ORM, you define this structure in your contract, the `contract.prisma` file that replaced `schema.prisma`. You still describe your models in it. Prisma ORM derives your TypeScript types and your migrations from it. If you are coming from Prisma ORM 7, [what changed](/orm/coming-from-prisma-orm-7) covers the rename and the rest of the differences. +In Prisma ORM, you define this structure in your contract, the `contract.prisma` file that replaced `schema.prisma`. You still describe your models in it. Prisma ORM derives your TypeScript types and your migrations from it. There is no `datasource` or `generator` block any more, and the connection string is in `prisma.config.ts` instead. [A complete contract](/orm/contract-authoring/psl-syntax#a-complete-contract) shows a whole file next to its config. If you are coming from Prisma ORM 7, [what changed](/orm/coming-from-prisma-orm-7) covers the rename and the rest of the differences. -Every contract is built from four things: +A contract is built from four building blocks: - [Models](#models): the things your application works with - [Primary keys](#primary-keys): how each record is identified @@ -33,13 +33,13 @@ model User { } ``` -Every record of a model has its own identity and its own history. Two posts with the same title are still two different posts, and a user who changes their email address is still the same user. +Two posts with the same title are still two different records, and a user who changes their email address is still the same user. On a relational database, a model becomes a table. On MongoDB, it becomes a collection. ## Primary keys -A primary key is the field that uniquely identifies each record. Give every model one, because Prisma ORM needs a primary key to update or delete a record. Mark it with `@id`: +A primary key is the field that uniquely identifies each record. Give every model one. `npx prisma contract emit` accepts a model without a primary key. The ORM client does not: `update` and `delete` need a primary key or a unique field, and on a model with neither they fail with an error whose `code` is `ORM.ROW_IDENTITY_MISSING`. Mark the primary key with `@id`: ```prisma tab="PostgreSQL" model User { @@ -56,13 +56,13 @@ model User { } ``` -On MongoDB, the primary key is the document's mandatory `_id` field, and it must be an `ObjectId`. `@map("_id")` gives the field that name in the database, and `@@map("users")` names the collection the model is stored in. Leave `id` out of the `create` call and MongoDB assigns the `ObjectId` for you. +On MongoDB, the primary key is the document's mandatory `_id` field, and it must be an `ObjectId`. `@map("_id")` gives the field that name in the database. The `create` input type makes `id` optional, so leaving it out type-checks and MongoDB assigns the `ObjectId` for you. `@@map("users")` names the collection and is optional: without it, the collection is the model name with a lowercase first letter. ### Natural keys A natural key is a value that already identifies the record in the real world. Use one when the value is stable, unique, and assigned outside your application: a standardized code you receive rather than invent. -A reference table, one that holds a fixed set of values the rest of your data points at, is the classic fit. A country is its ISO code, and the code never changes: +Natural keys fit reference tables best: tables that hold a fixed set of values other records point at. You can identify a country by its ISO code, and the code never changes: ```prisma model Country { @@ -115,9 +115,9 @@ model ApiToken { } ``` -A UUID takes more space than an integer, but independent services never produce the same one and the value tells nobody how many records you have. `@default(uuid())` gives you a random UUID. Write `@default(uuid(7))` instead for a UUID that starts with the time it was created, so new ids sort after older ones. +A UUID takes more space than an integer, but independent services never produce the same one and the value tells nobody how many records you have. `@default(uuid())` and `@default(uuid(4))` both give you a random UUID. Write `@default(uuid(7))` instead for a UUID that starts with the time it was created, so new ids sort after older ones. -On MongoDB the choice is made for you. The key is the `ObjectId` shown above, which MongoDB generates without asking any other server and which carries its own creation time. +On MongoDB the choice is made for you. The key is the `ObjectId` shown above. ### Composite keys @@ -132,7 +132,7 @@ model UserTag { } ``` -A duplicate `(userId, tagId)` pair would be meaningless, so the pair is the key. Avoid composite keys for ordinary models: everything that points at the model then has to store all of the key's fields. +A duplicate `(userId, tagId)` pair would be meaningless, so the pair is the key. The two relation fields that point back at `User` and `Tag` are left out here to keep the key in view, and [many-to-many](/orm/data-modeling/relational-databases#many-to-many) shows the complete model. Avoid composite keys for ordinary models: everything that points at the model then has to store all of the key's fields. ## Scalar fields @@ -152,13 +152,13 @@ A scalar field holds a single value. The common types: | `Jsonb` | Arbitrary JSON value, PostgreSQL's `jsonb` | | `ObjectId` | MongoDB document identifier | -The column names in the table are PostgreSQL columns. MongoDB has no such mapping, and `ObjectId` is the only type in the table that is specific to it. +Where a row above names a PostgreSQL type such as `numeric` or `jsonb`, that is the column type Prisma ORM uses on PostgreSQL. MongoDB has no such mapping, and `ObjectId` is the only type in the table that is specific to it. -A `Decimal` value reaches your code as a decimal string, so binary floating point never loses digits. Write `price Decimal` when any precision will do, and `price Numeric(10, 2)` when you want an explicit precision and scale. Both become a PostgreSQL `numeric` column. +A `Decimal` value reaches your code as a string, so nothing is rounded through a JavaScript number. Write `price Decimal` when any precision will do. Write `price Numeric(10, 2)` when you want an explicit precision and scale. `Numeric` is `Decimal` with the number of digits written out. Both become a PostgreSQL `numeric` column. -A `DateTime` value reaches your code as a `Temporal.Instant`, the standard JavaScript type for a point in time. [Raw queries](/orm/reference/raw-queries) says which Node.js versions have `Temporal` built in and how to add it to older ones. A `Bytes` value reaches your code as a `Uint8Array`. +A `DateTime` value reaches your code as a `Temporal.Instant`, the standard JavaScript type for a point in time. `Temporal` is built into Node.js 26.8.2 and later. On older versions, install the `temporal-polyfill` package. A `Bytes` value reaches your code as a `Uint8Array`. -Filtering on a `Jsonb` field is whole-value equality only: you can ask whether the column equals a given document, not whether a key inside it matches. A `Json` field offers no comparison at all in the ORM filter. Reach into the document with [raw SQL](/orm/reference/raw-queries) when you need a path query. +Filtering on a `Jsonb` field is whole-value equality only: you can ask whether the column equals a given document, not whether a key inside it matches. A `Json` field offers no comparison at all in the ORM filter, so use `Jsonb` unless you specifically need PostgreSQL's plain `json` column. Reach into the document with [raw SQL](/orm/reference/raw-queries) when you need a path query. [Extensions](/orm/extensions) add more types, such as vectors or geometry. @@ -167,7 +167,7 @@ Two modifiers change a field: - A trailing `?` makes the field optional: `name String?` - A trailing `[]` makes it a list: `tags String[]` -A field can also hold one of a fixed set of values, which you declare as an [enum](/orm/contract-authoring/psl-syntax#enums). +A field can also hold one of a fixed set of values. You declare those values in an `enum` block that lists each member and says how the members are stored, and [enums](/orm/contract-authoring/psl-syntax#enums) shows the block to copy. ### How to pick a data type @@ -182,7 +182,7 @@ model Address { } ``` -Money is not a `Float`. Floating-point numbers cannot represent decimal amounts exactly, so sums drift by fractions of a cent. Store an integer number of the smallest unit instead. `Decimal` is exact as well, so use it when your amounts need more decimal places than the currency's smallest unit, and use `Int` otherwise because a plain JavaScript number is easier to work with than a decimal string: +Money is not a `Float`. Floating-point numbers cannot represent decimal amounts exactly, so sums drift by fractions of a cent. For ordinary money, use `Int` and store the amount in the currency's smallest unit, such as cents, because you get a plain JavaScript number. Use `Decimal` only when you need more decimal places than the currency has, such as a per-unit rate: ```prisma model Product { @@ -208,7 +208,7 @@ Mark a field optional (`?`) only when "absent" means something different from a A relation connects two models: a user has many posts, a post belongs to one user. -Two kinds of field describe a relation, and each model in it needs the fields for its own side: +Two kinds of field describe a relation. The model that holds the link needs both of them. The model on the other end needs only a relation field: - The field that stores the other record's primary key, like `authorId`. On a relational database this is the foreign key column, and on MongoDB it is the field holding the other document's `_id`. - A relation field, typed as the other model, like `author User`. It stores nothing in the database. It tells Prisma ORM how to follow the connection in a query. @@ -228,22 +228,22 @@ model Post { } ``` -`posts Post[]` on `User` is the other end of the same relation, and like `author` it stores nothing in the database. +`posts Post[]` on `User` is the other end of the same relation. Relations come in three kinds: -- One-to-one: a user has at most one profile. -- One-to-many: a user has many posts. -- Many-to-many: a post has many tags, and a tag appears on many posts. +- One-to-one: a user has at most one profile. Write it as a one-to-many and add `@unique` to the field that stores the link. +- One-to-many: a user has many posts, as in the example above. +- Many-to-many: a post has many tags, and a tag appears on many posts. This needs a model for the join table, with one record per connected pair. How each kind is stored, and which model should hold the connecting field, is where relational and document databases differ. Continue with the guide for your database: -- [Relational data modeling](/orm/data-modeling/relational-databases) for PostgreSQL and other SQL databases: foreign keys, the model for the join table, and which side owns the key. +- [Relational data modeling](/orm/data-modeling/relational-databases) for PostgreSQL: foreign keys, the model for the join table, and which side owns the key. - [MongoDB data modeling](/orm/data-modeling/mongodb): embedding versus referencing, and polymorphic collections. ## Prompt your coding agent -Projects created with `npm create prisma@latest` include the [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8) for your coding agent. In an existing project, run `npx prisma skills sync`. The `prisma-8` skill covers contract authoring. Prompts that map to each section: +Projects created with `npm create prisma@latest` include the [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8), instruction files for coding agents. In an existing project, run `npx prisma skills sync`. The `prisma-8` skill covers contract authoring. Prompts that map to each section: - "Using the prisma-8 skill, add a Product model with a surrogate id and a unique sku field." - "Add a Country reference table keyed by its ISO code." @@ -252,7 +252,7 @@ Projects created with `npm create prisma@latest` include the [Prisma ORM skills] ## Next steps -- [Model relational data](/orm/data-modeling/relational-databases): one-to-one, one-to-many, many-to-many, and polymorphic relations on SQL databases. +- [Model relational data](/orm/data-modeling/relational-databases): one-to-one, one-to-many, many-to-many, and polymorphic relations on PostgreSQL. - [Model MongoDB data](/orm/data-modeling/mongodb): embed or reference, and polymorphic collections. -- [Generate a migration](/orm/migrations/generating-a-migration) to get the models you wrote into the database: run `npx prisma contract emit` after every change to the contract, then plan and apply a migration. +- [Generate a migration](/orm/migrations/generating-a-migration) to get the models you wrote into the database. Run `npx prisma contract emit` after every change to the contract. Then `npx prisma migration plan --name ` writes a migration, and `npx prisma db migrate` applies it. - [Query your models](/orm/fundamentals/reading-data) once your contract is in place. diff --git a/apps/docs/content/docs/orm/data-modeling/mongodb.mdx b/apps/docs/content/docs/orm/data-modeling/mongodb.mdx index f216f55ff7f..5f18e465115 100644 --- a/apps/docs/content/docs/orm/data-modeling/mongodb.mdx +++ b/apps/docs/content/docs/orm/data-modeling/mongodb.mdx @@ -10,7 +10,7 @@ MongoDB stores data as documents grouped into collections. A document can nest o ## Documents, collections, and the _id key -A Prisma ORM model maps to a collection, and each record is a document. Every document has an `_id` field as its primary key. `ObjectId` is the type MongoDB expects for it: +There is no `datasource` block on MongoDB. Your `prisma.config.ts` file holds the connection, using `defineConfig` from `@prisma/orm-mongo/config`. See [the data contract](/orm/contract-authoring/the-data-contract). A Prisma ORM model maps to a collection, and each record is a document. Every document has an `_id` field as its primary key. `ObjectId` is the type MongoDB expects for it: ```prisma model User { @@ -21,25 +21,25 @@ model User { } ``` -`@map("_id")` maps the model's `id` field to MongoDB's mandatory `_id` field, and `@@map("users")` names the collection. `@@map` is optional. Without it the collection takes the model name with a lowercase first letter, so `user`, and you reach it in code as `db.orm.user`. - -`ObjectId` is the type of the field. There is no `@default(auto())` and no `@db.ObjectId`. Leave `id` out of the values you pass to `create`, and MongoDB assigns the `ObjectId`: +`@map("_id")` maps the model's `id` field to MongoDB's mandatory `_id` field, and `@@map("users")` names the collection. `@@map` is optional. Without it the collection takes the model name with a lowercase first letter, so `user`. `ObjectId` replaces `String @db.ObjectId` and `@default(auto())`. Writing either of those attributes is an error whose `code` is `PSL_UNSUPPORTED_FIELD_ATTRIBUTE`. Leave the id out of the values you pass to `create`, and MongoDB assigns it: ```ts const user = await db.orm.users.create({ name: "Ada", email: "ada@example.com" }); ``` -`db` is the client you create once in `src/prisma/db.ts`, and `prisma orm init` writes that file. `db.orm` holds your models, and on MongoDB the accessor is the collection name, so `db.orm.users`. The values go straight in, without the `data:` wrapper Prisma ORM 7 used. Models themselves go in your contract, the `contract.prisma` file that replaced `schema.prisma`. Run `npx prisma contract emit` after every change to it. MongoDB has migrations like any other database: `npx prisma migration plan` writes one and `npx prisma db migrate` applies it, creating the collections and indexes. See [the data contract](/orm/contract-authoring/the-data-contract). +`db` is the client you create once in `src/prisma/db.ts`, and `npx prisma orm init` writes that file. `db.orm.users` is the accessor for the collection, and it follows the collection name, so `@@map` renames the accessor as well. In Prisma ORM 7 it did not. The values go straight into `create`, without the `data:` wrapper Prisma ORM 7 used. The id comes back as a string, and you pass it back as a string. There is no `ObjectId` to construct: -## Embed or reference +```ts +const found = await db.orm.users.where({ _id: user._id }).first(); +``` -Two pieces of related data can live together in one document (embedded) or in separate collections linked by an id (referenced). +You `await` the whole chain, and the last call says what you want back: `.first()` for one document or `null`, `.all()` for every matching document. -Embedding nests the data inside its parent: an order and its line items in one document. One read returns everything, and a write updates parent and children together, atomically. The trade-off is that you cannot read embedded data without its parent, and it comes back on every read of the parent. +## Embed or reference -Referencing keeps the data in its own collection and stores the linked document's `_id`. Loading both takes a second read, but each record stands on its own: it can be queried, listed, and updated independently, and it is not duplicated when several parents point at it. +Two pieces of related data can live together in one document (embedded) or in separate collections linked by an id (referenced). Embedding nests the data inside its parent: an order and its line items in one document. One read returns everything, and a write updates parent and children together, atomically. The trade-off is that you cannot read embedded data without its parent, and it comes back on every read of the parent. -A quick way to decide: +Referencing keeps the data in its own collection and stores the linked document's `_id`. Loading both takes a second read, but each record stands on its own: it can be queried, listed, and updated independently, and it is not duplicated when several parents point at it. A quick way to decide: | Signal | Example | Lean toward | | ------ | ------- | ----------- | @@ -54,7 +54,7 @@ A blog post's comments make it concrete. If you always render them with the post ## Embedded documents -Describe the shape of embedded data with a `type` block, then use it as a field. An embedded type has no collection of its own. It exists only inside its parent document. +Describe the shape of embedded data with a `type` block, then use it as a field. An embedded value has no `_id` and no collection of its own. It is created, loaded, updated, and deleted with the document that holds it. ```prisma type Address { @@ -62,33 +62,37 @@ type Address { city String country String } - model User { id ObjectId @id @map("_id") name String + tags String[] address Address? @@map("users") } ``` -A single embedded value models a one-to-one. A list of embedded values models a one-to-many: `items CartItem[]` on a `Cart` model stores every line item inside the cart document, with no second collection and no join to load them. - -An embedded value has no `_id` and no collection of its own. It is created, loaded, updated, and deleted with the document that holds it. Pass it to `create` as a plain nested object: +A single embedded value models a one-to-one. A list of embedded values models a one-to-many: `addresses Address[]` stores every address inside the user document, with no second collection and no join to load them. Pass an embedded value to `create` as a plain nested object: ```ts const address = { street: "1 Main St", city: "Berlin", country: "DE" }; -await db.orm.users.create({ name: "Ada", email: "ada@example.com", address }); +await db.orm.users.create({ name: "Ada", address }); ``` -To change one field inside an embedded value, give `update` the path to it: +In an `update`, `u` gives you each field of the document to change. Give it the path to change one field inside an embedded value, and use `.push(...)` and `.pull(...)` to add to and remove from a list of scalars: ```ts -await db.orm.users.where({ email: "ada@example.com" }).update((u) => [u("address.city").set("Hamburg")]); +await db.orm.users.where({ _id: user._id }).update((u) => [u("address.city").set("Hamburg")]); +await db.orm.users.where({ _id: user._id }).update((u) => [u.tags.push("admin"), u.tags.pull("new")]); ``` -`.where()` filters on top-level model fields only. To match on a field inside an embedded document, use the pipeline builder, the API for MongoDB aggregation pipelines, with a path such as `address.city`. See [the pipeline builder](/orm/fundamentals/advanced-queries#mongodb-pipeline-builder). +`.where()` filters on top-level model fields only. To match on a field inside an embedded document, use the pipeline builder, a chained builder for MongoDB aggregation pipelines: + +```ts +const berliners = db.query.from("users").match((f) => f("address.city").eq("Berlin")).build(); +const rows = await (await db.runtime()).query(berliners); +``` -Embedded arrays are only a good idea while they stay bounded. An array that grows forever slows every read of the parent and pushes the document toward MongoDB's 16 MB limit. When a list has no natural cap, reference it instead. +See [the pipeline builder](/orm/fundamentals/advanced-queries#mongodb-pipeline-builder) for the rest of the stages. Embedded arrays are only a good idea while they stay bounded. An array that grows forever slows every read of the parent and pushes the document toward MongoDB's 16 MB limit, and a write that exceeds it fails. When a list has no natural cap, reference it instead. ## References across collections @@ -101,7 +105,6 @@ model User { posts Post[] @@map("users") } - model Post { id ObjectId @id @map("_id") title String @@ -111,43 +114,39 @@ model Post { } ``` -This is a one-to-many by reference: many posts point at one user, and each post is queryable on its own. To load a post together with its author, chain `.include("author")`, which adds the related record to every row you get back: +This is a one-to-many by reference: many posts point at one user, and each post is queryable on its own. To link a new post to a user, set the reference field yourself. To load a post together with its author, chain `.include("author")`, which adds the related record to every row you get back: ```ts +await db.orm.posts.create({ title: "Hello", authorId: user._id }); const posts = await db.orm.posts.include("author").all(); ``` -See [relations and joins](/orm/fundamentals/relations-and-joins) for the rest of the query API. Resolving a reference costs a second read. If a read runs constantly, you can copy the few fields it needs into the parent document, such as a comment storing its author's name. +See [relations and joins](/orm/fundamentals/relations-and-joins) for the rest of the query API. Resolving a reference costs a second read. If a read runs often, you can copy the few fields it needs into the parent document, such as a comment storing its author's name. The copy then goes stale on its own, and keeping it up to date is your job. ## Polymorphic collections -Prisma ORM models a collection that holds more than one kind of document with a base model plus variant models, all sharing one collection. MongoDB collections do not enforce a single shape, so this is the usual way to keep the kinds together and tell them apart by a discriminator field, a field whose value says which kind each document is. +One collection can hold more than one kind of document. You declare a base model for the shared fields and a variant model for each kind. A discriminator field tells the kinds apart: its value says which kind each document is. ```prisma model Post { id ObjectId @id @map("_id") title String kind String - @@discriminator(kind) @@map("posts") } - model Article { summary String - @@base(Post, "article") } - model Tutorial { difficulty String duration Int - @@base(Post, "tutorial") } ``` -`@@discriminator(kind)` takes the name of the field that records each document's variant, so write `kind` and not `"kind"`. `@@base(Post, "article")` declares `Article` as the `"article"` variant of `Post`. A variant has every field of its base plus its own, so an `Article` has `id`, `title`, `kind`, and `summary`. +`@@discriminator(kind)` takes the name of the field that records each document's variant, so write `kind` and not `"kind"`. `@@base(Post, "article")` declares `Article` as the `"article"` variant of `Post`. A variant declares no `@@map` of its own and uses its base's collection. A variant has every field of its base plus its own, so an `Article` has `id`, `title`, `kind`, and `summary`. A query for posts returns articles and tutorials together. `db.orm.posts.variant("Article")` limits the query to one variant. `.variant()` takes the variant's model name, `"Article"`, and not the discriminator value `"article"`. Creating through a variant fills in `kind` for you: @@ -155,7 +154,7 @@ A query for posts returns articles and tutorials together. `db.orm.posts.variant const article = await db.orm.posts.variant("Article").create({ title: "Hello", summary: "A short post" }); ``` -Use one collection when you almost always query the variants together, for example email, SMS, and push notifications that you read as a single feed. Prefer separate collections when the types rarely appear in the same query or need very different indexes. Indexes are declared on the model with `@@index([authorId])`, which is covered in [PSL syntax](/orm/contract-authoring/psl-syntax). +Use one collection when you almost always query the variants together, for example email, SMS, and push notifications that you read as a single feed. Prefer separate collections when the types rarely appear in the same query or need very different indexes. ## Prompt your coding agent @@ -168,6 +167,7 @@ Projects created with `npm create prisma@latest` include the [Prisma ORM skills] ## Next steps +- Models go in your contract, the `contract.prisma` file that replaced `schema.prisma`. Run `npx prisma contract emit` after every change to it, then `npx prisma migration plan --name ` writes a migration and `npx prisma db migrate` applies it, creating the collections and indexes. See [generating a migration](/orm/migrations/generating-a-migration). - [Query documents](/orm/fundamentals/reading-data) and [resolve references](/orm/fundamentals/relations-and-joins) with `.include(...)`. - Aggregate and reshape documents with the [pipeline builder](/orm/fundamentals/advanced-queries#mongodb-pipeline-builder). - To model relational data, see the [relational data modeling guide](/orm/data-modeling/relational-databases). diff --git a/apps/docs/content/docs/orm/data-modeling/relational-databases.mdx b/apps/docs/content/docs/orm/data-modeling/relational-databases.mdx index 5bc1673f5c0..5bee0a4dffb 100644 --- a/apps/docs/content/docs/orm/data-modeling/relational-databases.mdx +++ b/apps/docs/content/docs/orm/data-modeling/relational-databases.mdx @@ -12,7 +12,7 @@ If you are new to models and keys, start with the [data modeling overview](/orm/ ## How relations are declared -The model that stores the connection has two fields. One is a scalar field for the foreign key column. The other is a relation field, typed as the other model. `@relation(fields:, references:)` ties them together: +The model whose table gets the foreign key column declares two fields. One is a scalar field for the foreign key column. The other is a relation field, typed as the other model. `@relation(fields:, references:)` ties them together: ```prisma model Post { @@ -22,17 +22,17 @@ model Post { } ``` -The side that declares the `fields` argument owns the foreign key. Which side that should be is the main decision in the shapes below. +The side that declares the `fields` argument owns the foreign key. Deciding which side that is is the main choice in each of the relation kinds below. -Run `npx prisma contract emit` after every change to your contract, the `contract.prisma` file that replaced `schema.prisma`. It checks the contract and writes `contract.json` and `contract.d.ts` beside it. To create the tables themselves, run `npx prisma migration plan --name add-posts` and then `npx prisma db migrate`, as described in [Generating a migration](/orm/migrations/generating-a-migration). +Your contract is the `contract.prisma` file that replaced `schema.prisma`. After every change to it, run `npx prisma contract emit`. That command checks the contract and regenerates your types. It only updates your code. To change the database, run `npx prisma migration plan --name add-posts`, which writes a migration file, and then `npx prisma db migrate`, which applies that file. See [Generating a migration](/orm/migrations/generating-a-migration). -To let a post have no author, make the foreign key field and the relation field both optional: `authorId Int?` and `author User?`. Making one optional and the other required is rejected with an error whose `code` is `PSL_RELATION_NULLABILITY_MISMATCH`. +To let a post have no author, make the foreign key field and the relation field both optional: `authorId Int?` and `author User?`. Making one optional and the other required is rejected with an error whose `code` is `PSL_RELATION_NULLABILITY_MISMATCH`. `PSL` is the Prisma Schema Language, the language `contract.prisma` is written in. Two relations between the same pair of models need a name on both sides, such as `@relation("Authored", fields: [authorId], references: [id])` on one side and `@relation("Authored")` on the other. Without the name, `npx prisma contract emit` reports an error whose `code` is `PSL_AMBIGUOUS_BACKRELATION`. ## One-to-many -A one-to-many relation (`1:n`) connects one record to many: one user writes many posts, and each post belongs to one user. This is the most common relation shape. +A one-to-many relation (`1:n`) connects one record to many: one user writes many posts, and each post belongs to one user. This is the most common relation kind. ```prisma model User { @@ -49,13 +49,11 @@ model Post { } ``` -The foreign key is on the "many" side, `Post.authorId`, because each post points at exactly one user, while a user points at an open-ended list of posts. +The foreign key is on the "many" side, `Post.authorId`, because each post points at exactly one user, while a user points at an open-ended list of posts. Use a one-to-many whenever a record belongs to one parent and a parent has many children: comments on a post, line items on an order, employees in a department. -`posts Post[]` on `User` is the mirror field: the field on the other model, which stores nothing in the database. `npx prisma contract emit` matches it to the foreign key on `Post`. Mirror fields are optional to declare. Leave it out and the relation still works, and you read it from the foreign key side only. +`posts Post[]` on `User` is the mirror field, the field on the other side of the relation, which stores nothing in the database. Declaring it is optional. Prisma ORM 7 required this field; Prisma ORM 8 does not. Leave it out and the relation still works, and you read it from the foreign key side only. -Use a one-to-many whenever a record belongs to one parent and a parent has many children: comments on a post, line items on an order, employees in a department. - -To connect a post to a user, set the foreign key when you create the post. `db.orm` holds your models by model name, and `public` is the PostgreSQL schema your tables live in, which is `public` unless you set another one. The `data:` wrapper from Prisma ORM 7 is gone, so the fields go straight into `create`: +To connect a post to a user, set the foreign key when you create the post. `db` is the client you create once in `src/prisma/db.ts`, the file `npx prisma orm init` writes. `db.orm` holds your models by model name, and `public` is the PostgreSQL schema your tables live in, unless your contract puts the model in another schema with a `namespace` block. Here `user` is a user record you read earlier: ```typescript const post = await db.orm.public.Post.create({ @@ -64,7 +62,11 @@ const post = await db.orm.public.Post.create({ }); ``` -See [Relations and joins](/orm/fundamentals/relations-and-joins#one-to-many) for querying in both directions. +Reading back the other way, `.include("author")` returns each post with its user nested on it, and [Relations and joins](/orm/fundamentals/relations-and-joins#one-to-many) covers querying in both directions: + +```typescript +const posts = await db.orm.public.Post.include("author").all(); +``` ## One-to-one @@ -86,9 +88,7 @@ model Profile { } ``` -Declare the relation on the side that holds the foreign key, `Profile` here. - -You can add the mirror field to the other model as well: `profile Profile?` on `User`. A bare field typed as the other model is enough, and it needs no `@relation` of its own. It has to be optional, because nothing in the database guarantees a matching profile row. The foreign key it mirrors has to be `@unique`. If it is not, `npx prisma contract emit` reports an error whose `code` is `PSL_NON_UNIQUE_BACKRELATION`. With the mirror field in place, [the relation reads from either side](/orm/fundamentals/relations-and-joins#one-to-one). +Declare the relation on the side that holds the foreign key, `Profile` here. You can add the mirror field to the other model as well. Three rules, in the order you apply them. First, add the field: `profile Profile?` on `User`, with a type and nothing else, because it needs no `@relation` of its own. Second, make it optional, because nothing in the database guarantees a matching profile row. Third, make the foreign key it mirrors `@unique`, or `npx prisma contract emit` reports an error whose `code` is `PSL_NON_UNIQUE_BACKRELATION`. With the mirror field in place, [the relation reads from either side](/orm/fundamentals/relations-and-joins#one-to-one). ### Which side owns the foreign key @@ -131,25 +131,21 @@ model PostTag { } ``` -The composite primary key on `PostTag`, `@@id([postId, tagId])`, allows one record per pair. The mirror fields on `Post` and `Tag` name the far side (`Tag[]`, `Post[]`), and `npx prisma contract emit` matches them to the two relations on `PostTag`, so queries read `post.tags` directly. If a second join table connects the same two models, name the relation on both sides. +The composite primary key on `PostTag`, `@@id([postId, tagId])`, allows one record per pair. Note that `Post.tags` is typed `Tag[]`, not `PostTag[]`: you name the model at the other end, and Prisma ORM works out the join through `PostTag`. The same goes for `Tag.posts`. If a second join table connects the same two models, name the relation on both sides. -Connect a post to a tag with a nested write on the mirror field, or create the `PostTag` record yourself. Both write the same row, so pick one per pair: running both inserts the same composite key twice, and the second fails. +Connect a post to a tag with a nested write on the mirror field: ```typescript -// Nested write on the mirror field await db.orm.public.Post.where({ id: post.id }).update({ tags: (t) => t.connect([{ id: tag.id }]), }); - -// Or create the join record yourself -await db.orm.public.PostTag.create({ postId: post.id, tagId: tag.id }); ``` -`t` is the argument of the nested write callback, and it offers `t.create([...])`, `t.connect([...])`, and `t.disconnect([...])`, each taking an array. [Relations and joins](/orm/fundamentals/relations-and-joins) covers them. +`t` is the argument of the nested write callback: `t.create([...])` makes new related records and links them, `t.connect([...])` links existing ones, and `t.disconnect([...])` removes the link. You can also write the same row yourself with `db.orm.public.PostTag.create({ postId: post.id, tagId: tag.id })`. `PostTag` is an ordinary model, so the join table can hold data about the pairing itself. To record when a tag was added, add `addedAt DateTime @default(now())` to `PostTag`. -An implicit many-to-many, with list fields on both sides and no model for the join table, as in Prisma ORM 7, is not supported. `npx prisma contract emit` rejects it with an error whose `code` is `PSL_ORPHANED_BACKRELATION`, and the message tells you to add an explicit model for the join table. Declare that model yourself. [Relations and joins](/orm/fundamentals/relations-and-joins#many-to-many) shows how to read across it in one query. +An implicit many-to-many, with list fields on both sides and no join table, as in Prisma ORM 7, is not supported. Declare the model for the join table yourself. If you leave it out, `npx prisma contract emit` reports an error whose `code` is `PSL_ORPHANED_BACKRELATION`. [Relations and joins](/orm/fundamentals/relations-and-joins#many-to-many) shows how to read across the join table in one query. ## Polymorphic relations @@ -183,22 +179,20 @@ model Feature { `@@discriminator(type)` marks the field that records the variant. `@@base(Task, "bug")` declares that `Bug` is the `"bug"` variant of `Task`. A variant has every field of its base plus the ones it declares, and it needs no `@id` of its own, because it uses the base model's primary key. -Read and write variants through the base model. `.variant(...)` limits the query to one variant, and it takes the variant's model name, `"Bug"`, not the discriminator value `"bug"`. On a create, the discriminator value is filled in for you: +Read and write variants through the base model. `.variant(...)` limits the query to one variant, and it takes the variant's model name, `"Bug"`. Passing the discriminator value `"bug"` is a TypeScript error, because the only names it accepts are the model names of the base model's variants. On a create, the discriminator value is filled in for you: ```typescript await db.orm.public.Task.variant("Bug").create({ title: "Crash on save", severity: "high" }); ``` -A read works the same way. `.variant("Bug")` filters on the discriminator, and the rows come back with the base fields plus that variant's fields. A query on `Task` with no `.variant(...)` returns every variant. +A read works the same way. A query on `Task` with no `.variant(...)` returns every variant, and each row carries the base fields plus only its own variant's fields, with the discriminator field holding that variant's value, such as `"bug"`. `@@map` decides how a variant is stored: - A variant with no `@@map`, like `Bug`, shares the base table. Its extra columns are added to the base table and are nullable there, because a `Feature` row has no severity. - A variant with its own `@@map`, like `Feature`, gets its own table. That table holds its extra columns plus the base model's primary key, which links it back to the base table. Those columns can be `NOT NULL`, and reading a full variant costs a join. -Start with the shared table. Give a variant its own table when its columns have to be `NOT NULL` in the database, or when the variant has many columns of its own. - -Use a base model with variants when you query the variants together (one feed of tasks, one stream of events) and each variant has real structure of its own. If you never query them together, separate models are simpler. If the variants differ by one nullable field, a single model is simpler still. +Start with the shared table. Give a variant its own table when its columns have to be `NOT NULL` in the database, or when the variant has many columns of its own. Use a base model with variants when you query the variants together (one feed of tasks, one stream of events) and each variant has real structure of its own. If you never query them together, separate models are simpler. If the variants differ by one nullable field, a single model is simpler still. ## Prompt your coding agent @@ -213,4 +207,9 @@ Projects created with `npm create prisma@latest` include the [Prisma ORM skills] - [Query relations](/orm/fundamentals/relations-and-joins) with `.include(...)`, filters on relation data, and reads across the join table. - To model for MongoDB, see the [MongoDB data modeling guide](/orm/data-modeling/mongodb). -- Referential actions: add `onDelete` and `onUpdate` to `@relation`, as in `@relation(fields: [userId], references: [id], onDelete: Cascade)`. `Cascade` deletes or updates the matching rows, `Restrict` blocks the change, `SetNull` clears the foreign key, `SetDefault` writes its default value, and `NoAction` leaves the decision to the database. They become the foreign key's `ON DELETE` and `ON UPDATE` clauses. +- Referential actions: add `onDelete` and `onUpdate` to `@relation`, as in `@relation(fields: [userId], references: [id], onDelete: Cascade)`. They become the foreign key's `ON DELETE` and `ON UPDATE` clauses. Write neither and the foreign key gets neither clause, so the database decides what happens. The five actions are: + - `Cascade` deletes or updates the matching rows. + - `Restrict` blocks the change. + - `SetNull` clears the foreign key. + - `SetDefault` writes the foreign key's default value. + - `NoAction` leaves the decision to the database. diff --git a/docs/orm-docs-audit/c21-data-modeling/index.fix2.md b/docs/orm-docs-audit/c21-data-modeling/index.fix2.md new file mode 100644 index 00000000000..131699fef88 --- /dev/null +++ b/docs/orm-docs-audit/c21-data-modeling/index.fix2.md @@ -0,0 +1,56 @@ +Round two edits are done on `/Users/will/Projects/prisma/web/.claude/worktrees/pr-conflicts-review-fb93f7/wip/c21/dm/r2/index.mdx`. Line count 258 before, 258 after. `check-plain.sh` clean, no em-dashes, no page-conventions banned words. + +## Marks and fixes + +**Contradiction, line 22 vs 241.** Removed "and other SQL databases" from the Next steps bullet and from the Relations bullet; the polymorphic bullet now says "on PostgreSQL". Only PostgreSQL and MongoDB are named anywhere. + +**"identity and its own history".** Abstract sentence deleted. Now: "Two posts with the same title are still two different records, and a user who changes their email address is still the same user." + +**MongoDB `id`.** "The `create` input type makes `id` optional, so leaving it out type-checks and MongoDB assigns the `ObjectId` for you." Same sentence now also covers `@@map`: "names the collection and is optional: without it, the collection is the model name with a lowercase first letter." + +**Reference table / ISO code.** "Natural keys fit reference tables best: tables that hold a fixed set of values other records point at. You can identify a country by its ISO code, and the code never changes." + +**"The column names in the table are PostgreSQL columns".** Now: "Where a row above names a PostgreSQL type such as `numeric` or `jsonb`, that is the column type Prisma ORM uses on PostgreSQL." + +**`Decimal` "so" backwards, and `Numeric` unintroduced.** "A `Decimal` value reaches your code as a string, so nothing is rounded through a JavaScript number. Write `price Decimal` when any precision will do. Write `price Numeric(10, 2)` when you want an explicit precision and scale. `Numeric` is `Decimal` with the number of digits written out. Both become a PostgreSQL `numeric` column." + +**Temporal / raw-queries pointer.** Replaced with "`Temporal` is built into Node.js 26.8.2 and later. On older versions, install the `temporal-polyfill` package." + +**Money paragraph.** Two sentences: "For ordinary money, use `Int` and store the amount in the currency's smallest unit, such as cents, because you get a plain JavaScript number. Use `Decimal` only when you need more decimal places than the currency has, such as a per-unit rate." + +**"each model needs the fields for its own side".** Now matches the example: "Two kinds of field describe a relation. The model that holds the link needs both of them. The model on the other end needs only a relation field." + +**"Skills".** Now "the [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8), instruction files for coding agents." + +**"four things" claimed completeness.** Now "A contract is built from four building blocks:". + +**The whole file.** Added to the intro: "There is no `datasource` or `generator` block any more, and the connection string is in `prisma.config.ts` instead. [A complete contract](/orm/contract-authoring/psl-syntax#a-complete-contract) shows a whole file next to its config." + +**Enums.** Kept as a sentence plus link rather than a block: the smallest valid enum needs `@@type("pg/text@1")`, and page-conventions bans type ids on these pages. Now: "You declare those values in an `enum` block that lists each member and says how the members are stored, and [enums](...) shows the block to copy." + +**`uuid(7)`.** "`@default(uuid())` and `@default(uuid(4))` both give you a random UUID." + +**Migration commands.** Next steps bullet now names all three: `npx prisma contract emit`, then `npx prisma migration plan --name `, then `npx prisma db migrate`. + +**No primary key.** "`npx prisma contract emit` accepts a model without a primary key. The ORM client does not: `update` and `delete` need a primary key or a unique field, and on a model with neither they fail with an error whose `code` is `ORM.ROW_IDENTITY_MISSING`." + +**`Json` vs `Jsonb`.** "so use `Jsonb` unless you specifically need PostgreSQL's plain `json` column." + +**`UserTag` links nothing.** Added the clause and link: "The two relation fields that point back at `User` and `Tag` are left out here to keep the key in view, and [many-to-many](/orm/data-modeling/relational-databases#many-to-many) shows the complete model." + +**One-to-one and many-to-many.** One clause each in the list: one-to-one is a one-to-many with `@unique` on the field that stores the link; many-to-many needs a model for the join table. + +**Mechanism cuts (to stay within budget).** Dropped "which MongoDB generates without asking any other server and which carries its own creation time", and the second "it stores nothing in the database" on `posts Post[]`. Kept the first one and kept the sequential-id warning the reader said earned its place. + +**Not changed.** Title stays "Overview" (per your instruction). "contract", "PSL", "emit", "polymorphic collections" left to the pages that define them; the contract sentence already carries the standard first-use gloss, and polymorphic collections keep their link to the MongoDB page. + +## Facts looked up + +- No primary key: `wip/prisma-src/packages/3-extensions/sql-orm-client/src/collection.ts:2422` — `update()`/`delete()` throw `ORM.ROW_IDENTITY_MISSING` when the table has no primary key or unique constraint. +- Enum syntax requires `@@type`: `apps/docs/content/docs/orm/contract-authoring/psl-syntax.mdx:183-196`. +- Complete contract plus `prisma.config.ts`, no `datasource`/`generator`: `apps/docs/content/docs/orm/contract-authoring/psl-syntax.mdx:13-125`. +- `#many-to-many` anchor exists: `apps/docs/content/docs/orm/data-modeling/relational-databases.mdx:105`. + +## Q list + +None. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/index.round2.md b/docs/orm-docs-audit/c21-data-modeling/index.round2.md new file mode 100644 index 00000000000..c48037cc823 --- /dev/null +++ b/docs/orm-docs-audit/c21-data-modeling/index.round2.md @@ -0,0 +1,111 @@ +Read once, top to bottom, as a Prisma 6 user who has never seen this version. + +## Sentences I could not restate after one reading + +**Line 22 vs line 241 — direct contradiction.** +> "All four apply to both databases Prisma ORM supports today, PostgreSQL and MongoDB." + +then + +> "[Relational data modeling](...) for PostgreSQL and other SQL databases" + +Which is it? I have a MySQL app. Line 22 says the ORM supports exactly two databases now; line 241 says "other SQL databases" exist. I stopped reading to figure out whether my database was dropped. The page never says. + +**Line 36.** +> "Every record of a model has its own identity and its own history." + +"Its own history" stopped me cold. Does Prisma now store record history/versions? I re-read the following sentence and concluded it's a metaphor for "it's the same record over time," but I genuinely could not tell on first pass. Plainest version: "Two posts with the same title are still two different records, and a user who changes their email is still the same user." — i.e. delete the abstract sentence, keep the concrete one. + +**Line 59.** +> "Leave `id` out of the `create` call and MongoDB assigns the `ObjectId` for you." + +The model above it is `id ObjectId @id @map("_id")` — required, no `@default`. In Prisma 6 I wrote `@default(auto())`. So will the generated TypeScript type actually let me omit `id`? The schema as printed says it's required. I could not restate this without guessing that ObjectId gets special-cased. + +**Line 65.** +> "A reference table, one that holds a fixed set of values the rest of your data points at, is the classic fit." + +A definition wedged inside a sentence, followed by "is the classic fit" — fit for what? (Natural keys, three sentences back.) Plainer: "Natural keys fit reference tables best — tables holding a fixed set of values that other records point at." + +**Line 66.** "A country is its ISO code, and the code never changes." A country is not its ISO code. You mean "identify a country by its ISO code." + +**Line 155.** +> "The column names in the table are PostgreSQL columns." + +The table's columns are "Type" and "Stores". I first read this as being about the table's own headers. I think you mean: "Where the table names a type like `numeric` or `jsonb`, that's the PostgreSQL column type Prisma uses." + +**Line 157 — two problems.** +> "A `Decimal` value reaches your code as a decimal string, so binary floating point never loses digits." + +The "so" is backwards. Binary floating point isn't involved at all; that's the point. Plainer: "Decimal values arrive as strings, so nothing is rounded through a JavaScript number." + +> "Write `price Decimal` when any precision will do, and `price Numeric(10, 2)` when you want an explicit precision and scale." + +`Numeric` is not in the type table directly above. Is it a type? A variant of `Decimal`? Where did it come from? This is the single most confusing line on the page. + +**Line 159.** +> "[Raw queries](/orm/reference/raw-queries) says which Node.js versions have `Temporal` built in and how to add it to older ones." + +I could not resolve why a raw-queries reference page would tell me about Node versions and Temporal polyfills. That's a runtime-requirements fact, and I'd never look there. + +**Line 185.** Too many ideas in one sentence, and it argues against itself: +> "Store an integer number of the smallest unit instead. `Decimal` is exact as well, so use it when your amounts need more decimal places than the currency's smallest unit, and use `Int` otherwise because a plain JavaScript number is easier to work with than a decimal string" + +First it tells me to use integer cents. Then it tells me `Decimal` is also exact. Then it gives a rule that requires me to hold "smallest unit," "decimal places," and "decimal string ergonomics" in my head at once. I read it three times. Plainer as two sentences: "Use `Int` and store cents for ordinary money — you get a plain JavaScript number. Use `Decimal` only when you need more decimal places than the currency has, like per-unit rates." + +**Line 211.** +> "Two kinds of field describe a relation, and each model in it needs the fields for its own side." + +Then the example gives `User` exactly one field (`posts Post[]`) and `Post` two. So "the fields" (plural) for its own side is wrong for `User`. I couldn't reconcile the rule with the example. + +**Line 246.** +> "Projects created with `npm create prisma@latest` include the [Prisma ORM skills](...) for your coding agent." + +"Skills" is used as an established noun and I don't know what it is. I guessed: some package of instructions an AI agent reads. + +## Words and phrases I had to guess + +- **"contract"** (line 13). I guessed: the new name for the schema file, and also a general noun for "the thing in that file." It's used both ways ("in your contract", "Every contract is built from") and I was never sure whether a contract is the file, the content, or a broader concept. +- **"PSL"** — never appears spelled out, but the link at line 170 is `/orm/contract-authoring/psl-syntax`. Guessed: Prisma Schema Language. +- **"emit"** (line 257, `npx prisma contract emit`). Guessed: the replacement for `prisma generate`. The page never says what it emits. +- **"surrogate key" / "natural key"** — these I know, but you define them, which is fine. +- **"Jsonb"** as a distinct Prisma type. In Prisma 6 there was only `Json`. Guessed: `Json` maps to `json`, `Jsonb` maps to `jsonb`, and I should almost always use `Jsonb`. The page never tells me which to choose, even though it says `Json` "offers no comparison at all" — so why would anyone pick it? +- **"polymorphic collections"** (line 242). Guessed: storing different shapes in one collection. Never defined. +- **"reference table"** — defined mid-sentence, see above. + +## "So what do I actually type?" + +1. **The whole file.** The page never shows a complete `contract.prisma`. Coming from Prisma 6 my first question is: are `datasource db { ... }` and `generator client { ... }` still in there? Do I still set `provider` and `url`? Not one word. +2. **Enums** (line 170). "A field can also hold one of a fixed set of values, which you declare as an [enum]." No syntax, not even a two-line block, in a page that shows a code block for everything else. +3. **`Numeric(10, 2)`** (line 157). Is that a type name I write literally? It's not in the table. +4. **MongoDB `id`.** Do I write `@default(auto())`, or truly nothing? See above. +5. **`uuid(7)`** (line 118). Is `uuid(4)` also valid? Is the bare `uuid()` the same as `uuid(4)`? Implied but not stated. +6. **Line 257.** "run `npx prisma contract emit` after every change to the contract, then plan and apply a migration." What are the plan and apply commands? You gave me one command and then described two more steps in prose without their commands. This is the last line of the page and the most actionable moment in it. +7. **`npx prisma skills sync`** (line 246) — fine, that one's concrete. +8. **Composite key example** (lines 126-133). `UserTag` has `userId` and `tagId` but no relation fields and no `@relation`. If I copy it, I have a model that links nothing. It's introduced as "a model that links two others" and then doesn't link them. +9. **One-to-one** (line 235). Listed as a kind of relation, no syntax shown, and the `@unique` that makes it one-to-one is never mentioned. +10. **What happens if I skip a primary key?** Line 42 says Prisma "needs" one. Error at emit time? Silent read-only model? Not said. +11. **`@@map("users")`** appears only in the MongoDB tab (line 55) with no explanation of why it's there and not in the PostgreSQL tab. Do I need it on Mongo? It's presented as part of the required MongoDB shape. + +## Places explaining internals when I wanted to know what to do + +- **Line 120.** "which MongoDB generates without asking any other server and which carries its own creation time." I don't get a choice here — you just told me the choice is made for me. The mechanism is trivia at that point. +- **Line 214 and again line 231.** "It stores nothing in the database." / "like `author` it stores nothing in the database." Said twice. The first time is useful. The second is padding. +- **Line 36.** "Every record of a model has its own identity and its own history." Philosophy, not a thing I can act on. +- **Line 108.** "It is also sequential, so it leaks row counts and invites guessing if you expose it in URLs." This one earns its place — it changes what I'd pick. + +## Could I do what the page is for, after one reading? + +Partly. I could write a model with a surrogate integer id, scalar fields, and a one-to-many relation. That's the core and it lands. + +What I would still not know: + +- Whether my MySQL database is still supported. +- What else goes in `contract.prisma` besides models — I've never seen a whole file. +- How to declare an enum, despite being told to use them. +- Whether `Numeric` is a type I can write. +- The commands to get my models into the database. "Plan and apply a migration" is not a command. +- How to write a one-to-one or many-to-many relation, or a working link model. The one link model shown is incomplete. +- When to use `Json` instead of `Jsonb`, given you told me `Json` can't be filtered at all. +- What to actually write for a MongoDB `id` field. + +Two more things worth fixing. **"Every contract is built from four things"** (line 15) is then contradicted by the page itself — enums are a fifth, and datasource/generator presumably a sixth and seventh. Say "four building blocks" and don't claim completeness. And the title is **"Overview"** while `metaTitle` is "Data modeling in Prisma ORM"; as a reader landing on a page called "Overview" I have no idea what it overviews until line 9. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/mongodb.fix2.md b/docs/orm-docs-audit/c21-data-modeling/mongodb.fix2.md new file mode 100644 index 00000000000..633fbc22b9b --- /dev/null +++ b/docs/orm-docs-audit/c21-data-modeling/mongodb.fix2.md @@ -0,0 +1,45 @@ +Round two done. `wip/c21/dm/r2/mongodb.mdx`, 173 lines before and after, plain-language check clean, no em-dashes. + +## Marks and what I did + +**The eight-fact paragraph under the `_id` example.** Cut to four MongoDB-client facts and moved the rest into one Next steps bullet. It now reads: "`db` is the client you create once in `src/prisma/db.ts`, and `npx prisma orm init` writes that file. `db.orm.users` is the accessor for the collection, and it follows the collection name, so `@@map` renames the accessor as well. In Prisma ORM 7 it did not. The values go straight into `create`, without the `data:` wrapper Prisma ORM 7 used." The contract sentence, `npx prisma contract emit`, and both migration commands are now the first Next steps bullet with a link to `/orm/migrations/generating-a-migration`. + +**Accessor changes with `@@map` (stated twice, never contrasted with v7).** The `@@map` paragraph no longer repeats the accessor; the sentence above carries it once, with the Prisma ORM 7 contrast. + +**"There is no `@default(auto())` and no `@db.ObjectId`."** Now: "`ObjectId` replaces `String @db.ObjectId` and `@default(auto())`. Writing either of those attributes is an error whose `code` is `PSL_UNSUPPORTED_FIELD_ATTRIBUTE`." + +**`Cart` / `CartItem` referenced but never shown.** Dropped both names; the sentence now uses the page's own type: "`addresses Address[]` stores every address inside the user document, with no second collection and no join to load them." + +**"Prisma ORM models a collection..." garden path.** Now: "One collection can hold more than one kind of document. You declare a base model for the shared fields and a variant model for each kind. A discriminator field tells the kinds apart: its value says which kind each document is." The "MongoDB collections do not enforce a single shape" sentence is cut, as instructed. + +**16 MB limit with no consequence.** Now "...pushes the document toward MongoDB's 16 MB limit, and a write that exceeds it fails." + +**Copied fields go stale.** Added: "The copy then goes stale on its own, and keeping it up to date is your job." + +**"contract", `contract emit`, `migration plan`.** All three now appear once, together, in the Next steps bullet, with the contract defined as "the `contract.prisma` file that replaced `schema.prisma`" (the section's fixed wording). + +**Pipeline builder gloss.** Now "a chained builder for MongoDB aggregation pipelines", with a two-line example. + +**`u`.** Defined at first use: "In an `update`, `u` gives you each field of the document to change." + +**`.all()`.** Defined once, where `.first()` first appears: "You `await` the whole chain, and the last call says what you want back: `.first()` for one document or `null`, `.all()` for every matching document." + +**"So what do I type?" items.** Connection: one sentence at the top of the first section, no `datasource` block, `prisma.config.ts` with `defineConfig` from `@prisma/orm-mongo/config`, link to `/orm/contract-authoring/the-data-contract`. Creating a referenced record: `db.orm.posts.create({ title: "Hello", authorId: user._id })`. Filtering on an embedded field: the `db.query.from("users").match(...)` example. Querying by id: `db.orm.users.where({ _id: user._id }).first()` with "the id comes back as a string, and you pass it back as a string. There is no `ObjectId` to construct." Adding to a list: `u.tags.push("admin")` / `u.tags.pull("new")` on a `tags String[]` field added to the embedded-section model. Variant `@@map`: "A variant declares no `@@map` of its own and uses its base's collection." Indexes: the trailing index clause in the polymorphic section is cut, per the brief. + +Cuts that paid for the additions: the collection-shape sentence, the index clause, the blank lines inside the PSL blocks, and merged paragraphs that repeated each other (embed/reference intro, embedded-type intro, the two reference query examples now in one block). + +## Facts looked up (rc.10 source) + +- `where` key is `_id`, not `id`: contract fields are keyed by the mapped name. `wip/prisma-src/packages/2-mongo-family/2-authoring/contract-psl/src/interpreter.ts:1320-1336` ("the mapped-name-keyed field record"), and the fixture `.../1-foundation/mongo-contract/test/fixtures/orm-contract.json` keys `User.fields` by `_id`. Test usage `where({ _id: created._id as string })` and `expect(typeof created._id).toBe('string')` at `.../5-query-builders/orm/test/integration/orm-ergonomics.test.ts:66-71`. +- `@default(...)` and `@db.ObjectId` are both `PSL_UNSUPPORTED_FIELD_ATTRIBUTE`: `.../contract-psl/test/interpreter.attribute-specs.test.ts:87-117`; the supported field attributes are only `@id`, `@unique`, `@map`, `@relation` (`.../src/mongo-attribute-specs.ts:256-261`). +- Setting a reference field directly on create: `orm.tasks.create({ title, type, assigneeId: user._id })`, `orm-ergonomics.test.ts:236-245`. +- `push` / `pull` on a scalar list: `orm-ergonomics.test.ts:135-155`. Embedded (value-object) paths get only `set`/`unset`/`exists`/`eq(null)` (`.../query-builder/src/field-accessor.ts:92-110`), so I showed `push` on `tags String[]`, not on an embedded list. +- Variant `@@map`: rejected only when it names a different collection, `interpreter.ts:460-465`. +- `@prisma/orm-mongo/config` export exists (`wip/prisma-src/packages/9-public/@prisma/orm-mongo/package.json`), matching the config example on `apps/docs/content/docs/orm/contract-authoring/psl-syntax.mdx:118-128`. +- Pipeline filter form `f("address.city").eq("NYC")` and `db.query.from("")`: `.../query-builder/test/builder.test.ts:77-86` and `apps/docs/content/docs/orm/fundamentals/advanced-queries.mdx:307-351`. + +## Q list + +**Q1.** Whether `npx prisma db migrate` must run before the first write on MongoDB. `wip/prisma-src/skills/prisma-8/references/migrations.md:47` says the marker is a document in `_prisma_migrations`, written after schema verification, but I found no check of that marker in the MongoDB query path (nothing matching `verifyMarker`/`marker` in `wip/prisma-src/packages/2-mongo-family/7-runtime/src/`). So I could not confirm that an unmigrated database rejects a write, and the page does not claim it either way. The reader asked for this ordering explicitly. + +**Q2.** Adding to a list of embedded values (`items CartItem[]`). The typed accessor for a value-object path has no `push`, and no test shows one, so the page shows `push`/`pull` on a scalar list only. If the runtime does support it on embedded lists, the example should move there, since that is the case the embed-vs-reference table sends readers to. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/mongodb.round2.md b/docs/orm-docs-audit/c21-data-modeling/mongodb.round2.md new file mode 100644 index 00000000000..9ca62c9ea95 --- /dev/null +++ b/docs/orm-docs-audit/c21-data-modeling/mongodb.round2.md @@ -0,0 +1,90 @@ +Read it once, straight through, as a Prisma 7 user who has never seen 8. + +## Sentences I could not restate after one reading + +**Line 32 — the whole paragraph.** This is the worst thing on the page. + +> "`db` is the client you create once in `src/prisma/db.ts`, and `prisma orm init` writes that file. `db.orm` holds your models, and on MongoDB the accessor is the collection name, so `db.orm.users`. The values go straight in, without the `data:` wrapper Prisma ORM 7 used. Models themselves go in your contract, the `contract.prisma` file that replaced `schema.prisma`. Run `npx prisma contract emit` after every change to it. MongoDB has migrations like any other database: `npx prisma migration plan` writes one and `npx prisma db migrate` applies it, creating the collections and indexes." + +Eight unrelated new facts in one paragraph, none of them about MongoDB modeling: client file location, an init command, accessor naming, a removed `data:` wrapper, a renamed schema file, an `emit` command, and two migration commands. I stopped reading and went back three times. It is a "everything that changed in v8" dump wedged under a code sample about `_id`. It belongs in its own section, or on the migration page, with the MongoDB-specific bit (`db.orm.users`) left here. + +**Line 24 vs 29/32 — the accessor name.** + +> "Without it the collection takes the model name with a lowercase first letter, so `user`, and you reach it in code as `db.orm.user`." + +I had to stop and work out that the *client accessor changes when I rename the collection*. In v7 the accessor was the model name and `@@map` was purely a database-side rename. The page states the new behavior twice but never says "unlike v7, `@@map` also renames the client accessor" — which is the only thing I actually needed to be told. I would have gotten this wrong. + +**Line 26.** + +> "There is no `@default(auto())` and no `@db.ObjectId`." + +Three readings and I still don't know what happens if I write them. Removed? Ignored? Contract-emit error? "No" is not a behavior. Plainest version: "`@default(auto())` and `@db.ObjectId` are gone — writing either is an error. The `ObjectId` type covers both." + +**Line 74.** + +> "A list of embedded values models a one-to-many: `items CartItem[]` on a `Cart` model stores every line item inside the cart document" + +Two model/type names (`Cart`, `CartItem`) that appear nowhere on the page, referenced as if I'd already seen them. I could not check my mental model against anything. Show the three-line `type CartItem` + `model Cart` instead of describing it. + +**Line 124.** + +> "Prisma ORM models a collection that holds more than one kind of document with a base model plus variant models, all sharing one collection." + +Garden path: I read "models" as a noun ("Prisma ORM models" = my models) and had to restart the sentence. Also "base model plus variant models" and "polymorphic" are dropped on me as established terms. Plainer: "One collection can hold more than one kind of document. You declare a base model for the shared fields and a variant model for each kind." + +**Line 91.** + +> "pushes the document toward MongoDB's 16 MB limit" + +Minor, but "16 MB limit" appears with no statement of what happens at the limit (the write fails). I guessed. + +**Line 120.** + +> "If a read runs constantly, you can copy the few fields it needs into the parent document, such as a comment storing its author's name." + +I know this is denormalization. The sentence does not say that copies go stale and that keeping them in sync is now my job. That is the entire cost of the advice, and it's missing. + +## Words and phrases I had to guess + +- **"contract"** (line 32) — guessed: the new name for the schema file. The page says "your contract, the `contract.prisma` file that replaced `schema.prisma`" but never says what a contract *is* or why it isn't just called a schema anymore. +- **"`prisma contract emit`"** — guessed: regenerates the client, i.e. the old `prisma generate`. The page never says what it emits or why I need it. +- **"`npx prisma migration plan` writes one"** — guessed: "one" = a migration file. Guessed it goes in a `migrations/` folder. Not stated. +- **"the pipeline builder, the API for MongoDB aggregation pipelines"** (line 89) — the gloss tells me what it wraps, not what it looks like. Guessed: a chained query builder. +- **"`u`"** in `update((u) => [u("address.city").set("Hamburg")])` — guessed: a field-accessor for the row being updated. Never named or explained anywhere on the page. +- **"`.all()`"** (line 117) — guessed: executes and returns many rows. It appears once, unannounced, and `create` on line 29 does *not* need it, so I don't know the rule. +- **"variant"** (line 152) — guessed correctly, but only because `@@base(Post, "article")` was right above it. +- **"`@@discriminator(kind)`"** — the page does define "discriminator field" inline, which worked. + +## "So what do I actually type?" — unanswered + +1. **How do I tell the contract this is MongoDB.** There is no `datasource` block, no `provider = "mongodb"`, no connection string, anywhere on a page titled "MongoDB data modeling". For MongoDB specifically this is the first thing I need. +2. **How do I create a referenced record.** The references section shows the schema and a read (`.include("author")`), and then stops. I don't know whether I write `authorId` directly, or use something like v7's `connect:`, or a nested create. This is the most common thing I'd do after reading that section. +3. **How do I filter on an embedded field.** Line 89 tells me `.where()` won't do it, tells me the tool that will, and gives me zero code. I'm left with "use the pipeline builder with a path such as `address.city`" and a link. One example would fix this. +4. **How do I query by `_id`.** Do I pass a string, or construct an `ObjectId`? Every v7 MongoDB user hits this in the first hour. Not mentioned. +5. **How do I push to or remove from an embedded array.** The page recommends embedded arrays for one-to-many, then only shows `.set()` on a scalar path. Adding a cart item — the exact example it used — is not shown. +6. **Do variant models need `@@map`?** `Article` has no `id`, no `@@map`. I inferred it inherits the collection from `@@base(Post, ...)`, but that's a guess and the page should say it in five words. +7. **Where does `@@index([authorId])` go and when do I need one?** It's mentioned in one trailing clause of the polymorphic section, which has nothing to do with indexes, and then handed off to another page. +8. **Migrations for MongoDB.** "MongoDB has migrations like any other database" — but I have no idea what a migration does on a schemaless store, what `plan` produces, or whether I must run these before my first `create`. The sentence raises the question and then leaves. + +## Internal mechanics where I only wanted to know what to do + +- Line 24, `@@map("_id")` explained as a *mapping* between model field and MongoDB field. Fine and short — this one earns its place. +- Line 32, `prisma contract emit` "after every change" is the reverse problem: pure mechanism with no explanation, presented as a ritual. +- Line 124, "MongoDB collections do not enforce a single shape, so this is the usual way..." — background on MongoDB's storage model when I was trying to find out how to declare a variant. Cut it or move it after the code. + +Nothing else on the page over-explains internals. The failure mode here is the opposite: commands and APIs introduced with no explanation at all. + +## Could I do what the page is for, after one read? + +**Partly.** I could decide embed vs reference — that section (lines 36–53, including the table) is genuinely good and is the only part I could restate cleanly without rereading. I could also write the `type Address` / `model User` embedded schema and the reference schema from memory. + +**I could not actually build anything**, because after one reading I still don't know: + +- How to point the contract at my MongoDB database at all. +- How to create a record on either side of a reference. +- How to query or filter by anything inside an embedded document — the page explicitly tells me `.where()` won't work and then doesn't show me what does. +- Whether I have to run `prisma contract emit` and `prisma db migrate` before my first write, or in what order. +- How to pass an `_id` into a query. +- What `u`, `.all()`, or `.set()` are, beyond guessing from context. + +The page is a good *decision* guide and an incomplete *task* guide. The embed-vs-reference material is the strongest thing here; the v8 API and tooling around it is asserted in fragments, mostly inside one overloaded paragraph, and never demonstrated end to end. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/relational-databases.fix2.md b/docs/orm-docs-audit/c21-data-modeling/relational-databases.fix2.md new file mode 100644 index 00000000000..7cc4263f097 --- /dev/null +++ b/docs/orm-docs-audit/c21-data-modeling/relational-databases.fix2.md @@ -0,0 +1,54 @@ +Done. The page is at 215 lines (was 216), plain-language check clean, no em-dashes. + +## Marks and what I did + +**Line 27, four new things at once** — rewritten in the ordered sequence: contract definition first ("Your contract is the `contract.prisma` file that replaced `schema.prisma`"), then "After every change to it, run `npx prisma contract emit`. That command checks the contract and regenerates your types. It only updates your code." Then "To change the database, run `npx prisma migration plan --name add-posts`, which writes a migration file, and then `npx prisma db migrate`, which applies that file." Cut the `contract.json` / `contract.d.ts` clause. + +**Line 15, "stores the connection"** — now "The model whose table gets the foreign key column declares two fields." + +**Line 25, garbled clause and "shapes"** — "Deciding which side that is is the main choice in each of the relation kinds below." "Shapes" replaced with "kinds" everywhere. + +**Line 54, "mirror field" arrives cold, v7 contradiction** — "`posts Post[]` on `User` is the mirror field, the field on the other side of the relation, which stores nothing in the database. Declaring it is optional. Prisma ORM 7 required this field; Prisma ORM 8 does not." The "emit matches it to the foreign key" clause is cut. + +**Line 91, "bare field" plus five unordered rules** — now numbered in application order: "Three rules, in the order you apply them. First, add the field: `profile Profile?` on `User`, with a type and nothing else, because it needs no `@relation` of its own. Second, make it optional... Third, make the foreign key it mirrors `@unique`, or `npx prisma contract emit` reports an error whose `code` is `PSL_NON_UNIQUE_BACKRELATION`." + +**Line 109, "the model for the join table" repeated four times** — kept as the first-use name (page-conventions requires it), then `PostTag` or "the join table" afterwards. Repeats at old lines 134 and 152 are gone. + +**Line 134, "name the far side"** — "Note that `Post.tags` is typed `Tag[]`, not `PostTag[]`: you name the model at the other end, and Prisma ORM works out the join through `PostTag`." The emit-matching clause is cut. + +**Line 136, "pick one per pair"** — cut, along with the second code block. The alternative is now one inline clause: "You can also write the same row yourself with `db.orm.public.PostTag.create({ postId: post.id, tagId: tag.id })`." + +**Line 148, `t.create` / `connect` / `disconnect` named but not explained** — "`t.create([...])` makes new related records and links them, `t.connect([...])` links existing ones, and `t.disconnect([...])` removes the link." Only the `connect` example remains. + +**Line 184, variant has no `@id`** — unchanged; the sentence already answers it and the reader could restate it. + +**Line 186, two nearly identical strings** — now says what goes wrong: "it takes the variant's model name, `"Bug"`. Passing the discriminator value `"bug"` is a TypeScript error, because the only names it accepts are the model names of the base model's variants." + +**Line 216, five definitions in one run, and no default** — the bullet now states the two clauses it produces, then what happens when you write neither, then a five-item sub-list with one action per line. + +**"contract" / "PSL" / "`data:` wrapper" guesses** — contract defined at first use; "`PSL` is the Prisma Schema Language, the language `contract.prisma` is written in" added at the first error code; the `data:` wrapper sentence cut. + +**"Where does `db` come from?" and undefined `user`** — "`db` is the client you create once in `src/prisma/db.ts`, the file `npx prisma orm init` writes... Here `user` is a user record you read earlier." + +**"What is `public` and how do I change it?"** — "`public` is the PostgreSQL schema your tables live in, unless your contract puts the model in another schema with a [`namespace` block](/orm/contract-authoring/psl-syntax)." + +**"How do I read a relation?"** — added a one-line `.include("author")` example under one-to-many, paid for by the cuts above. + +**"What does an unfiltered `Task` query return?"** — "A query on `Task` with no `.variant(...)` returns every variant, and each row carries the base fields plus only its own variant's fields, with the discriminator field holding that variant's value, such as `"bug"`." + +**"Is there a `Bug` accessor?"** — left as "Read and write variants through the base model", no claim either way, per the brief. + +**`PSL_ORPHANED_BACKRELATION` reads as unhelpful** — the instruction now comes first ("Declare the model for the join table yourself"), the error code follows in one short sentence. + +## Facts looked up + +- Un-filtered base read: `wip/prisma-src/test/integration/test/sql-orm-client/polymorphism.test.ts:74-128`. Rows carry base fields plus only their own variant's field, with the discriminator holding `'bug'` / `'feature'`. +- Referential actions: `wip/prisma-src/packages/2-sql/2-authoring/contract-psl/src/psl-relation-resolution.ts:24-34` maps only the five names; no default is applied. The DDL builder at `wip/prisma-src/packages/3-targets/3-targets/postgres/src/core/migrations/operations/constraints.ts:31-45` emits `ON DELETE` / `ON UPDATE` only when the value is defined, so writing neither produces a foreign key with neither clause. +- Schemas: `namespace { … }` blocks exist and Postgres accepts explicit blocks (`wip/prisma-src/packages/2-sql/2-authoring/contract-psl/src/interpreter.ts:197-231`), so the "another schema" wording stays, linked to `/orm/contract-authoring/psl-syntax`. + +## Q list + +- **Q1**: the reader asked what the default `onDelete` is. The source shows Prisma ORM writes no `ON DELETE` / `ON UPDATE` clause when you write neither, so the page says exactly that and stops at "the database decides what happens". I did not find a Prisma-level default in `REFERENTIAL_ACTION_MAP` or the DDL builder, and I did not assert PostgreSQL's own `NO ACTION` default because that is not in this source. If you want the page to name `NO ACTION`, that needs your call. +- **Q2**: `/orm/contract-authoring/psl-syntax` does not currently document `namespace` blocks (only `the-contract-artifact.mdx` mentions namespaces, in the JSON). The link I added points at the right page for contract syntax but the reader will not find the block documented there yet. + +Line count: 216 before, 215 after. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/relational-databases.round2.md b/docs/orm-docs-audit/c21-data-modeling/relational-databases.round2.md new file mode 100644 index 00000000000..d50e12fcd29 --- /dev/null +++ b/docs/orm-docs-audit/c21-data-modeling/relational-databases.round2.md @@ -0,0 +1,73 @@ +Read once, top to bottom, as a Prisma 7 user. + +## Sentences I could not restate after one reading + +**Line 27** — "Run `npx prisma contract emit` after every change to your contract, the `contract.prisma` file that replaced `schema.prisma`. It checks the contract and writes `contract.json` and `contract.d.ts` beside it. To create the tables themselves, run `npx prisma migration plan --name add-posts` and then `npx prisma db migrate`, as described in [Generating a migration](...)." + +What stopped me: four new things at once. "Contract" is never defined — it's used as both the file and some abstract thing ("your contract"). I don't know what `contract.json` and `contract.d.ts` are for, or whether `contract emit` is the new `prisma generate`. And I don't know why creating tables takes two commands instead of one, or what `migration plan` produces that `db migrate` then consumes. Plainest version I'd have understood: "`contract.prisma` is the new name for `schema.prisma`. After editing it run `npx prisma contract emit`, which checks it and regenerates the client types. That only updates your code — to change the database, run X, which writes a migration file, then Y, which applies it." + +**Line 15** — "The model that stores the connection has two fields." I read "stores the connection" as some new storage concept before working out you just mean the table with the foreign key column in it. Say that: "The model whose table gets the foreign key column declares two fields." + +**Line 25** — "Which side that should be is the main decision in the shapes below." Two problems. "Which side that should be" is a garbled clause I had to re-read. "The shapes below" — I didn't know "shape" was going to be your word for relation kinds until line 35. Plainer: "Deciding which side that is is the main choice in each of the relation types below." + +**Line 54** — "`posts Post[]` on `User` is the mirror field: the field on the other model, which stores nothing in the database." I stalled on "mirror field" — it's a term I've never seen (in Prisma 7 this was just the other side of the relation, or the back-relation). The definition that follows is fine, but the term arrives before I know I need one. Also "Mirror fields are optional to declare" contradicted my whole mental model from v7, where leaving it off was an error, and the page doesn't flag that this changed. + +**Line 91** — "A bare field typed as the other model is enough, and it needs no `@relation` of its own." "Bare field" is doing work I had to guess at. And this paragraph stacks five separate rules (add it, no `@relation`, must be optional, the FK must be `@unique`, error code if not) into five short sentences with no ordering. I could restate each one but not the paragraph. + +**Line 109** — "you add a third model, the model for the join table, which holds one record per connected pair." "The model for the join table" is a clumsy phrase you then repeat three more times (lines 134, 152, 209). In v7 I called this the relation table or join model. Pick one short name and use it. + +**Line 134** — "The mirror fields on `Post` and `Tag` name the far side (`Tag[]`, `Post[]`), and `npx prisma contract emit` matches them to the two relations on `PostTag`." "Name the far side" — I guessed this means the field's type is the model at the other end of the relation, not `PostTag`. That's the surprising part and it's buried. Say it outright: "Note that `Post.tags` is typed `Tag[]`, not `PostTag[]` — you name the model at the other end, and Prisma works out the join through `PostTag`." + +**Line 136** — "Both write the same row, so pick one per pair: running both inserts the same composite key twice, and the second fails." Fine mechanically, but I had no idea why I'd ever be tempted to run both, so the warning read as a puzzle. + +**Line 148** — "`t` is the argument of the nested write callback, and it offers `t.create([...])`, `t.connect([...])`, and `t.disconnect([...])`, each taking an array." This explains the syntax of an API I've never seen by naming its parts. I wanted to know why it's a callback at all and what the difference between `create` and `connect` is. The v7 equivalent was a plain object; this is a real change and gets one sentence. + +**Line 184** — "A variant has every field of its base plus the ones it declares, and it needs no `@id` of its own, because it uses the base model's primary key." Restatable, but note `Bug` and `Feature` in the code block above have no `@id` and I'd already flagged that as a typo before reaching this line. + +**Line 186** — "`.variant(...)` limits the query to one variant, and it takes the variant's model name, `"Bug"`, not the discriminator value `"bug"`." I understand the words. I do not understand why two nearly identical strings exist, or what happens if I pass the wrong one. This looks like a trap and the page just points at it. + +**Line 216** — "Referential actions: add `onDelete` and `onUpdate` to `@relation`, as in `...`. `Cascade` deletes or updates the matching rows, `Restrict` blocks the change, `SetNull` clears the foreign key, `SetDefault` writes its default value, and `NoAction` leaves the decision to the database. They become the foreign key's `ON DELETE` and `ON UPDATE` clauses." Five options defined in one sentence, inside a bullet in "Next steps". I can't hold five definitions from one comma-separated run. Also it doesn't say what the default is when I write neither — that's the only thing I actually needed. + +## Words and phrases I had to guess + +- **"contract"** — guessed it's the new word for schema, and `contract emit` is the new `prisma generate`. Never confirmed. +- **"PSL"** in `PSL_RELATION_NULLABILITY_MISMATCH` etc. — guessed "Prisma Schema Language". Never expanded anywhere on the page. +- **"mirror field"** — guessed: what v7 called the back-relation / the other side. +- **"shapes"** (lines 25, 35) — guessed: kinds of relation. +- **"far side"** (134) — guessed: the model at the other end of the relation. +- **"bare field"** (91) — guessed: a field with a type and nothing else, no attributes. +- **"discriminator"** (158) — the page does define it, but only after using it in the heading sentence. I guessed correctly from the SQL sense. +- **"variant models"** (158) — guessed: subtypes. The page never says whether this is inheritance in the TypeScript sense. +- **"the `data:` wrapper from Prisma ORM 7 is gone"** (58) — I knew what this meant only because I'd used v7. Fine for me, meaningless to anyone else. +- **"ORPHANED_BACKRELATION"** (152) — the error name has nothing to do with "you used an implicit many-to-many". I'd have no idea why I got this error if I hit it in the wild. + +## Where I asked "so what do I actually type?" + +1. **Where does `db` come from?** Line 61 is `await db.orm.public.Post.create(...)` with no import, no construction, no file. This is the first line of runtime code on the page and I cannot run it. `user` in `authorId: user.id` is also undefined. +2. **What is `public` and how do I change it?** Line 58 says it "is `public` unless you set another one" — set it where? In `contract.prisma`? In the connection URL? Circular and unactionable. +3. **What's the actual command sequence for a schema change?** Line 27 lists three commands but never as an ordered "do this every time" list, and never again for one-to-one, many-to-many, or polymorphic. By the polymorphic section I no longer knew whether adding `@@base` needs a migration. +4. **How do I read a relation?** Every read is deferred to `/orm/fundamentals/relations-and-joins`. That's four separate punts. At minimum I wanted one `include`-style example on a page about modeling relations, because how I read it is half of how I decide who owns the key. +5. **How do I query a variant's own model?** "Read and write variants through the base model" implies `db.orm.public.Bug` doesn't exist, but the page never says so. I'd try it and find out. +6. **What do `t.create` and `t.disconnect` look like?** Named at line 148, never shown. Only `connect` has an example. +7. **What does a query on `Task` with no `.variant()` return in TypeScript?** Line 192 says "returns every variant" — as what type? A union? Base fields only? This decides whether the feature is usable. +8. **`npx prisma skills sync`** — run where, and does it need anything else? +9. **What's the default `onDelete`?** See above. + +## Places explaining internals when I wanted instructions + +- **Line 27** — "It checks the contract and writes `contract.json` and `contract.d.ts` beside it." I don't care what files it writes. Tell me to run it after every schema edit. +- **Line 54** — "`npx prisma contract emit` matches it to the foreign key on `Post`." Also line 134, same move. The matching step is the tool's business. What I need to know is: declare it or don't, and here's what changes for me if you do. +- **Line 152** — "`npx prisma contract emit` rejects it with an error whose `code` is `PSL_ORPHANED_BACKRELATION`, and the message tells you to add an explicit model for the join table." Three clauses about the error to deliver one instruction: implicit many-to-many is gone, write the join model yourself. +- **Line 197** — "That table holds its extra columns plus the base model's primary key, which links it back to the base table." This one earns its place, because it explains the join cost in the next clause. The rest of the error-code and file-writing detail does not. + +Four separate error codes (lines 29, 31, 91, 152) are presented as page content. They read as reference material bolted into a tutorial. None of them is something I'd look up here. + +## Could I do what the page is for, after one reading? + +I could write the `.prisma` models. The four schema examples are clear, the foreign-key ownership rule at line 25 is stated once and correctly, and the one-to-one "which side" section (93–103) is the best-written part of the page — three concrete signals, all pointing the same way. + +I could not run any of it. I don't know how to get a `db` object, I don't know the full command sequence to get from an edited `contract.prisma` to tables in Postgres, and I don't know what `contract emit` is actually for beyond "run it". I'd also be stuck the first time I wanted to read a relation back, since every read is a link to another page. + +The polymorphic section I'd have to reread. I understand `@@discriminator` and `@@base` in isolation, but not what the TypeScript side looks like — what comes back from an un-filtered `Task` query, whether variant models exist as their own entry points, or why `.variant("Bug")` and the `"bug"` discriminator value are spelled differently. + +Path: `/Users/will/Projects/prisma/web/.claude/worktrees/pr-conflicts-review-fb93f7/wip/c21/dm/r2/relational-databases.mdx` \ No newline at end of file From f6b3dbb848313f760d78ff189f549eaa3e6ac36d Mon Sep 17 00:00:00 2001 From: willbot Date: Mon, 14 Sep 2026 17:20:08 +0200 Subject: [PATCH 05/15] docs(orm): data-modeling reader review round three Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- .../content/docs/orm/data-modeling/index.mdx | 87 +++++++++---------- .../docs/orm/data-modeling/mongodb.mdx | 23 +++-- .../data-modeling/relational-databases.mdx | 57 ++++++------ .../c21-data-modeling/index.fix3.md | 28 ++++++ .../c21-data-modeling/index.round3.md | 86 ++++++++++++++++++ .../c21-data-modeling/mongodb.fix3.md | 35 ++++++++ .../c21-data-modeling/mongodb.round3.md | 71 +++++++++++++++ .../c21-data-modeling/page-conventions.md | 2 +- .../relational-databases.fix3.md | 48 ++++++++++ .../relational-databases.round3.md | 72 +++++++++++++++ 10 files changed, 423 insertions(+), 86 deletions(-) create mode 100644 docs/orm-docs-audit/c21-data-modeling/index.fix3.md create mode 100644 docs/orm-docs-audit/c21-data-modeling/index.round3.md create mode 100644 docs/orm-docs-audit/c21-data-modeling/mongodb.fix3.md create mode 100644 docs/orm-docs-audit/c21-data-modeling/mongodb.round3.md create mode 100644 docs/orm-docs-audit/c21-data-modeling/relational-databases.fix3.md create mode 100644 docs/orm-docs-audit/c21-data-modeling/relational-databases.round3.md diff --git a/apps/docs/content/docs/orm/data-modeling/index.mdx b/apps/docs/content/docs/orm/data-modeling/index.mdx index daebf874456..550338f1b88 100644 --- a/apps/docs/content/docs/orm/data-modeling/index.mdx +++ b/apps/docs/content/docs/orm/data-modeling/index.mdx @@ -10,7 +10,7 @@ Data modeling is the process of describing the data your application needs and h For example, a blog has users, posts, and comments. A user has fields like an email and a name. A post has fields like a title and content. These models also relate to each other: a user can write many posts, and a post can have many comments. -In Prisma ORM, you define this structure in your contract, the `contract.prisma` file that replaced `schema.prisma`. You still describe your models in it. Prisma ORM derives your TypeScript types and your migrations from it. There is no `datasource` or `generator` block any more, and the connection string is in `prisma.config.ts` instead. [A complete contract](/orm/contract-authoring/psl-syntax#a-complete-contract) shows a whole file next to its config. If you are coming from Prisma ORM 7, [what changed](/orm/coming-from-prisma-orm-7) covers the rename and the rest of the differences. +In Prisma ORM, you define this structure in your contract, the `contract.prisma` file that replaced `schema.prisma`. It holds your models, as `schema.prisma` did. Prisma ORM derives your TypeScript types and your migrations from it. There is no `datasource` or `generator` block any more, and the connection string is in `prisma.config.ts` instead. [A complete contract](/orm/contract-authoring/psl-syntax#a-complete-contract) shows a whole file next to its config. If you are coming from Prisma ORM 7, [what changed](/orm/coming-from-prisma-orm-7) covers the rename and the rest of the differences. A contract is built from four building blocks: @@ -19,7 +19,7 @@ A contract is built from four building blocks: - [Scalar fields](#scalar-fields): the values a model stores - [Relations](#relations): how models connect to each other -All four apply to both databases Prisma ORM supports today, PostgreSQL and MongoDB. How you model relations is where the two differ, so continue with [relational data modeling](/orm/data-modeling/relational-databases) or [MongoDB data modeling](/orm/data-modeling/mongodb). +Prisma ORM 8 supports PostgreSQL and MongoDB only. All four blocks apply to both. How you model relations is where the two differ, so continue with [relational data modeling](/orm/data-modeling/relational-databases) or [MongoDB data modeling](/orm/data-modeling/mongodb). ## Models @@ -33,13 +33,11 @@ model User { } ``` -Two posts with the same title are still two different records, and a user who changes their email address is still the same user. - On a relational database, a model becomes a table. On MongoDB, it becomes a collection. ## Primary keys -A primary key is the field that uniquely identifies each record. Give every model one. `npx prisma contract emit` accepts a model without a primary key. The ORM client does not: `update` and `delete` need a primary key or a unique field, and on a model with neither they fail with an error whose `code` is `ORM.ROW_IDENTITY_MISSING`. Mark the primary key with `@id`: +A primary key is the field that uniquely identifies each record. Give every model one. Prisma ORM accepts a model without one, but `update` and `delete` on that model fail at runtime, because they need a primary key or a unique field. Mark the primary key with `@id`: ```prisma tab="PostgreSQL" model User { @@ -56,7 +54,7 @@ model User { } ``` -On MongoDB, the primary key is the document's mandatory `_id` field, and it must be an `ObjectId`. `@map("_id")` gives the field that name in the database. The `create` input type makes `id` optional, so leaving it out type-checks and MongoDB assigns the `ObjectId` for you. `@@map("users")` names the collection and is optional: without it, the collection is the model name with a lowercase first letter. +On MongoDB, the primary key is the document's mandatory `_id` field, and it must be an `ObjectId`. `@map("_id")` gives the field that name in the database. Omit `id` when you create a document. MongoDB assigns it. `@@map("users")` names the collection and is optional: without it, the collection is the model name with a lowercase first letter. ### Natural keys @@ -115,9 +113,7 @@ model ApiToken { } ``` -A UUID takes more space than an integer, but independent services never produce the same one and the value tells nobody how many records you have. `@default(uuid())` and `@default(uuid(4))` both give you a random UUID. Write `@default(uuid(7))` instead for a UUID that starts with the time it was created, so new ids sort after older ones. - -On MongoDB the choice is made for you. The key is the `ObjectId` shown above. +A UUID takes more space than an integer, but independent services never produce the same one and the value tells nobody how many records you have. `@default(uuid())` and `@default(uuid(4))` both give you a random UUID. Write `@default(uuid(7))` instead for a UUID that starts with the time it was created, so new ids sort after older ones. On MongoDB the choice is made for you. The key is the `ObjectId` shown above. ### Composite keys @@ -132,42 +128,47 @@ model UserTag { } ``` -A duplicate `(userId, tagId)` pair would be meaningless, so the pair is the key. The two relation fields that point back at `User` and `Tag` are left out here to keep the key in view, and [many-to-many](/orm/data-modeling/relational-databases#many-to-many) shows the complete model. Avoid composite keys for ordinary models: everything that points at the model then has to store all of the key's fields. +A duplicate `(userId, tagId)` pair would be meaningless, so the pair is the key. The same model is shown with its relation fields under [many-to-many](/orm/data-modeling/relational-databases#many-to-many). Avoid composite keys for ordinary models: everything that points at the model then has to store all of the key's fields. ## Scalar fields A scalar field holds a single value. The common types: -| Type | Stores | -| ---------- | ------------------------------------------------ | -| `String` | Text | -| `Int` | 32-bit integer | -| `BigInt` | 64-bit integer | -| `Float` | Floating-point number | -| `Decimal` | Exact decimal, PostgreSQL `numeric` | -| `Boolean` | `true` / `false` | -| `DateTime` | Timestamp, PostgreSQL `timestamptz` | -| `Bytes` | Binary data, PostgreSQL `bytea` | -| `Json` | Arbitrary JSON value, PostgreSQL's native `json` | -| `Jsonb` | Arbitrary JSON value, PostgreSQL's `jsonb` | -| `ObjectId` | MongoDB document identifier | +| Type | Stores | +| --------------------------- | ------------------------------------------------ | +| `String` | Text | +| `Int` | 32-bit integer | +| `BigInt` | 64-bit integer | +| `Float` | Floating-point number | +| `Decimal`, `Numeric(10, 2)` | Exact decimal, PostgreSQL `numeric` | +| `Boolean` | `true` / `false` | +| `DateTime` | Timestamp, PostgreSQL `timestamptz` | +| `Bytes` | Binary data, PostgreSQL `bytea` | +| `Json` | Arbitrary JSON value, PostgreSQL's native `json` | +| `Jsonb` | Arbitrary JSON value, PostgreSQL's `jsonb` | +| `ObjectId` | MongoDB document identifier | -Where a row above names a PostgreSQL type such as `numeric` or `jsonb`, that is the column type Prisma ORM uses on PostgreSQL. MongoDB has no such mapping, and `ObjectId` is the only type in the table that is specific to it. +Where a row names a PostgreSQL type such as `numeric`, that is the column type on PostgreSQL. `ObjectId` is the only MongoDB-only type. -A `Decimal` value reaches your code as a string, so nothing is rounded through a JavaScript number. Write `price Decimal` when any precision will do. Write `price Numeric(10, 2)` when you want an explicit precision and scale. `Numeric` is `Decimal` with the number of digits written out. Both become a PostgreSQL `numeric` column. +A `Decimal` value reaches your code as a string, so nothing is rounded through a JavaScript number. Write `price Decimal` when any precision will do. Write `price Numeric(10, 2)` when you want an explicit precision and scale. -A `DateTime` value reaches your code as a `Temporal.Instant`, the standard JavaScript type for a point in time. `Temporal` is built into Node.js 26.8.2 and later. On older versions, install the `temporal-polyfill` package. A `Bytes` value reaches your code as a `Uint8Array`. +A `DateTime` value reaches your code as a `Temporal.Instant`, the standard JavaScript type for a point in time. `Temporal` is built into Node.js 26.8.2 and later. On older versions, install the `temporal-polyfill` package and add `import "temporal-polyfill/full/global"`. A `Bytes` value reaches your code as a `Uint8Array`. -Filtering on a `Jsonb` field is whole-value equality only: you can ask whether the column equals a given document, not whether a key inside it matches. A `Json` field offers no comparison at all in the ORM filter, so use `Jsonb` unless you specifically need PostgreSQL's plain `json` column. Reach into the document with [raw SQL](/orm/reference/raw-queries) when you need a path query. +`Json` and `Jsonb` are two separate types. Filtering on a `Jsonb` field is whole-value equality only: you can ask whether the column equals a given document, not whether a key inside it matches. A `Json` field offers no comparison at all in the ORM filter, so use `Jsonb` unless you specifically need PostgreSQL's plain `json` column. Reach into the document with [raw SQL](/orm/reference/raw-queries) when you need a path query. [Extensions](/orm/extensions) add more types, such as vectors or geometry. -[Extensions](/orm/extensions) add more types, such as vectors or geometry. +A trailing `?` makes a field optional: `name String?`. A trailing `[]` makes it a list: `tags String[]`. -Two modifiers change a field: +A field can also hold one of a fixed set of values. Declare them in an `enum` block: -- A trailing `?` makes the field optional: `name String?` -- A trailing `[]` makes it a list: `tags String[]` +```prisma +enum Priority { + @@type("pg/text@1") + Low = "low" + High = "high" +} +``` -A field can also hold one of a fixed set of values. You declare those values in an `enum` block that lists each member and says how the members are stored, and [enums](/orm/contract-authoring/psl-syntax#enums) shows the block to copy. +`@@type("pg/text@1")` says how the values are stored, here as PostgreSQL `text`. [Enums](/orm/contract-authoring/psl-syntax#enums) has the rest of the syntax. ### How to pick a data type @@ -200,7 +201,7 @@ model Post { } ``` -When you are unsure between two sizes, lean toward the wider one. For a counter that could grow past two billion, that means `BigInt` over `Int`. Widening a type later is a migration, so picking the wider type now saves you one. `BigInt` does cost you something in your code: the value reaches you as a JavaScript `bigint`, which `JSON.stringify` refuses to serialize. Convert it to a string before you send it anywhere as JSON. +When you are unsure between two sizes, lean toward the wider one. For a counter that could grow past two billion, that means `BigInt` over `Int`. Widening a type later is a migration, so picking the wider type now saves you one. `BigInt` does cost you something in your code: the value reaches you as a JavaScript `bigint`, which `JSON.stringify` refuses to serialize. Call `.toString()` on it first. Mark a field optional (`?`) only when "absent" means something different from a sensible default. A required field with a default is often the clearer model. @@ -208,12 +209,12 @@ Mark a field optional (`?`) only when "absent" means something different from a A relation connects two models: a user has many posts, a post belongs to one user. -Two kinds of field describe a relation. The model that holds the link needs both of them. The model on the other end needs only a relation field: +Two kinds of field describe a relation, the link field and the relation field. The model that holds the link needs both of them. The model on the other end needs only a relation field: -- The field that stores the other record's primary key, like `authorId`. On a relational database this is the foreign key column, and on MongoDB it is the field holding the other document's `_id`. -- A relation field, typed as the other model, like `author User`. It stores nothing in the database. It tells Prisma ORM how to follow the connection in a query. +- The link field, the one that stores the other record's primary key, like `authorId`. On a relational database this is the foreign key column, and on MongoDB it is the field holding the other document's `_id`. +- A relation field, typed as the other model, like `author User`. You do not get a column for it. It tells Prisma ORM how to follow the connection in a query. -The `@relation` attribute ties them together: `fields` names the local field that stores the link, and `references` names the field it points at on the other model. +The `@relation` attribute ties them together: `fields` names the link field, and `references` names the field it points at on the other model. On the other model, `posts Post[]` is the other end of the same relation. ```prisma model User { @@ -228,22 +229,20 @@ model Post { } ``` -`posts Post[]` on `User` is the other end of the same relation. - Relations come in three kinds: -- One-to-one: a user has at most one profile. Write it as a one-to-many and add `@unique` to the field that stores the link. +- One-to-one: a user has at most one profile. The model that holds the link gets `@unique` on its link field, and the field on `User` is `profile Profile?`, not a list. - One-to-many: a user has many posts, as in the example above. -- Many-to-many: a post has many tags, and a tag appears on many posts. This needs a model for the join table, with one record per connected pair. +- Many-to-many: a post has many tags, and a tag appears on many posts. This needs a model for the join table, with one record per connected pair. List fields on both sides with no such model are rejected, so write the model out as [many-to-many](/orm/data-modeling/relational-databases#many-to-many) shows. -How each kind is stored, and which model should hold the connecting field, is where relational and document databases differ. Continue with the guide for your database: +How each kind is stored, and which model should hold the link field, is where relational and document databases differ. Continue with the guide for your database: - [Relational data modeling](/orm/data-modeling/relational-databases) for PostgreSQL: foreign keys, the model for the join table, and which side owns the key. - [MongoDB data modeling](/orm/data-modeling/mongodb): embedding versus referencing, and polymorphic collections. ## Prompt your coding agent -Projects created with `npm create prisma@latest` include the [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8), instruction files for coding agents. In an existing project, run `npx prisma skills sync`. The `prisma-8` skill covers contract authoring. Prompts that map to each section: +Projects created with `npm create prisma@latest` include the [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8) for your coding agent, instruction files that tell it how Prisma ORM 8 works. In an existing project, run `npx prisma skills sync`. Prompts that map to each section: - "Using the prisma-8 skill, add a Product model with a surrogate id and a unique sku field." - "Add a Country reference table keyed by its ISO code." @@ -254,5 +253,5 @@ Projects created with `npm create prisma@latest` include the [Prisma ORM skills] - [Model relational data](/orm/data-modeling/relational-databases): one-to-one, one-to-many, many-to-many, and polymorphic relations on PostgreSQL. - [Model MongoDB data](/orm/data-modeling/mongodb): embed or reference, and polymorphic collections. -- [Generate a migration](/orm/migrations/generating-a-migration) to get the models you wrote into the database. Run `npx prisma contract emit` after every change to the contract. Then `npx prisma migration plan --name ` writes a migration, and `npx prisma db migrate` applies it. +- [Generate a migration](/orm/migrations/generating-a-migration) to get the models you wrote into the database. `npx prisma contract emit` checks the contract and regenerates your types. `npx prisma migration plan --name ` writes a migration file. `npx prisma db migrate` applies it. - [Query your models](/orm/fundamentals/reading-data) once your contract is in place. diff --git a/apps/docs/content/docs/orm/data-modeling/mongodb.mdx b/apps/docs/content/docs/orm/data-modeling/mongodb.mdx index 5f18e465115..069408f3e8f 100644 --- a/apps/docs/content/docs/orm/data-modeling/mongodb.mdx +++ b/apps/docs/content/docs/orm/data-modeling/mongodb.mdx @@ -10,7 +10,7 @@ MongoDB stores data as documents grouped into collections. A document can nest o ## Documents, collections, and the _id key -There is no `datasource` block on MongoDB. Your `prisma.config.ts` file holds the connection, using `defineConfig` from `@prisma/orm-mongo/config`. See [the data contract](/orm/contract-authoring/the-data-contract). A Prisma ORM model maps to a collection, and each record is a document. Every document has an `_id` field as its primary key. `ObjectId` is the type MongoDB expects for it: +Your models go in your contract, the `contract.prisma` file that replaced `schema.prisma`. There is no `datasource` block on MongoDB. The connection string is in your `prisma.config.ts` file instead, passed to `defineConfig` from `@prisma/orm-mongo/config`. See [the data contract](/orm/contract-authoring/the-data-contract) for that file. A model maps to a collection, and each record is a document. Every document has an `_id` field as its primary key. `ObjectId` is the type MongoDB expects for it: ```prisma model User { @@ -21,13 +21,13 @@ model User { } ``` -`@map("_id")` maps the model's `id` field to MongoDB's mandatory `_id` field, and `@@map("users")` names the collection. `@@map` is optional. Without it the collection takes the model name with a lowercase first letter, so `user`. `ObjectId` replaces `String @db.ObjectId` and `@default(auto())`. Writing either of those attributes is an error whose `code` is `PSL_UNSUPPORTED_FIELD_ATTRIBUTE`. Leave the id out of the values you pass to `create`, and MongoDB assigns it: +In your TypeScript code the field is called `_id`, not `id`. `@map("_id")` maps the model's `id` field to MongoDB's mandatory `_id` field and sets the name on both sides, so you write `user._id` and `where({ _id })`. `@@map("users")` names the collection. `@@map` is optional. Without it the collection takes the model name with a lowercase first letter, so `user`. Write `ObjectId` for the key type. The old `String @db.ObjectId` and `@default(auto())` are no longer allowed and fail at `npx prisma contract emit`, the command that checks your contract and regenerates your types. Leave the id out of the values you pass to `create`, and MongoDB assigns it: ```ts const user = await db.orm.users.create({ name: "Ada", email: "ada@example.com" }); ``` -`db` is the client you create once in `src/prisma/db.ts`, and `npx prisma orm init` writes that file. `db.orm.users` is the accessor for the collection, and it follows the collection name, so `@@map` renames the accessor as well. In Prisma ORM 7 it did not. The values go straight into `create`, without the `data:` wrapper Prisma ORM 7 used. The id comes back as a string, and you pass it back as a string. There is no `ObjectId` to construct: +`npx prisma orm init` writes `src/prisma/db.ts`, which creates the client `db`. `db.orm.users` is how you query the `users` collection. The name after `db.orm.` is the collection name, so with `@@map("users")` you write `db.orm.users`, not `db.orm.user`. The values go straight into `create`, without the `data:` wrapper Prisma ORM 7 used. The id comes back as a string, and you pass it back as a string. There is no `ObjectId` to construct: ```ts const found = await db.orm.users.where({ _id: user._id }).first(); @@ -71,28 +71,27 @@ model User { } ``` -A single embedded value models a one-to-one. A list of embedded values models a one-to-many: `addresses Address[]` stores every address inside the user document, with no second collection and no join to load them. Pass an embedded value to `create` as a plain nested object: +A single embedded value models a one-to-one relation. A list of embedded values models a one-to-many relation: `addresses Address[]` stores every address inside the user document, with no second collection and no join to load them. Pass an embedded value to `create` as a plain nested object: ```ts const address = { street: "1 Main St", city: "Berlin", country: "DE" }; await db.orm.users.create({ name: "Ada", address }); ``` -In an `update`, `u` gives you each field of the document to change. Give it the path to change one field inside an embedded value, and use `.push(...)` and `.pull(...)` to add to and remove from a list of scalars: +In an `update`, `u` builds the changes you want. Use `u.fieldName` to change a top-level field, and `u("path.to.field")` to change a field inside an embedded document. Use `.push(...)` and `.pull(...)` to add to and remove from a list of scalars: ```ts await db.orm.users.where({ _id: user._id }).update((u) => [u("address.city").set("Hamburg")]); await db.orm.users.where({ _id: user._id }).update((u) => [u.tags.push("admin"), u.tags.pull("new")]); ``` -`.where()` filters on top-level model fields only. To match on a field inside an embedded document, use the pipeline builder, a chained builder for MongoDB aggregation pipelines: +Replace a whole embedded value with `u("address").set({ ... })`, and remove it with `u("address").unset()`. A list of embedded values is replaced as a whole with `u.addresses.set([ ... ])`. `.where()` filters on top-level model fields only. To match on a field inside an embedded document, use the pipeline builder, a chained builder for MongoDB aggregation pipelines: ```ts const berliners = db.query.from("users").match((f) => f("address.city").eq("Berlin")).build(); -const rows = await (await db.runtime()).query(berliners); ``` -See [the pipeline builder](/orm/fundamentals/advanced-queries#mongodb-pipeline-builder) for the rest of the stages. Embedded arrays are only a good idea while they stay bounded. An array that grows forever slows every read of the parent and pushes the document toward MongoDB's 16 MB limit, and a write that exceeds it fails. When a list has no natural cap, reference it instead. +See [the pipeline builder](/orm/fundamentals/advanced-queries#mongodb-pipeline-builder) for running it and what comes back. Embedded arrays are only a good idea while they stay bounded. An array that grows forever slows every read of the parent and pushes the document toward MongoDB's 16 MB limit, and a write that exceeds it fails. When a list has no natural cap, reference it instead. ## References across collections @@ -114,7 +113,7 @@ model Post { } ``` -This is a one-to-many by reference: many posts point at one user, and each post is queryable on its own. To link a new post to a user, set the reference field yourself. To load a post together with its author, chain `.include("author")`, which adds the related record to every row you get back: +This is a one-to-many by reference: many posts point at one user, and each post is queryable on its own. There are no nested writes on MongoDB: create the user first, then the post with its `authorId`. To load a post together with its author, chain `.include("author")`, which adds the related record to every row you get back: ```ts await db.orm.posts.create({ title: "Hello", authorId: user._id }); @@ -146,9 +145,9 @@ model Tutorial { } ``` -`@@discriminator(kind)` takes the name of the field that records each document's variant, so write `kind` and not `"kind"`. `@@base(Post, "article")` declares `Article` as the `"article"` variant of `Post`. A variant declares no `@@map` of its own and uses its base's collection. A variant has every field of its base plus its own, so an `Article` has `id`, `title`, `kind`, and `summary`. +`@@discriminator(kind)` takes the name of the field that records each document's variant, so write `kind` and not `"kind"`. `@@base(Post, "article")` declares `Article` as the `"article"` variant of `Post`. A variant declares only its own fields. `id`, `title`, and `kind` come from `Post`. A variant declares no `@@map` of its own and uses its base's collection. -A query for posts returns articles and tutorials together. `db.orm.posts.variant("Article")` limits the query to one variant. `.variant()` takes the variant's model name, `"Article"`, and not the discriminator value `"article"`. Creating through a variant fills in `kind` for you: +A query for posts returns articles and tutorials together. You reach a single variant through the base model: `db.orm.posts.variant("Article")` limits the query to one variant. `.variant()` takes the variant's model name, `"Article"`, and not the discriminator value `"article"`. Creating through a variant fills in `kind` for you: ```ts const article = await db.orm.posts.variant("Article").create({ title: "Hello", summary: "A short post" }); @@ -167,7 +166,7 @@ Projects created with `npm create prisma@latest` include the [Prisma ORM skills] ## Next steps -- Models go in your contract, the `contract.prisma` file that replaced `schema.prisma`. Run `npx prisma contract emit` after every change to it, then `npx prisma migration plan --name ` writes a migration and `npx prisma db migrate` applies it, creating the collections and indexes. See [generating a migration](/orm/migrations/generating-a-migration). +- Run `npx prisma contract emit` after every change to your contract. Then `npx prisma migration plan --name ` writes a migration and `npx prisma db migrate` applies it, creating the collections and indexes. See [generating a migration](/orm/migrations/generating-a-migration). - [Query documents](/orm/fundamentals/reading-data) and [resolve references](/orm/fundamentals/relations-and-joins) with `.include(...)`. - Aggregate and reshape documents with the [pipeline builder](/orm/fundamentals/advanced-queries#mongodb-pipeline-builder). - To model relational data, see the [relational data modeling guide](/orm/data-modeling/relational-databases). diff --git a/apps/docs/content/docs/orm/data-modeling/relational-databases.mdx b/apps/docs/content/docs/orm/data-modeling/relational-databases.mdx index 5bee0a4dffb..5ac4d1ac029 100644 --- a/apps/docs/content/docs/orm/data-modeling/relational-databases.mdx +++ b/apps/docs/content/docs/orm/data-modeling/relational-databases.mdx @@ -6,9 +6,7 @@ metaTitle: Relational data modeling in Prisma ORM metaDescription: 'How to model one-to-one, one-to-many, many-to-many, and polymorphic relations in Prisma ORM for PostgreSQL, including which side owns the foreign key.' --- -In a relational database, each model becomes a table and each scalar field becomes a column. Models connect through foreign keys: a column in one table that holds the primary key of a row in another table. - -If you are new to models and keys, start with the [data modeling overview](/orm/data-modeling). +In a relational database, each model becomes a table and each scalar field becomes a column. Models connect through foreign keys: a column in one table that holds the primary key of a row in another table. If you are new to models and keys, start with the [data modeling overview](/orm/data-modeling). ## How relations are declared @@ -22,13 +20,21 @@ model Post { } ``` -The side that declares the `fields` argument owns the foreign key. Deciding which side that is is the main choice in each of the relation kinds below. +The side that declares the `fields` argument owns the foreign key. Deciding which side declares `fields` is the main choice in each relation kind below. + +Your contract is the `contract.prisma` file that replaced `schema.prisma`. After every change to it, run `npx prisma contract emit`. That command checks the contract and regenerates your types. It only updates your code, not the database. + +To let a post have no author, make the foreign key field and the relation field both optional: `authorId Int?` and `author User?`. The two fields have to agree. Making one optional and the other required is rejected. -Your contract is the `contract.prisma` file that replaced `schema.prisma`. After every change to it, run `npx prisma contract emit`. That command checks the contract and regenerates your types. It only updates your code. To change the database, run `npx prisma migration plan --name add-posts`, which writes a migration file, and then `npx prisma db migrate`, which applies that file. See [Generating a migration](/orm/migrations/generating-a-migration). +Referential actions decide what the database does to the rows on the foreign key side when the row they point at is deleted or its key changes. Add `onDelete` and `onUpdate` to `@relation`, as in `@relation(fields: [userId], references: [id], onDelete: Cascade)`. They become the foreign key's `ON DELETE` and `ON UPDATE` clauses. Write neither and the foreign key gets neither clause, so the database decides what happens. The five actions are: -To let a post have no author, make the foreign key field and the relation field both optional: `authorId Int?` and `author User?`. Making one optional and the other required is rejected with an error whose `code` is `PSL_RELATION_NULLABILITY_MISMATCH`. `PSL` is the Prisma Schema Language, the language `contract.prisma` is written in. +- `Cascade` deletes or updates the matching rows. +- `Restrict` blocks the change. +- `SetNull` clears the foreign key. +- `SetDefault` writes the foreign key's default value. +- `NoAction` writes the clause out, which does the same thing as writing no clause at all. -Two relations between the same pair of models need a name on both sides, such as `@relation("Authored", fields: [authorId], references: [id])` on one side and `@relation("Authored")` on the other. Without the name, `npx prisma contract emit` reports an error whose `code` is `PSL_AMBIGUOUS_BACKRELATION`. +Two relations between the same pair of models need a name on both sides, such as `@relation("Authored", fields: [authorId], references: [id])` on one side and `@relation("Authored")` on the other. Without the name, `npx prisma contract emit` reports an error. ## One-to-many @@ -49,11 +55,11 @@ model Post { } ``` -The foreign key is on the "many" side, `Post.authorId`, because each post points at exactly one user, while a user points at an open-ended list of posts. Use a one-to-many whenever a record belongs to one parent and a parent has many children: comments on a post, line items on an order, employees in a department. +The foreign key is on the "many" side, `Post.authorId`, because each post points at exactly one user, while a user points at an open-ended list of posts. Use a one-to-many whenever a record belongs to one parent and a parent has many children: comments on a post, line items on an order, employees in a department. A model can also point at itself: a category with a parent category is an ordinary one-to-many whose relation field is typed as the same model. -`posts Post[]` on `User` is the mirror field, the field on the other side of the relation, which stores nothing in the database. Declaring it is optional. Prisma ORM 7 required this field; Prisma ORM 8 does not. Leave it out and the relation still works, and you read it from the foreign key side only. +`posts Post[]` on `User` is the mirror field, the field on the other side of the relation, which stores nothing in the database. Prisma ORM 7 called it the back-relation, and its error codes still use that word. Prisma ORM 7 required this field. Prisma ORM 8 does not. Leave it out and the relation still works, and you read it from the foreign key side only, with `db.orm.public.Post.where({ authorId: user.id }).all()`. `.all()` is the call that runs the query and returns every matching row. -To connect a post to a user, set the foreign key when you create the post. `db` is the client you create once in `src/prisma/db.ts`, the file `npx prisma orm init` writes. `db.orm` holds your models by model name, and `public` is the PostgreSQL schema your tables live in, unless your contract puts the model in another schema with a `namespace` block. Here `user` is a user record you read earlier: +To connect a post to a user, set the foreign key when you create the post. `db` is the client you create once in `src/prisma/db.ts`, the file `npx prisma orm init` writes. `db.orm` holds your models by model name, and `public` is the PostgreSQL schema your tables live in. Here `user` is a user record you read earlier: ```typescript const post = await db.orm.public.Post.create({ @@ -62,11 +68,7 @@ const post = await db.orm.public.Post.create({ }); ``` -Reading back the other way, `.include("author")` returns each post with its user nested on it, and [Relations and joins](/orm/fundamentals/relations-and-joins#one-to-many) covers querying in both directions: - -```typescript -const posts = await db.orm.public.Post.include("author").all(); -``` +Reading back the other way, `db.orm.public.Post.include("author").all()` returns each post with its user nested on it. [Relations and joins](/orm/fundamentals/relations-and-joins#one-to-many) covers querying in both directions. ## One-to-one @@ -88,7 +90,11 @@ model Profile { } ``` -Declare the relation on the side that holds the foreign key, `Profile` here. You can add the mirror field to the other model as well. Three rules, in the order you apply them. First, add the field: `profile Profile?` on `User`, with a type and nothing else, because it needs no `@relation` of its own. Second, make it optional, because nothing in the database guarantees a matching profile row. Third, make the foreign key it mirrors `@unique`, or `npx prisma contract emit` reports an error whose `code` is `PSL_NON_UNIQUE_BACKRELATION`. With the mirror field in place, [the relation reads from either side](/orm/fundamentals/relations-and-joins#one-to-one). +Declare the relation on the side that holds the foreign key, `Profile` here. You can also add the mirror field `profile Profile?` to `User`, with a type and nothing else, because it needs no `@relation` of its own. It has to be optional, because nothing in the database guarantees a matching profile row. The `@unique` already on `Profile.userId` is what allows the mirror field. With it in place, [the relation reads from either side](/orm/fundamentals/relations-and-joins#one-to-one): + +```typescript +const users = await db.orm.public.User.include("profile").all(); +``` ### Which side owns the foreign key @@ -100,8 +106,6 @@ The other signals all point at the same side. The dependent record is: - The one you would delete when the parent goes away. - The one whose absence is normal. A user without a profile is fine, and a profile without a user is broken data. -Placing the key on the dependent side keeps `User` free of profile concerns, lets a user exist with no profile, and guarantees every profile has exactly one user. - ## Many-to-many A many-to-many relation (`m:n`) connects many records on each side: a post has many tags, and a tag applies to many posts. @@ -131,7 +135,7 @@ model PostTag { } ``` -The composite primary key on `PostTag`, `@@id([postId, tagId])`, allows one record per pair. Note that `Post.tags` is typed `Tag[]`, not `PostTag[]`: you name the model at the other end, and Prisma ORM works out the join through `PostTag`. The same goes for `Tag.posts`. If a second join table connects the same two models, name the relation on both sides. +The composite primary key on `PostTag`, `@@id([postId, tagId])`, allows one record per pair. Note that `Post.tags` is typed `Tag[]`, not `PostTag[]`: you name the model at the other end. The same goes for `Tag.posts`. If a second join table connects the same two models, put the relation name on the list field, `Post.tags`, and on `PostTag.post`, the field that points back at `Post`. Connect a post to a tag with a nested write on the mirror field: @@ -145,7 +149,7 @@ await db.orm.public.Post.where({ id: post.id }).update({ `PostTag` is an ordinary model, so the join table can hold data about the pairing itself. To record when a tag was added, add `addedAt DateTime @default(now())` to `PostTag`. -An implicit many-to-many, with list fields on both sides and no join table, as in Prisma ORM 7, is not supported. Declare the model for the join table yourself. If you leave it out, `npx prisma contract emit` reports an error whose `code` is `PSL_ORPHANED_BACKRELATION`. [Relations and joins](/orm/fundamentals/relations-and-joins#many-to-many) shows how to read across the join table in one query. +An implicit many-to-many, with list fields on both sides and no join table, as in Prisma ORM 7, is not supported. Declare the model for the join table yourself. If you skip that model, `npx prisma contract emit` reports an error whose `code` is `PSL_ORPHANED_BACKRELATION`. [Relations and joins](/orm/fundamentals/relations-and-joins#many-to-many) shows how to read across the join table in one query. ## Polymorphic relations @@ -177,7 +181,7 @@ model Feature { } ``` -`@@discriminator(type)` marks the field that records the variant. `@@base(Task, "bug")` declares that `Bug` is the `"bug"` variant of `Task`. A variant has every field of its base plus the ones it declares, and it needs no `@id` of its own, because it uses the base model's primary key. +`@@discriminator(type)` marks the field that records the variant. `@@base(Task, "bug")` declares that `Bug` is the `"bug"` variant of `Task`. `@@discriminator(type)` and the first argument of `@@base` take a field or model name without quotes, while the variant value is a string. A variant has every field of its base plus the ones it declares, and it needs no `@id` of its own, because it uses the base model's primary key. Read and write variants through the base model. `.variant(...)` limits the query to one variant, and it takes the variant's model name, `"Bug"`. Passing the discriminator value `"bug"` is a TypeScript error, because the only names it accepts are the model names of the base model's variants. On a create, the discriminator value is filled in for you: @@ -185,12 +189,12 @@ Read and write variants through the base model. `.variant(...)` limits the query await db.orm.public.Task.variant("Bug").create({ title: "Crash on save", severity: "high" }); ``` -A read works the same way. A query on `Task` with no `.variant(...)` returns every variant, and each row carries the base fields plus only its own variant's fields, with the discriminator field holding that variant's value, such as `"bug"`. +A read works the same way. A query on `Task` with no `.variant(...)` returns every variant, and each row carries the base fields plus only its own variant's fields, with the discriminator field holding that variant's value, such as `"bug"`. In TypeScript those rows are a union with one member per variant, and each member's discriminator field is that variant's value, so switch on the discriminator field to reach a variant's own fields. `@@map` decides how a variant is stored: - A variant with no `@@map`, like `Bug`, shares the base table. Its extra columns are added to the base table and are nullable there, because a `Feature` row has no severity. -- A variant with its own `@@map`, like `Feature`, gets its own table. That table holds its extra columns plus the base model's primary key, which links it back to the base table. Those columns can be `NOT NULL`, and reading a full variant costs a join. +- A variant with its own `@@map`, like `Feature`, gets its own table. Its columns there can be `NOT NULL`, and reading a full variant costs a join. Start with the shared table. Give a variant its own table when its columns have to be `NOT NULL` in the database, or when the variant has many columns of its own. Use a base model with variants when you query the variants together (one feed of tasks, one stream of events) and each variant has real structure of its own. If you never query them together, separate models are simpler. If the variants differ by one nullable field, a single model is simpler still. @@ -206,10 +210,5 @@ Projects created with `npm create prisma@latest` include the [Prisma ORM skills] ## Next steps - [Query relations](/orm/fundamentals/relations-and-joins) with `.include(...)`, filters on relation data, and reads across the join table. +- To change the database to match the contract, run `npx prisma migration plan --name `, which writes a migration file, and then `npx prisma db migrate`, which applies it. See [Generating a migration](/orm/migrations/generating-a-migration). - To model for MongoDB, see the [MongoDB data modeling guide](/orm/data-modeling/mongodb). -- Referential actions: add `onDelete` and `onUpdate` to `@relation`, as in `@relation(fields: [userId], references: [id], onDelete: Cascade)`. They become the foreign key's `ON DELETE` and `ON UPDATE` clauses. Write neither and the foreign key gets neither clause, so the database decides what happens. The five actions are: - - `Cascade` deletes or updates the matching rows. - - `Restrict` blocks the change. - - `SetNull` clears the foreign key. - - `SetDefault` writes the foreign key's default value. - - `NoAction` leaves the decision to the database. diff --git a/docs/orm-docs-audit/c21-data-modeling/index.fix3.md b/docs/orm-docs-audit/c21-data-modeling/index.fix3.md new file mode 100644 index 00000000000..ce6e884e582 --- /dev/null +++ b/docs/orm-docs-audit/c21-data-modeling/index.fix3.md @@ -0,0 +1,28 @@ +Done. Edited only `/Users/will/Projects/prisma/web/.claude/worktrees/pr-conflicts-review-fb93f7/wip/c21/dm/r3/index.mdx`. 258 lines before, 257 after. `check-plain.sh` is clean, no em-dashes, no page-conventions banned words. + +**Marks and fixes** + +- "contract" undefined: kept the conventions wording and replaced "You still describe your models in it" with "It holds your models, as `schema.prisma` did." +- "both databases Prisma ORM supports today": now "Prisma ORM 8 supports PostgreSQL and MongoDB only. All four blocks apply to both." +- "Two posts with the same title..." sentence: cut. +- Primary key paragraph: "Prisma ORM accepts a model without one, but `update` and `delete` on that model fail at runtime, because they need a primary key or a unique field." The error code is gone, and `prisma contract emit` no longer appears before it is explained. +- "the `create` input type": "Omit `id` when you create a document. MongoDB assigns it." +- Composite key commentary: "The same model is shown with its relation fields under [many-to-many](/orm/data-modeling/relational-databases#many-to-many)." +- How to read the type table: one sentence pair, "Where a row names a PostgreSQL type such as `numeric`, that is the column type on PostgreSQL. `ObjectId` is the only MongoDB-only type." +- `Numeric(10, 2)`: now introduced in the table's `Decimal` row (`` `Decimal`, `Numeric(10, 2)` ``); the "`Numeric` is `Decimal` with the number of digits written out" sentence and the redundant "Both become a PostgreSQL `numeric` column" are cut. +- `Json` vs `Jsonb`: the paragraph opens "`Json` and `Jsonb` are two separate types." +- Enum: added the five-line block with `@@type("pg/text@1")` plus one clause, "says how the values are stored, here as PostgreSQL `text`", and the link for the rest. +- Relation field naming: named once as "the link field, the one that stores the other record's primary key", and `fields` is now explained as "names the link field". The lead-in names both kinds before the bullets. "It stores nothing in the database" became "You do not get a column for it." "the connecting field" later in the section became "the link field". +- One-to-one: "The model that holds the link gets `@unique` on its link field, and the field on `User` is `profile Profile?`, not a list." +- Many-to-many: added "List fields on both sides with no such model are rejected, so write the model out as [many-to-many](...) shows." +- `temporal-polyfill`: "install the `temporal-polyfill` package and add `import "temporal-polyfill/full/global"`." +- `BigInt`: "Call `.toString()` on it first." +- Next steps commands: each gets its own short sentence (emit checks the contract and regenerates your types; `migration plan --name ` writes a migration file; `db migrate` applies it). +- Skills sentence moved to the conventions wording; "The `prisma-8` skill covers contract authoring" cut for room. +- Not changed, as instructed: no `prisma.config.ts` snippet, no project-setup section; "polymorphic collections" still links to the MongoDB page rather than being redefined. + +**Line budget paid for** by cutting the "two posts" sentence, folding the MongoDB surrogate-key sentence into the UUID paragraph, folding the Extensions sentence into the `Jsonb` paragraph, folding "`posts Post[]` is the other end" into the `@relation` paragraph, and collapsing the two modifier bullets into one line. + +**Facts checked in-repo** (both already verified for me, confirmed against the docs source): enum block at `apps/docs/content/docs/orm/contract-authoring/psl-syntax.mdx:188-196`; polyfill import at `apps/docs/content/docs/orm/reference/raw-queries.mdx:94`. + +**Q list:** none. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/index.round3.md b/docs/orm-docs-audit/c21-data-modeling/index.round3.md new file mode 100644 index 00000000000..fe6eb724ca4 --- /dev/null +++ b/docs/orm-docs-audit/c21-data-modeling/index.round3.md @@ -0,0 +1,86 @@ +Read once, top to bottom, as a Prisma 7 user. + +## Sentences I could not restate after one reading + +**Line 13** — "In Prisma ORM, you define this structure in your contract, the `contract.prisma` file that replaced `schema.prisma`." +Stopped by: "your contract." The page names the new word and immediately uses it as if I now know it. I know what a schema is. I do not know why this is called a contract, or whether "contract" means something more than "the file formerly called schema.prisma." Plainest version I'd have understood: "Prisma ORM 8 renames `schema.prisma` to `contract.prisma`. It holds the same model definitions; the file is called your contract." + +**Line 22** — "All four apply to both databases Prisma ORM supports today, PostgreSQL and MongoDB." +Stopped by the implication, not the words. I ship on MySQL. This sentence says my database is gone and then moves on to the next heading. Either say it plainly ("Prisma ORM 8 supports PostgreSQL and MongoDB only") or don't raise it here. + +**Line 36** — "Two posts with the same title are still two different records, and a user who changes their email address is still the same user." +Stopped by: I cannot tell what this sentence is for. It sits under a model code block, makes no claim about models, and nothing before or after picks it up. I assume it is a hint at primary keys, but the primary key section never refers back to it. + +**Line 42** — "`npx prisma contract emit` accepts a model without a primary key. The ORM client does not." +Two problems. First, `npx prisma contract emit` is used here with no explanation of what it does; the page does not say until line 257. Second, "The ORM client does not" reads as a parallel to "accepts," but a client doesn't accept a model — it fails at runtime. Plainer: "Prisma will generate a client for a model with no primary key, but `update` and `delete` on that model fail at runtime." + +**Line 59** — "The `create` input type makes `id` optional, so leaving it out type-checks and MongoDB assigns the `ObjectId` for you." +Stopped by "the `create` input type." That is the generated-types' name for something, and I only wanted to know: omit `id` when creating. Plainer: "Omit `id` when you create a document; MongoDB assigns it." + +**Line 155** — "Where a row above names a PostgreSQL type such as `numeric` or `jsonb`, that is the column type Prisma ORM uses on PostgreSQL. MongoDB has no such mapping, and `ObjectId` is the only type in the table that is specific to it." +Stopped by: this is a sentence about how to read the table I just read. Two ideas crammed together (what the PostgreSQL column names mean; which row is MongoDB-only). I had to reread to work out it wasn't telling me anything I do. + +**Line 157** — "Write `price Numeric(10, 2)` when you want an explicit precision and scale. `Numeric` is `Decimal` with the number of digits written out." +This is the worst sentence on the page for me. `Numeric` is not in the type table directly above. So is it a type? An alias? In Prisma 7 I wrote `Decimal @db.Decimal(10, 2)`, so I cannot tell whether `@db.` is gone, whether `Numeric` is a new scalar, or whether every type now takes arguments. "with the number of digits written out" is a guess-invitation, not a definition. + +**Line 161** — "Filtering on a `Jsonb` field is whole-value equality only: you can ask whether the column equals a given document, not whether a key inside it matches." +Restatable on the second read, not the first. Also unexplained: `Json` and `Jsonb` are now two separate scalar types. In Prisma 7 there was one `Json` type. The page never flags the change. + +**Line 170** — "You declare those values in an `enum` block that lists each member and says how the members are stored." +Stopped hard by "says how the members are stored." In Prisma 7 an enum is just a list of names — storage is not part of it. This sentence tells me the syntax changed and then refuses to show me the syntax, linking away instead. An enum block is four lines. Show it. + +**Line 211** — "Two kinds of field describe a relation. The model that holds the link needs both of them. The model on the other end needs only a relation field." +Three abstractions stacked before either kind of field has been named. I had to read the bullets, then come back up to this paragraph for it to parse. + +**Line 213** — "The field that stores the other record's primary key, like `authorId`." +The second bullet is named ("A relation field"). This one is not. I never learn what to call it, which matters because the next paragraph says `fields` "names the local field" — a third name for the same thing. + +**Line 235** — "One-to-one: a user has at most one profile. Write it as a one-to-many and add `@unique` to the field that stores the link." +Not restatable. "Write it as a one-to-many" — but a one-to-many has `Profile[]` on the user side. Do I write `Profile[]` and then it isn't a list? Or `Profile?`? The page tells me to do something whose result contradicts its own description, and shows no code. + +## Words and phrases I had to guess + +- **"contract"** — guessed: the new name for schema.prisma, nothing more. +- **"emit"** in `prisma contract emit` — guessed: the new name for `prisma generate`. +- **"the ORM client"** (line 42) — guessed: what used to be called Prisma Client. The page never says they are the same thing. +- **"`ORM.ROW_IDENTITY_MISSING`"** — guessed: an error code string I could match on in a catch block. Not told where I see it. +- **"`Numeric(10, 2)`"** — guessed: a scalar type that takes precision arguments, replacing `@db.Decimal`. +- **"`Jsonb`"** as a distinct type — guessed: what `Json @db.JsonB` used to be. +- **"a model for the join table"** (line 237) — guessed: the thing that used to be called an explicit many-to-many relation table. +- **"skills"** / **"the `prisma-8` skill"** (line 246) — guessed: instruction files for Claude/Cursor. The page half-defines it ("instruction files for coding agents") but I still don't know what `npx prisma skills sync` writes or where. +- **"polymorphic collections"** (line 242, 256) — guessed: storing different shapes in one collection. Never explained, appears twice. + +## Where I asked "so what do I actually type?" + +1. **Line 13, `prisma.config.ts`.** I'm told the connection string moved there and the `datasource` block is gone. That is the single change most likely to break my first five minutes, and the page shows zero lines of it. Not even a three-line snippet. +2. **Line 13, where does `contract.prisma` go?** Same directory as `schema.prisma` did? Is there a rename command, or do I move the file myself? +3. **Line 170, enums.** Told the block "says how the members are stored" and sent to another page. Show the block. +4. **Line 235, one-to-one.** Described in prose only. I cannot write it from this. +5. **Line 237, many-to-many.** "This needs a model for the join table." In Prisma 7 I could write `tags Tag[]` / `posts Post[]` and get an implicit join table. Is implicit many-to-many removed? The page implies yes by omission and never says so. No code shown. +6. **Line 159, `Temporal`.** "install the `temporal-polyfill` package" — do I then need to import or configure anything, or does Prisma pick it up? +7. **Line 203, `BigInt`.** "Convert it to a string before you send it anywhere as JSON." Where — in my serializer, per field, globally? No snippet. +8. **Line 135, the composite key example.** "The two relation fields ... are left out here." So if I copy this block, does it compile? I can't tell. +9. **Line 257, the three new commands.** `npx prisma contract emit`, `npx prisma migration plan --name `, `npx prisma db migrate`. All three replace `prisma migrate dev` and all three appear for the first time in one bullet, with no statement of what each does or when. The naming is also inconsistent — `migration plan` versus `db migrate` — which made me read the bullet three times to be sure it wasn't a typo. + +## Where it explains internals when I wanted instructions + +- **Line 42**, `ORM.ROW_IDENTITY_MISSING`. The instruction is "give every model a primary key." The error code is machinery. It sits in the middle of the instruction and interrupts it. +- **Line 59**, "The `create` input type makes `id` optional, so leaving it out type-checks." I wanted "omit `id`." I got the mechanism by which omitting it works. +- **Line 155**, the paragraph explaining how to read the type table's PostgreSQL column. +- **Line 214**, "It stores nothing in the database. It tells Prisma ORM how to follow the connection in a query." This one is borderline — it does correct a real misconception — but it's framed as how Prisma works rather than as "you don't get a column for this." +- **Line 135**, "are left out here to keep the key in view." Commentary about the docs' own editing choices. + +## After one reading, could I do what the page is for? + +Partly. The middle of the page is genuinely usable: I could write models, pick a primary key strategy, and choose scalar types from the "How to pick a data type" section without rereading. That section is the best writing on the page — concrete rule, concrete reason, code. + +What I still could not do: + +- **Set up a Prisma 8 project at all.** I know `datasource` and `generator` are gone and that a `prisma.config.ts` exists. I have never seen one. I cannot connect to a database from this page. +- **Write an enum.** The page describes syntax it declines to show. +- **Write a one-to-one or a many-to-many relation.** Prose only, and the one-to-one instruction contradicts itself as written. +- **Write a `Decimal` with fixed precision.** `Numeric(10, 2)` appears from nowhere and is not in the type table. +- **Run a migration.** Three unfamiliar commands in one bullet, none explained, in a "Next steps" list rather than anywhere I'd look for them. +- **Know whether my MySQL app has a path forward.** "both databases Prisma ORM supports today" is the only signal, and it's dropped in a transitional sentence. + +The page is billed as the four building blocks. It covers three of them well and the fourth — relations — mostly by pointing elsewhere. That's defensible for a hub page. The unforced errors are the undefined new vocabulary ("contract", "emit", "ORM client"), the `Numeric` type that isn't in its own type table, and the enum paragraph that describes syntax instead of printing it. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/mongodb.fix3.md b/docs/orm-docs-audit/c21-data-modeling/mongodb.fix3.md new file mode 100644 index 00000000000..d79d46cdc88 --- /dev/null +++ b/docs/orm-docs-audit/c21-data-modeling/mongodb.fix3.md @@ -0,0 +1,35 @@ +Done. `wip/c21/dm/r3/mongodb.mdx`, 173 → 172 lines, plain-language check clean, no em-dashes, no extra banned words. + +## Marks and what I did + +**1. `PSL_UNSUPPORTED_FIELD_ATTRIBUTE`** — code cut. Now: "Write `ObjectId` for the key type. The old `String @db.ObjectId` and `@default(auto())` are no longer allowed and fail at `npx prisma contract emit`, the command that checks your contract and regenerates your types." That also defines `emit` at its first use. + +**2. "accessor" / version aside** — "`db.orm.users` is how you query the `users` collection. The name after `db.orm.` is the collection name, so with `@@map("users")` you write `db.orm.users`, not `db.orm.user`." "In Prisma ORM 7 it did not" cut. The word "accessor" no longer appears in prose. + +**3. `db.ts` self-contradiction** — "`npx prisma orm init` writes `src/prisma/db.ts`, which creates the client `db`." + +**4. "`u` gives you each field"** — "In an `update`, `u` builds the changes you want. Use `u.fieldName` to change a top-level field, and `u("path.to.field")` to change a field inside an embedded document." + +**5. `db.runtime()` double await** — the `db.runtime()` line is cut. The filter example stays and ends: "See [the pipeline builder](/orm/fundamentals/advanced-queries#mongodb-pipeline-builder) for running it and what comes back." + +**6. Buried operational sentence** — the contract definition moved to the first mention of "contract" (first sentence under the first heading): "Your models go in your contract, the `contract.prisma` file that replaced `schema.prisma`." The Next steps bullet now starts at `npx prisma contract emit`, whose meaning is given where it first appears. + +**Guessed words:** "contract" defined at first use (see above); "accessor" and "PSL" removed; "models a one-to-one" → "models a one-to-one relation" (and the list case → "one-to-many relation"); "pipeline builder" keeps its existing one-clause definition; "discriminator" and "variant" the reader said are adequate, so untouched. + +**"What do I type?" gaps:** +- `_id` vs `id`: added right after the first model, folded into the `@map` sentence — "In your TypeScript code the field is called `_id`, not `id`. `@map("_id")` maps the model's `id` field to MongoDB's mandatory `_id` field and sets the name on both sides, so you write `user._id` and `where({ _id })`." +- `prisma.config.ts`: no snippet added per the brief; the sentence now says the connection string is in that file and links to the data contract page. +- Appending to / replacing embedded data: "Replace a whole embedded value with `u("address").set({ ... })`, and remove it with `u("address").unset()`. A list of embedded values is replaced as a whole with `u.addresses.set([ ... ])`." +- Nested writes: "There are no nested writes on MongoDB: create the user first, then the post with its `authorId`." +- Variants: "A variant declares only its own fields. `id`, `title`, and `kind` come from `Post`." And "You reach a single variant through the base model: `db.orm.posts.variant("Article")` limits the query to one variant." No claim made about `db.orm.articles` either way. +- Typed results after a pipeline filter: answered by the pipeline-builder link ("for running it and what comes back"), since this page does not own that. + +## Facts looked up + +- Nested writes: no `connect` and no `create` callback anywhere in `/Users/will/Projects/prisma/web/.claude/worktrees/pr-conflicts-review-fb93f7/wip/prisma-src/packages/2-mongo-family/5-query-builders/orm/src/` (grep for `connect` and `nested` returns nothing). Supports the "no nested writes" sentence. +- Variant access: `/Users/will/Projects/prisma/web/.claude/worktrees/pr-conflicts-review-fb93f7/wip/prisma-src/packages/2-mongo-family/5-query-builders/orm/test/integration/polymorphism.test.ts` uses only `orm.tasks.variant('Bug')` style, never `orm.bugs`. No `variant` accessor generation in `mongo-orm.ts`. So the page keeps "through the base model" and claims nothing more. +- `set` on a list path: `field-accessor.ts:70` gives `set(value: MongoValue)` on `LeafExpression`, and `resolve-path.ts` (`FieldToLeaf`, around lines 98-106) maps a `many: true` contract field to a leaf with `codecId: 'mongo/array@1'`. So a list of embedded values resolves to a leaf and `u.addresses.set([...])` is valid. Whole-value `set`/`unset` on a single embedded path is `field-accessor.ts:92-112` (`ObjectExpression`). + +## Q list + +None. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/mongodb.round3.md b/docs/orm-docs-audit/c21-data-modeling/mongodb.round3.md new file mode 100644 index 00000000000..dee9d701e95 --- /dev/null +++ b/docs/orm-docs-audit/c21-data-modeling/mongodb.round3.md @@ -0,0 +1,71 @@ +## Sentences I could not restate after one reading + +**1.** "`ObjectId` replaces `String @db.ObjectId` and `@default(auto())`. Writing either of those attributes is an error whose `code` is `PSL_UNSUPPORTED_FIELD_ATTRIBUTE`." (line 24) + +What stopped me: "PSL" is never spelled out. And the second sentence is internal plumbing — I don't type error codes, I read them. Plainest version I'd have understood: "Write `ObjectId`. The old `String @db.ObjectId` and `@default(auto())` are no longer allowed and will fail validation." + +**2.** "`db.orm.users` is the accessor for the collection, and it follows the collection name, so `@@map` renames the accessor as well. In Prisma ORM 7 it did not." (line 30) + +Three ideas stacked: what the accessor is, that it tracks `@@map`, and a version difference. "Accessor" is a word I had to guess at. Plainer: "`db.orm.users` is how you query the `users` collection. The name after `db.orm.` matches the collection name, so if you set `@@map("users")` you write `db.orm.users`, not `db.orm.user`." + +**3.** "`db` is the client you create once in `src/prisma/db.ts`, and `npx prisma orm init` writes that file." (line 30) + +This contradicts itself in one sentence — do I write it or does `init` write it? I still don't know whether I need to author anything there. + +**4.** "In an `update`, `u` gives you each field of the document to change." (line 81) + +I cannot restate this. "gives you each field" means nothing concrete. Then the example shows two completely different shapes — `u("address.city").set(...)` and `u.tags.push(...)` — and the page never says why one is a function call with a string and the other is a property. Plainer: "`u` is a builder. Use `u.fieldName` for a top-level field, and `u("path.to.field")` when the field is inside an embedded document." + +**5.** "`const rows = await (await db.runtime()).query(berliners);`" plus "use the pipeline builder, a chained builder for MongoDB aggregation pipelines" (lines 88–92) + +The double `await` and `db.runtime()` appear once, are never explained, and appear nowhere else on the page. What is `db.runtime()`? Why does the normal `db.orm.*` path not work here? What type is `rows` — my `User` model, or raw MongoDB documents? I'd have to guess raw. + +**6.** "Models go in your contract, the `contract.prisma` file that replaced `schema.prisma`. Run `npx prisma contract emit` after every change to it, then `npx prisma migration plan --name ` writes a migration and `npx prisma db migrate` applies it, creating the collections and indexes." (line 170) + +One sentence, three unfamiliar commands, and "emit" is never defined — emit *what*, to where? This is the single most important operational sentence on the page and it's buried in a Next steps bullet. + +## Words and phrases I had to guess + +- **"the data contract"** / **"contract"** — guessed: the new name for the schema file. The link at line 13 is dropped before the page has told me what a contract is; I only learn it's `contract.prisma` in the final bullet, 157 lines later. +- **"accessor"** — guessed: the property on `db.orm` you call query methods on. +- **"PSL"** — guessed: Prisma Schema Language. +- **"pipeline builder"** — guessed: a fluent API that constructs a MongoDB aggregation pipeline. +- **"discriminator"** — the page does define it, adequately. +- **"models a one-to-one"** (line 74) — one-to-one *what*? Guessed: relation. +- **"variant"** — guessed from context; the page does eventually make it clear. + +## "So what do I actually type?" — unanswered + +1. **`_id` vs `id` in TypeScript.** The model declares `id ObjectId @id @map("_id")`. Then every code sample uses `user._id` (lines 33, 84, 85, 120). So which is the JavaScript property — `id` or `_id`? The page never says, and the mismatch with the model is exactly the thing I'd get wrong. This is the biggest gap on the page. + +2. **The connection.** "Your `prisma.config.ts` file holds the connection, using `defineConfig` from `@prisma/orm-mongo/config`" — there is no example. Not one line of what that file looks like, where the connection string goes, or what env var it reads. + +3. **Adding to a list of embedded documents.** Line 74 introduces `addresses Address[]`. Line 81 says `.push(...)`/`.pull(...)` work on "a list of scalars." So how do I append an `Address` to `addresses`? Never shown. + +4. **Replacing or clearing a whole embedded object.** I'm shown `u("address.city").set("Hamburg")` for one field. How do I swap the entire `address`, or set it to null? + +5. **Creating a post and its author together.** "To link a new post to a user, set the reference field yourself" tells me nested writes are gone, but doesn't say whether there's any replacement for Prisma 7's nested `connect`/`create`. I'd be left wondering if I now have to do two round trips by hand. + +6. **Variant models.** Do `Article` and `Tutorial` declare `id`/`title` themselves? The example omits them and says "A variant has every field of its base," so I guess no — but I'd want that stated as a rule, not inferred. Also: is there a `db.orm.articles`, or is `db.orm.posts.variant("Article")` the only way in? + +7. **Filtering on an embedded field, then using the result.** After the pipeline example I have `rows`, and no idea if I can then `.update()` them, or whether I'm now outside the typed API entirely. + +## Implementation detail where I wanted instructions + +- The error code `PSL_UNSUPPORTED_FIELD_ATTRIBUTE` (line 24). Tell me not to write it; don't tell me the internal code name. +- "In Prisma ORM 7 it did not" (line 30). A one-clause aside about old behavior dropped mid-explanation of new behavior. If the migration difference matters, say what to change; if not, cut it. +- `db.runtime()` (line 92) exposes a layer of the client I otherwise never touch and never needed explained. + +## Could I do what the page is for after one reading? + +Partly. I could confidently write the Prisma models — documents, `type` blocks, references, and the discriminator syntax are all clear, and the embed-vs-reference table is genuinely the best part of the page. I'd make the right modeling decision. + +I could not write working code. Specifically I would still not know: + +- whether to type `user.id` or `user._id` +- what `prisma.config.ts` needs to contain to connect at all +- how to add an item to an embedded array of objects +- what `npx prisma contract emit` does or why it must run before the migration commands +- how to get typed results back when I need to filter on an embedded field + +The page reads as if the model syntax is the hard part. For someone coming from Prisma 7, the hard part is that the entire client API changed — no `data:` wrapper, a builder callback for updates, a separate pipeline API, a `db.runtime()` escape hatch — and those changes are each mentioned once, in passing, without enough of an example to copy. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/page-conventions.md b/docs/orm-docs-audit/c21-data-modeling/page-conventions.md index 1adb7341579..df14a4dce1c 100644 --- a/docs/orm-docs-audit/c21-data-modeling/page-conventions.md +++ b/docs/orm-docs-audit/c21-data-modeling/page-conventions.md @@ -8,7 +8,7 @@ - Relation fields: "a relation field, typed as the other model" and "the field that stores the other record's primary key" (the foreign key column on SQL, the field holding the `_id` on MongoDB). "Back-relation" and "virtual" only with the plain meaning beside them ("stores nothing in the database"). - `prisma contract emit` is the command that checks the contract. Say "`npx prisma contract emit` rejects it with an error whose `code` is `X`". Never "emit rejects", "the schema compiler", "the emitter lowers", "lowered". Referential actions: "become the foreign key's `ON DELETE` and `ON UPDATE` clauses". - Banned on these pages in addition to the list: gate / gated; capability in prose; codec; canonical; deterministic; pure; "source of truth"; "escape hatch"; "purpose-built"; "first-class"; "wiring" / "wired"; "hangs off"; "consult"; "authoritative"; "physical" (write "in the database"); "idiomatic" (write "the usual choice" or "what MongoDB expects"); "denormalize" only with its meaning beside it ("keep a small copy of the fields a read needs"); "hot paths" (write "reads that run often"); "opaque"; "polymorphism" as a bare noun (write "a base model with variants"); "lifecycle"; "injected" (write "filled in for you"); "narrow" as a verb for `.variant()` only with "limit the query to one variant" beside it; "at the storage level" (write "in the table"); "rides along"; "lives on" / "lives inside" (write "is on", "is stored inside"). -- Types: the scalar table names Prisma types (`String`, `Int`, ...) and says what PostgreSQL column each becomes only where it matters (`Decimal`, `Bytes`, `Json`, `Jsonb`). Type ids such as `pg/text@1` do not belong on these pages. Version references to a pre-release ("before 0.17") do not belong on these pages; say what the type is now. +- Types: the scalar table names Prisma types (`String`, `Int`, ...) and says what PostgreSQL column each becomes only where it matters (`Decimal`, `Bytes`, `Json`, `Jsonb`). Type ids such as `pg/text@1` do not belong in prose on these pages. Inside a code block where the syntax requires one (an `enum` needs `@@type`), show it and explain it in one clause ("stored as PostgreSQL `text`"), nothing more. Version references to a pre-release ("before 0.17") do not belong on these pages; say what the type is now. - Query examples on these pages are short and use the page's own models. The ORM client is `db.orm.public.User` on PostgreSQL and `db.orm.users` on MongoDB (see conventions.md). Link to `/orm/fundamentals/relations-and-joins` for querying; do not restate it. - "Prompt your coding agent" sections: keep, but the first sentence reads "Projects created with `npm create prisma@latest` include the Prisma ORM skills for your coding agent. In an existing project, run `npx prisma skills sync`." Keep the example prompts. - The source of truth is prisma/orm at 8.0.0-rc.10 (`cfccb09be2`), exported at `wip/prisma-src/`. rc.11 changed nothing in the contract language, so rc.10 facts hold for rc.11. diff --git a/docs/orm-docs-audit/c21-data-modeling/relational-databases.fix3.md b/docs/orm-docs-audit/c21-data-modeling/relational-databases.fix3.md new file mode 100644 index 00000000000..0e427b18a7b --- /dev/null +++ b/docs/orm-docs-audit/c21-data-modeling/relational-databases.fix3.md @@ -0,0 +1,48 @@ +Round three edits are done. 215 lines before, 214 after; plain-language check clean, no em-dashes, no clause-joining semicolons. + +## What I did with each mark + +**"which side that is is" (line 25).** Now: "The side that declares the `fields` argument owns the foreign key. Deciding which side declares `fields` is the main choice in each relation kind below." + +**Migration paragraph crammed into "How relations are declared" (line 27).** The section keeps only the contract sentence and the emit sentence: "...It only updates your code, not the database." The migration commands moved to a Next steps bullet using ``, not `add-posts`. The reader's "what is a contract, really" question stays unanswered by design (contract-authoring owns that term, per page-conventions). + +**`PSL_RELATION_NULLABILITY_MISMATCH` and the PSL acronym sentence.** Cut. The rule remains: "The two fields have to agree. Making one optional and the other required is rejected." + +**Mirror field vs back-relation (line 54).** Added one sentence: "Prisma ORM 7 called it the back-relation, and its error codes still use that word." + +**"read it from the foreign key side only" with no query.** Added inline: `db.orm.public.Post.where({ authorId: user.id }).all()`, followed by the one-time `.all()` definition from conventions.md. + +**`namespace` clause (line 56).** Cut. + +**"Three rules" for the one-to-one mirror field (line 91).** Collapsed to: add `profile Profile?` on `User`, it has to be optional, and "The `@unique` already on `Profile.userId` is what allows the mirror field." `PSL_NON_UNIQUE_BACKRELATION` cut. + +**One-to-one has no TypeScript.** Added `const users = await db.orm.public.User.include("profile").all();` + +**"name the relation on both sides" (line 134).** Now names the two fields: the list field `Post.tags` and `PostTag.post`. Also cut "and Prisma ORM works out the join through `PostTag`". + +**"If you leave it out" (line 148).** Now "If you skip that model". `PSL_ORPHANED_BACKRELATION` kept. + +**Three quoting conventions in the polymorphic block.** One clause added: "`@@discriminator(type)` and the first argument of `@@base` take a field or model name without quotes, while the variant value is a string." + +**Type-narrowing a polymorphic read.** Added: "In TypeScript those rows are a union with one member per variant, and each member's discriminator field is that variant's value, so switch on the discriminator field to reach a variant's own fields." + +**Self-relations.** One sentence in one-to-many: "A model can also point at itself: a category with a parent category is an ordinary one-to-many whose relation field is typed as the same model." + +**`NoAction` duplicates writing nothing.** Bullet now: "`NoAction` writes the clause out, which does the same thing as writing no clause at all." + +**Referential actions filed under Next steps.** Moved into "How relations are declared", after the optional-relation paragraph, with a new opening sentence saying what they decide. The five-item list is unchanged apart from `NoAction`. Dropped from Next steps. + +## Facts checked in the source + +- Polymorphic rows are a discriminated union: `VariantRow` in `wip/prisma-src/packages/3-extensions/sql-orm-client/src/types.ts:526-546` maps each variant to `Omit & DefaultModelRow & Record` and unions them, so each member pins the discriminator to that variant's literal value. Matches `wip/prisma-src/test/integration/test/sql-orm-client/polymorphism.test.ts:74-128`. +- Self-relations: `wip/prisma-src/test/integration/test/sql-orm-client/self-relations.test.ts:12-40` shows `invitedById` as an ordinary foreign key on the same model with an `invitedUsers` list. +- `.include("profile")` from the mirror-field side: `wip/prisma-src/test/integration/test/sql-orm-client/include.test.ts:105-147` includes `profile` from `users` and returns the profile row or `null`. + +## Cuts that paid for the additions + +Merged the overview link into the opening paragraph; inlined the `.include("author")` call instead of a fenced block; dropped the closing "Placing the key on the dependent side..." paragraph, which restated the three bullets above it. + +## Q list + +- **Q1.** The reader asked whether polymorphic models need anything extra at migration time (two tables, one discriminator column, nullable extra columns). The CLI is not in the `wip/prisma-src/` export, and I found no test or doc stating what `npx prisma migration plan` does for a base model with variants. Nothing added. +- **Q2.** The reader asked whether `--name` is required on `npx prisma migration plan` and whether the `migration ` vs `db ` split follows a rule. No CLI argument definitions in the export. The page shows `--name ` as the brief specifies and says nothing about the split. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/relational-databases.round3.md b/docs/orm-docs-audit/c21-data-modeling/relational-databases.round3.md new file mode 100644 index 00000000000..961098ec891 --- /dev/null +++ b/docs/orm-docs-audit/c21-data-modeling/relational-databases.round3.md @@ -0,0 +1,72 @@ +Read once, top to bottom, as a Prisma 7 user who has never seen v8. + +## Sentences I could not restate after one reading + +**Line 25** — "The side that declares the `fields` argument owns the foreign key. Deciding which side that is is the main choice in each of the relation kinds below." +"is is" made me re-read to check it wasn't a typo for something else. Plainer: "Deciding which side declares `fields` is the main choice in each relation kind below." + +**Line 27** — the whole paragraph. "Your contract is the `contract.prisma` file that replaced `schema.prisma`. After every change to it, run `npx prisma contract emit`. That command checks the contract and regenerates your types. It only updates your code. To change the database, run `npx prisma migration plan --name add-posts`, which writes a migration file, and then `npx prisma db migrate`, which applies that file." +Four things at once: a renamed file, a new noun ("contract"), a codegen command, and a two-step migration flow — dropped mid-way through a section about how to declare a relation. I do not know what "contract" *means* beyond "the new name of schema.prisma," and the page never says. If that's all it means, say so; if it means more, say what. I would split this into its own short section or a callout, not bury it between two relation explanations. + +**Line 29** — "Making one optional and the other required is rejected with an error whose `code` is `PSL_RELATION_NULLABILITY_MISMATCH`. `PSL` is the Prisma Schema Language, the language `contract.prisma` is written in." +The error code is internals. I wanted to know the rule: both fields optional or both required. The code tells me nothing I can act on, and then the page has to stop and define an acronym it only introduced in order to print the code. Cut the code, keep the rule. + +**Line 54** — "`posts Post[]` on `User` is the mirror field, the field on the other side of the relation, which stores nothing in the database. Declaring it is optional. Prisma ORM 7 required this field; Prisma ORM 8 does not. Leave it out and the relation still works, and you read it from the foreign key side only." +The v7/v8 contrast is the most useful sentence on the page. But "you read it from the foreign key side only" is the part I couldn't turn into an action — see the "what do I type" list below. + +**Line 56** — "`db` is the client you create once in `src/prisma/db.ts`, the file `npx prisma orm init` writes. `db.orm` holds your models by model name, and `public` is the PostgreSQL schema your tables live in, unless your contract puts the model in another schema with a `namespace` block." +Four new things (`db.ts`, `orm init`, `db.orm`, `public`, `namespace`) in two sentences, as an aside before an example about connecting a post to a user. The `namespace` exception is a detail I cannot use because no `namespace` block is ever shown. I'd cut the exception clause entirely here. + +**Line 91** — "Three rules, in the order you apply them. First, add the field... Second, make it optional... Third, make the foreign key it mirrors `@unique`, or `npx prisma contract emit` reports an error..." +The third rule is the same `@unique` the code block sixteen lines above already has, and which line 75 already told me is what makes the relation one-to-one. Reading this, I genuinely could not tell whether the mirror field imposes a *new* `@unique` requirement or restates the existing one. Also "Three rules" is over-structure for what is really: add `profile Profile?`, that's it (the `@unique` is already there). + +**Line 134** — "If a second join table connects the same two models, name the relation on both sides." +"Both sides" of *what*? `Post.tags` and `Tag.posts`, or `PostTag.post` and `PostTag.tag`? Four fields are in play and the sentence picks none. Earlier at line 31 "both sides" meant the two `@relation` attributes; here it can't mean the same thing, because `Post.tags` has no `@relation` at all. + +**Line 148** — "An implicit many-to-many, with list fields on both sides and no join table, as in Prisma ORM 7, is not supported. Declare the model for the join table yourself. If you leave it out, `npx prisma contract emit` reports an error whose `code` is `PSL_ORPHANED_BACKRELATION`." +"If you leave it out" — leave out the join model, I assume, but "it" is three sentences from its antecedent and the nearest noun is "the model for the join table yourself." + +**Lines 210 and 215** — "Write neither and the foreign key gets neither clause, so the database decides what happens." then "`NoAction` leaves the decision to the database." +Those are the same outcome in different words. After one reading I don't know why I'd ever type `NoAction` instead of nothing. Either say what differs or say they're equivalent. + +## Words and phrases I had to guess + +- **"contract"** (line 27, and "unless your contract puts the model…" line 56) — guessed: the new name for the schema file, nothing more. Never actually defined. +- **"mirror field"** (line 54) — guessed: what v7 called the back-relation field. The page's own error codes say `BACKRELATION` (`PSL_AMBIGUOUS_BACKRELATION`, `PSL_NON_UNIQUE_BACKRELATION`, `PSL_ORPHANED_BACKRELATION`) while the prose says "mirror field." Two names for one thing in the same document, never connected. If I search the docs for the error I hit, I won't find "mirror field." +- **"the model for the join table"** (lines 109, 148) — guessed: what v7 docs called an explicit many-to-many / join model. The phrasing is clumsy enough that I wasn't sure whether the model *is* the join table or merely describes one. +- **"variant"** / **"base model"** / **"discriminator"** (lines 154–180) — guessed from context, and the guesses held up. The discriminator explanation is the clearest part of the page. +- **"dependent side"** (line 95) — immediately defined in the same sentence. Fine. +- **`.all()`** (line 68) — guessed: v8's `findMany`. Never stated. +- **`add-posts`** (line 27) — guessed: an arbitrary migration name I choose, not a literal. + +## Where I asked "so what do I actually type?" and got no answer + +1. **Reading a user's posts when I skipped the mirror field.** Line 54 says "you read it from the foreign key side only" and then never shows that query. I'd want `db.orm.public.Post.where({ authorId: user.id }).all()` or whatever it actually is. +2. **Anything at all for one-to-one.** The one-to-one section has zero TypeScript. How do I create a profile for an existing user? How do I read a user with their profile — `.include("profile")` from `User`, or `.include("user")` from `Profile`? The section says "the relation reads from either side" and shows neither. +3. **Naming a relation when two join tables connect the same models** (line 134) — which of the four fields gets the name, and what does the syntax look like when `Post.tags` has no `@relation` to hang it on. +4. **Getting at variant fields after a base query.** Line 188: "each row carries the base fields plus only its own variant's fields." So after `await db.orm.public.Task.all()`, what is the TypeScript type, and how do I narrow a row to a `Bug` to reach `severity`? Switch on `type`? Some helper? This is the first thing I'd hit in real code and it isn't there. +5. **Whether polymorphic models need anything extra at migration time.** Two tables, one discriminator column, nullable extra columns — does `npx prisma migration plan` just handle it? Silence. +6. **`@@discriminator(type)` takes a bare identifier, `@@base(Task, "bug")` takes a bare identifier *and* a string, `@@map("features")` takes a string.** Three quoting conventions in one code block, unexplained. I would get this wrong on the first try. +7. **Self-relations.** A category with a parent category, a user who manages users. Very common, in v7's docs, absent here. I'd have to guess whether it's just a one-to-many pointed at the same model. +8. **`npx prisma migration plan` vs `npx prisma db migrate`** — one is `migration `, the other `db `. Is there a rule to the CLI shape, or do I memorize? Also, is `--name` required? + +## Where it explains the internals when I only wanted to know what to do + +- **Every `PSL_*` error code** (lines 29, 31, 91, 148). Four of them. I only need the rule; I'll read the error when I hit it. These also import the "PSL" acronym and the "backrelation" vocabulary for no reader benefit. +- **Line 134** — "you name the model at the other end, and Prisma ORM works out the join through `PostTag`." The "Prisma ORM works out the join" half is mechanism. The instruction is: type `Tag[]`, not `PostTag[]`. +- **Line 193** — "That table holds its extra columns plus the base model's primary key, which links it back to the base table." Table layout I don't act on. The next clause, "Those columns can be `NOT NULL`, and reading a full variant costs a join," is the part that drives my decision, and the paragraph does use it that way, so only the first half is surplus. +- **Line 27** — "That command checks the contract and regenerates your types. It only updates your code." Fine, actually — it stops me from expecting a database change. Keep. + +## Could I do what the page is for after one reading? + +One-to-many, yes. One-to-one schema, yes; one-to-one *queries*, no, because there aren't any. Many-to-many, mostly — I could write the three models and the `t.connect` call, but I'd be guessing the moment a second join table appeared. Polymorphic, I could write the schema and a `.variant("Bug").create(...)`, and then get stuck the first time I read a mixed list back and needed the variant fields in TypeScript. + +What I'd still not know after one reading: +- What a "contract" is, beyond a renamed file. +- Whether "mirror field" and "backrelation" are the same thing (they clearly are, and the page never says). +- How to query a relation from the side without the mirror field. +- How to type-narrow a polymorphic read. +- How to model a self-relation. +- Why `NoAction` exists when omitting the clause does the same thing. + +One more structural complaint: referential actions — `onDelete: Cascade`, the thing I reach for in nearly every real schema — are a single bullet in "Next steps," below the AI-prompt section. That's a main topic filed under links. \ No newline at end of file From b1b051807b377ea7d358cd14a5b3cd5edb722cc6 Mon Sep 17 00:00:00 2001 From: willbot Date: Mon, 14 Sep 2026 17:27:14 +0200 Subject: [PATCH 06/15] docs(orm): data-modeling reader review round four Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- .../content/docs/orm/data-modeling/index.mdx | 73 +++++++++--------- .../docs/orm/data-modeling/mongodb.mdx | 28 +++---- .../data-modeling/relational-databases.mdx | 56 +++++++------- .../c21-data-modeling/index.fix4.md | 45 +++++++++++ .../c21-data-modeling/index.round4.md | 70 +++++++++++++++++ .../c21-data-modeling/mongodb.fix4.md | 49 ++++++++++++ .../c21-data-modeling/mongodb.round4.md | 76 +++++++++++++++++++ .../relational-databases.fix4.md | 48 ++++++++++++ .../relational-databases.round4.md | 76 +++++++++++++++++++ 9 files changed, 440 insertions(+), 81 deletions(-) create mode 100644 docs/orm-docs-audit/c21-data-modeling/index.fix4.md create mode 100644 docs/orm-docs-audit/c21-data-modeling/index.round4.md create mode 100644 docs/orm-docs-audit/c21-data-modeling/mongodb.fix4.md create mode 100644 docs/orm-docs-audit/c21-data-modeling/mongodb.round4.md create mode 100644 docs/orm-docs-audit/c21-data-modeling/relational-databases.fix4.md create mode 100644 docs/orm-docs-audit/c21-data-modeling/relational-databases.round4.md diff --git a/apps/docs/content/docs/orm/data-modeling/index.mdx b/apps/docs/content/docs/orm/data-modeling/index.mdx index 550338f1b88..dad45776560 100644 --- a/apps/docs/content/docs/orm/data-modeling/index.mdx +++ b/apps/docs/content/docs/orm/data-modeling/index.mdx @@ -10,16 +10,18 @@ Data modeling is the process of describing the data your application needs and h For example, a blog has users, posts, and comments. A user has fields like an email and a name. A post has fields like a title and content. These models also relate to each other: a user can write many posts, and a post can have many comments. -In Prisma ORM, you define this structure in your contract, the `contract.prisma` file that replaced `schema.prisma`. It holds your models, as `schema.prisma` did. Prisma ORM derives your TypeScript types and your migrations from it. There is no `datasource` or `generator` block any more, and the connection string is in `prisma.config.ts` instead. [A complete contract](/orm/contract-authoring/psl-syntax#a-complete-contract) shows a whole file next to its config. If you are coming from Prisma ORM 7, [what changed](/orm/coming-from-prisma-orm-7) covers the rename and the rest of the differences. +In Prisma ORM, you define this structure in your contract, the `contract.prisma` file that replaced `schema.prisma`. It holds your models, as `schema.prisma` did. Prisma ORM derives your TypeScript types and your migrations from it. -A contract is built from four building blocks: +There is no `datasource` or `generator` block any more, and the connection string is in `prisma.config.ts` instead. [A complete contract](/orm/contract-authoring/psl-syntax#a-complete-contract) shows a whole file, and that page shows the config file too. If you are [coming from Prisma ORM 6 or 7](/orm/coming-from-prisma-orm-7), that page covers the rename and the rest of the differences. + +Prisma ORM 8 supports PostgreSQL and MongoDB only. A contract is built from four building blocks: - [Models](#models): the things your application works with - [Primary keys](#primary-keys): how each record is identified - [Scalar fields](#scalar-fields): the values a model stores - [Relations](#relations): how models connect to each other -Prisma ORM 8 supports PostgreSQL and MongoDB only. All four blocks apply to both. How you model relations is where the two differ, so continue with [relational data modeling](/orm/data-modeling/relational-databases) or [MongoDB data modeling](/orm/data-modeling/mongodb). +All four blocks apply to both databases. How you model relations is where the two differ, so continue with [relational data modeling](/orm/data-modeling/relational-databases) or [MongoDB data modeling](/orm/data-modeling/mongodb). ## Models @@ -37,7 +39,7 @@ On a relational database, a model becomes a table. On MongoDB, it becomes a coll ## Primary keys -A primary key is the field that uniquely identifies each record. Give every model one. Prisma ORM accepts a model without one, but `update` and `delete` on that model fail at runtime, because they need a primary key or a unique field. Mark the primary key with `@id`: +A primary key is the field that uniquely identifies each record. Give every model one and mark it with `@id`, because `update` and `delete` fail at runtime on a model that has neither a primary key nor a unique field: ```prisma tab="PostgreSQL" model User { @@ -54,13 +56,13 @@ model User { } ``` -On MongoDB, the primary key is the document's mandatory `_id` field, and it must be an `ObjectId`. `@map("_id")` gives the field that name in the database. Omit `id` when you create a document. MongoDB assigns it. `@@map("users")` names the collection and is optional: without it, the collection is the model name with a lowercase first letter. +On MongoDB, the primary key is the document's mandatory `_id` field, and it must be an `ObjectId`. `@map("_id")` gives the field that name in the database. The field takes no `@default`. Omit `id` when you create a document and MongoDB assigns it. `@@map("users")` names the collection and is optional: without it, the collection is the model name with a lowercase first letter. ### Natural keys A natural key is a value that already identifies the record in the real world. Use one when the value is stable, unique, and assigned outside your application: a standardized code you receive rather than invent. -Natural keys fit reference tables best: tables that hold a fixed set of values other records point at. You can identify a country by its ISO code, and the code never changes: +Some tables hold a fixed set of values that other records point at. Natural keys fit these reference tables best. You can identify a country by its ISO code, and the code never changes: ```prisma model Country { @@ -84,11 +86,6 @@ model User { id Int @id @default(autoincrement()) email String @unique } - -model Product { - id Int @id @default(autoincrement()) - sku String @unique -} ``` ### Which surrogate type to pick @@ -113,7 +110,7 @@ model ApiToken { } ``` -A UUID takes more space than an integer, but independent services never produce the same one and the value tells nobody how many records you have. `@default(uuid())` and `@default(uuid(4))` both give you a random UUID. Write `@default(uuid(7))` instead for a UUID that starts with the time it was created, so new ids sort after older ones. On MongoDB the choice is made for you. The key is the `ObjectId` shown above. +A UUID takes more space than an integer, but independent services never produce the same one. Write `@default(uuid(7))` for ids that sort by creation time. `@default(uuid())` is random. On MongoDB the choice is made for you. The key is the `ObjectId` shown above. ### Composite keys @@ -134,27 +131,25 @@ A duplicate `(userId, tagId)` pair would be meaningless, so the pair is the key. A scalar field holds a single value. The common types: -| Type | Stores | -| --------------------------- | ------------------------------------------------ | -| `String` | Text | -| `Int` | 32-bit integer | -| `BigInt` | 64-bit integer | -| `Float` | Floating-point number | -| `Decimal`, `Numeric(10, 2)` | Exact decimal, PostgreSQL `numeric` | -| `Boolean` | `true` / `false` | -| `DateTime` | Timestamp, PostgreSQL `timestamptz` | -| `Bytes` | Binary data, PostgreSQL `bytea` | -| `Json` | Arbitrary JSON value, PostgreSQL's native `json` | -| `Jsonb` | Arbitrary JSON value, PostgreSQL's `jsonb` | -| `ObjectId` | MongoDB document identifier | - -Where a row names a PostgreSQL type such as `numeric`, that is the column type on PostgreSQL. `ObjectId` is the only MongoDB-only type. +| Type | Stores | +| --------------------------------------------------------- | ------------------------------------------------ | +| `String` | Text | +| `Int` | 32-bit integer | +| `BigInt` | 64-bit integer | +| `Float` | Floating-point number | +| `Decimal`, or `Numeric(10, 2)` with a precision and scale | Exact decimal, PostgreSQL `numeric` | +| `Boolean` | `true` / `false` | +| `DateTime` | Timestamp, PostgreSQL `timestamptz` | +| `Bytes` | Binary data, PostgreSQL `bytea` | +| `Json` | Arbitrary JSON value, PostgreSQL's native `json` | +| `Jsonb` | Arbitrary JSON value, PostgreSQL's `jsonb` | +| `ObjectId` | MongoDB document identifier | -A `Decimal` value reaches your code as a string, so nothing is rounded through a JavaScript number. Write `price Decimal` when any precision will do. Write `price Numeric(10, 2)` when you want an explicit precision and scale. +The PostgreSQL names in the table are the column types Prisma ORM creates. `ObjectId` is the only MongoDB-only type. -A `DateTime` value reaches your code as a `Temporal.Instant`, the standard JavaScript type for a point in time. `Temporal` is built into Node.js 26.8.2 and later. On older versions, install the `temporal-polyfill` package and add `import "temporal-polyfill/full/global"`. A `Bytes` value reaches your code as a `Uint8Array`. +A `Decimal` value reaches your code as a string, so nothing is rounded through a JavaScript number. A `DateTime` value reaches your code as a `Temporal.Instant`, the standard JavaScript type for a point in time. `Temporal` is built into Node.js 26.8.2 and later. On older versions, install the `temporal-polyfill` package and add `import "temporal-polyfill/full/global"` in your app's entry file, before any query runs. A `Bytes` value reaches your code as a `Uint8Array`. -`Json` and `Jsonb` are two separate types. Filtering on a `Jsonb` field is whole-value equality only: you can ask whether the column equals a given document, not whether a key inside it matches. A `Json` field offers no comparison at all in the ORM filter, so use `Jsonb` unless you specifically need PostgreSQL's plain `json` column. Reach into the document with [raw SQL](/orm/reference/raw-queries) when you need a path query. [Extensions](/orm/extensions) add more types, such as vectors or geometry. +`Json` and `Jsonb` are two separate types. On a `Jsonb` field, `.where()` compares the whole value. You can ask whether the column equals a given document, but not whether a key inside it matches. A `Json` field cannot be compared in `.where()` at all, so use `Jsonb` unless you specifically need PostgreSQL's plain `json` column. Reach into the document with [raw SQL](/orm/reference/raw-queries) when you need a path query. [Extensions](/orm/extensions) add more types, such as vectors or geometry. A trailing `?` makes a field optional: `name String?`. A trailing `[]` makes it a list: `tags String[]`. @@ -168,7 +163,7 @@ enum Priority { } ``` -`@@type("pg/text@1")` says how the values are stored, here as PostgreSQL `text`. [Enums](/orm/contract-authoring/psl-syntax#enums) has the rest of the syntax. +`@@type` is required on every enum and says how the values are stored, here as PostgreSQL `text`. `pg/text@1` is the PostgreSQL type plus a version, always `@1` today. On MongoDB, write `@@type("mongo/string@1")`. [Enums](/orm/contract-authoring/psl-syntax#enums) has the rest of the syntax. ### How to pick a data type @@ -201,18 +196,18 @@ model Post { } ``` -When you are unsure between two sizes, lean toward the wider one. For a counter that could grow past two billion, that means `BigInt` over `Int`. Widening a type later is a migration, so picking the wider type now saves you one. `BigInt` does cost you something in your code: the value reaches you as a JavaScript `bigint`, which `JSON.stringify` refuses to serialize. Call `.toString()` on it first. +When you are unsure between two sizes, lean toward the wider one. For a counter that could grow past two billion, that means `BigInt` over `Int`. Changing `Int` to `BigInt` later needs a migration. Pick `BigInt` now and you skip it. `BigInt` does cost you something in your code: the value reaches you as a JavaScript `bigint`, which `JSON.stringify` refuses to serialize. Call `.toString()` on it first. -Mark a field optional (`?`) only when "absent" means something different from a sensible default. A required field with a default is often the clearer model. +Mark a field optional (`?`) only when "absent" means something different from a sensible default, such as `publishedAt DateTime?` where no value means unpublished. A required field with a default is often the clearer model. ## Relations A relation connects two models: a user has many posts, a post belongs to one user. -Two kinds of field describe a relation, the link field and the relation field. The model that holds the link needs both of them. The model on the other end needs only a relation field: +A relation uses two kinds of field. The model that holds the link needs both. The model on the other end needs only a relation field: -- The link field, the one that stores the other record's primary key, like `authorId`. On a relational database this is the foreign key column, and on MongoDB it is the field holding the other document's `_id`. -- A relation field, typed as the other model, like `author User`. You do not get a column for it. It tells Prisma ORM how to follow the connection in a query. +- The link field stores the other record's primary key, like `authorId`. Prisma ORM 7 called this the relation scalar field. On a relational database it is the foreign key column, and on MongoDB it is the field holding the other document's `_id`. +- The relation field is typed as the other model, like `author User`. You do not get a column for it. It tells Prisma ORM how to follow the connection in a query. The `@relation` attribute ties them together: `fields` names the link field, and `references` names the field it points at on the other model. On the other model, `posts Post[]` is the other end of the same relation. @@ -231,9 +226,9 @@ model Post { Relations come in three kinds: -- One-to-one: a user has at most one profile. The model that holds the link gets `@unique` on its link field, and the field on `User` is `profile Profile?`, not a list. +- One-to-one: a user has at most one profile. The model that holds the link gets `@unique` on its link field, and the field on `User` is `profile Profile?`, not a list. [One-to-one](/orm/data-modeling/relational-databases#one-to-one) has both models written out. - One-to-many: a user has many posts, as in the example above. -- Many-to-many: a post has many tags, and a tag appears on many posts. This needs a model for the join table, with one record per connected pair. List fields on both sides with no such model are rejected, so write the model out as [many-to-many](/orm/data-modeling/relational-databases#many-to-many) shows. +- Many-to-many: a post has many tags, and a tag appears on many posts. This needs a model for the join table, with one record per connected pair. Prisma ORM 7 let you write a list field on both sides with no such model. That is gone, and `npx prisma contract emit` rejects it. Write the model out as [many-to-many](/orm/data-modeling/relational-databases#many-to-many) shows. How each kind is stored, and which model should hold the link field, is where relational and document databases differ. Continue with the guide for your database: @@ -253,5 +248,5 @@ Projects created with `npm create prisma@latest` include the [Prisma ORM skills] - [Model relational data](/orm/data-modeling/relational-databases): one-to-one, one-to-many, many-to-many, and polymorphic relations on PostgreSQL. - [Model MongoDB data](/orm/data-modeling/mongodb): embed or reference, and polymorphic collections. -- [Generate a migration](/orm/migrations/generating-a-migration) to get the models you wrote into the database. `npx prisma contract emit` checks the contract and regenerates your types. `npx prisma migration plan --name ` writes a migration file. `npx prisma db migrate` applies it. +- [Generate a migration](/orm/migrations/generating-a-migration) to get the models you wrote into the database. Edit the contract, then run `npx prisma contract emit` to check it and regenerate your types, then `npx prisma migration plan --name ` to write a migration file, then `npx prisma db migrate` to apply it. `npx prisma contract emit` replaces `prisma generate`. - [Query your models](/orm/fundamentals/reading-data) once your contract is in place. diff --git a/apps/docs/content/docs/orm/data-modeling/mongodb.mdx b/apps/docs/content/docs/orm/data-modeling/mongodb.mdx index 069408f3e8f..83044efad05 100644 --- a/apps/docs/content/docs/orm/data-modeling/mongodb.mdx +++ b/apps/docs/content/docs/orm/data-modeling/mongodb.mdx @@ -10,30 +10,30 @@ MongoDB stores data as documents grouped into collections. A document can nest o ## Documents, collections, and the _id key -Your models go in your contract, the `contract.prisma` file that replaced `schema.prisma`. There is no `datasource` block on MongoDB. The connection string is in your `prisma.config.ts` file instead, passed to `defineConfig` from `@prisma/orm-mongo/config`. See [the data contract](/orm/contract-authoring/the-data-contract) for that file. A model maps to a collection, and each record is a document. Every document has an `_id` field as its primary key. `ObjectId` is the type MongoDB expects for it: +Your models go in your contract, the `contract.prisma` file that replaced `schema.prisma`. A model maps to a collection, and each record is a document. Every document has an `_id` field as its primary key. Your connection string goes in `prisma.config.ts`, passed to `defineConfig` from `@prisma/orm-mongo/config`. See [the data contract](/orm/contract-authoring/the-data-contract). That page shows the config file. There is no `datasource` block on MongoDB. ```prisma model User { id ObjectId @id @map("_id") name String - email String + email String @unique @@map("users") } ``` -In your TypeScript code the field is called `_id`, not `id`. `@map("_id")` maps the model's `id` field to MongoDB's mandatory `_id` field and sets the name on both sides, so you write `user._id` and `where({ _id })`. `@@map("users")` names the collection. `@@map` is optional. Without it the collection takes the model name with a lowercase first letter, so `user`. Write `ObjectId` for the key type. The old `String @db.ObjectId` and `@default(auto())` are no longer allowed and fail at `npx prisma contract emit`, the command that checks your contract and regenerates your types. Leave the id out of the values you pass to `create`, and MongoDB assigns it: +In the contract you call the field `id`. In TypeScript it is always `_id`, never `id`. `@map("_id")` sets the name in the database and in your code, so you write `user._id` and `where({ _id })`. `@@map("users")` names the collection. `@@map` is optional. Without it the collection takes the model name with a lowercase first letter, so `user`. `@unique` on `email` becomes a unique index when you migrate. -```ts -const user = await db.orm.users.create({ name: "Ada", email: "ada@example.com" }); -``` +Write `ObjectId` for the key type. `ObjectId` is the contract type. In TypeScript ids are plain strings, so there is no `ObjectId` to construct. The old `String @db.ObjectId` and `@default(auto())` are no longer allowed. -`npx prisma orm init` writes `src/prisma/db.ts`, which creates the client `db`. `db.orm.users` is how you query the `users` collection. The name after `db.orm.` is the collection name, so with `@@map("users")` you write `db.orm.users`, not `db.orm.user`. The values go straight into `create`, without the `data:` wrapper Prisma ORM 7 used. The id comes back as a string, and you pass it back as a string. There is no `ObjectId` to construct: +Leave the id out of the values you pass to `create`, and MongoDB assigns it. `db` is the client created in `src/prisma/db.ts`. `db.orm` is the ORM client, one of the query APIs on `db`. See [the ORM reference](/orm/reference). ```ts +import { db } from "./prisma/db"; +const user = await db.orm.users.create({ name: "Ada", email: "ada@example.com" }); const found = await db.orm.users.where({ _id: user._id }).first(); ``` -You `await` the whole chain, and the last call says what you want back: `.first()` for one document or `null`, `.all()` for every matching document. +The name after `db.orm.` is the collection name, so with `@@map("users")` you write `db.orm.users`, not `db.orm.user`. The values go straight into `create`, without the `data:` wrapper Prisma ORM 7 used. You `await` the whole chain, and the last call says what you want back: `.first()` for one document or `null`, `.all()` for every matching document. ## Embed or reference @@ -78,7 +78,7 @@ const address = { street: "1 Main St", city: "Berlin", country: "DE" }; await db.orm.users.create({ name: "Ada", address }); ``` -In an `update`, `u` builds the changes you want. Use `u.fieldName` to change a top-level field, and `u("path.to.field")` to change a field inside an embedded document. Use `.push(...)` and `.pull(...)` to add to and remove from a list of scalars: +In an `update`, `u` builds the changes you want. `update` takes a callback that returns an array, so several changes go in one call. Use `u.fieldName` to change a top-level field. Use `u("path.to.field")` for a field inside an embedded document, written as a string because a dotted path is not a property name. Use `.push(...)` and `.pull(...)` to add to and remove from a list of scalars: ```ts await db.orm.users.where({ _id: user._id }).update((u) => [u("address.city").set("Hamburg")]); @@ -88,10 +88,10 @@ await db.orm.users.where({ _id: user._id }).update((u) => [u.tags.push("admin"), Replace a whole embedded value with `u("address").set({ ... })`, and remove it with `u("address").unset()`. A list of embedded values is replaced as a whole with `u.addresses.set([ ... ])`. `.where()` filters on top-level model fields only. To match on a field inside an embedded document, use the pipeline builder, a chained builder for MongoDB aggregation pipelines: ```ts -const berliners = db.query.from("users").match((f) => f("address.city").eq("Berlin")).build(); +const query = db.query.from("users").match((f) => f("address.city").eq("Berlin")).build(); ``` -See [the pipeline builder](/orm/fundamentals/advanced-queries#mongodb-pipeline-builder) for running it and what comes back. Embedded arrays are only a good idea while they stay bounded. An array that grows forever slows every read of the parent and pushes the document toward MongoDB's 16 MB limit, and a write that exceeds it fails. When a list has no natural cap, reference it instead. +`build()` returns the query, not the rows. See [the pipeline builder](/orm/fundamentals/advanced-queries#mongodb-pipeline-builder) for how to run it and what comes back. Embedded arrays are only a good idea while they stay bounded. An array that grows forever slows every read of the parent and pushes the document toward MongoDB's 16 MB limit, and a write that exceeds it fails. When a list has no natural cap, reference it instead. ## References across collections @@ -113,14 +113,14 @@ model Post { } ``` -This is a one-to-many by reference: many posts point at one user, and each post is queryable on its own. There are no nested writes on MongoDB: create the user first, then the post with its `authorId`. To load a post together with its author, chain `.include("author")`, which adds the related record to every row you get back: +This is a one-to-many by reference: many posts point at one user, and each post is queryable on its own. There are no nested writes on MongoDB: create the user first, then the post with its `authorId`. In `references: [id]` you write the field's contract-side name, `id`, not `_id`. To load a post together with its author, chain `.include("author")`, which adds the related record to every row you get back: ```ts await db.orm.posts.create({ title: "Hello", authorId: user._id }); const posts = await db.orm.posts.include("author").all(); ``` -See [relations and joins](/orm/fundamentals/relations-and-joins) for the rest of the query API. Resolving a reference costs a second read. If a read runs often, you can copy the few fields it needs into the parent document, such as a comment storing its author's name. The copy then goes stale on its own, and keeping it up to date is your job. +`.include("posts")` on `db.orm.users` works the other way and gives each user an array of their posts. See [relations and joins](/orm/fundamentals/relations-and-joins) for the rest of the query API. Resolving a reference costs a second read. If a read runs often, you can copy the few fields it needs into the parent document, such as a comment storing its author's name. If the original changes, the copy does not. You update it yourself. ## Polymorphic collections @@ -166,7 +166,7 @@ Projects created with `npm create prisma@latest` include the [Prisma ORM skills] ## Next steps -- Run `npx prisma contract emit` after every change to your contract. Then `npx prisma migration plan --name ` writes a migration and `npx prisma db migrate` applies it, creating the collections and indexes. See [generating a migration](/orm/migrations/generating-a-migration). +- Start a new project with `npm create prisma@latest -- my-app`, or run `npx prisma orm init` in an existing one. After every change to your contract, run `npx prisma contract emit`, which checks your contract and regenerates your types. Then `npx prisma migration plan --name ` writes a migration, and `npx prisma db migrate` applies it, creating the collections and indexes. See [generating a migration](/orm/migrations/generating-a-migration). - [Query documents](/orm/fundamentals/reading-data) and [resolve references](/orm/fundamentals/relations-and-joins) with `.include(...)`. - Aggregate and reshape documents with the [pipeline builder](/orm/fundamentals/advanced-queries#mongodb-pipeline-builder). - To model relational data, see the [relational data modeling guide](/orm/data-modeling/relational-databases). diff --git a/apps/docs/content/docs/orm/data-modeling/relational-databases.mdx b/apps/docs/content/docs/orm/data-modeling/relational-databases.mdx index 5ac4d1ac029..e419df131cb 100644 --- a/apps/docs/content/docs/orm/data-modeling/relational-databases.mdx +++ b/apps/docs/content/docs/orm/data-modeling/relational-databases.mdx @@ -1,6 +1,6 @@ --- title: Relational data modeling -description: 'Model one-to-one, one-to-many, many-to-many, and polymorphic relations for PostgreSQL and other SQL databases.' +description: 'Model one-to-one, one-to-many, many-to-many, and polymorphic relations for PostgreSQL.' url: /orm/data-modeling/relational-databases metaTitle: Relational data modeling in Prisma ORM metaDescription: 'How to model one-to-one, one-to-many, many-to-many, and polymorphic relations in Prisma ORM for PostgreSQL, including which side owns the foreign key.' @@ -8,6 +8,8 @@ metaDescription: 'How to model one-to-one, one-to-many, many-to-many, and polymo In a relational database, each model becomes a table and each scalar field becomes a column. Models connect through foreign keys: a column in one table that holds the primary key of a row in another table. If you are new to models and keys, start with the [data modeling overview](/orm/data-modeling). +You write your models in your contract, the `contract.prisma` file that replaced `schema.prisma`. After every change to it, run `npx prisma contract emit`, which checks the contract and regenerates your TypeScript types. It does not touch the database. `db` is the client you create once in `src/prisma/db.ts`, the file `npx prisma orm init` writes. `db.orm` holds your models by model name, and `public` is the PostgreSQL schema your tables are in. + ## How relations are declared The model whose table gets the foreign key column declares two fields. One is a scalar field for the foreign key column. The other is a relation field, typed as the other model. `@relation(fields:, references:)` ties them together: @@ -22,17 +24,15 @@ model Post { The side that declares the `fields` argument owns the foreign key. Deciding which side declares `fields` is the main choice in each relation kind below. -Your contract is the `contract.prisma` file that replaced `schema.prisma`. After every change to it, run `npx prisma contract emit`. That command checks the contract and regenerates your types. It only updates your code, not the database. - -To let a post have no author, make the foreign key field and the relation field both optional: `authorId Int?` and `author User?`. The two fields have to agree. Making one optional and the other required is rejected. +To let a post have no author, make the foreign key field and the relation field both optional: `authorId Int?` and `author User?`. The two fields have to agree. If one is optional and the other is required, `npx prisma contract emit` reports an error. -Referential actions decide what the database does to the rows on the foreign key side when the row they point at is deleted or its key changes. Add `onDelete` and `onUpdate` to `@relation`, as in `@relation(fields: [userId], references: [id], onDelete: Cascade)`. They become the foreign key's `ON DELETE` and `ON UPDATE` clauses. Write neither and the foreign key gets neither clause, so the database decides what happens. The five actions are: +When a `User` row is deleted or its `id` changes, referential actions decide what happens to the `Post` rows that point at it. Add `onDelete` and `onUpdate` to `@relation`, as in `@relation(fields: [userId], references: [id], onDelete: Cascade)`. They become the foreign key's `ON DELETE` and `ON UPDATE` clauses. Write neither and the foreign key gets neither clause, so the database decides what happens. The five actions are: - `Cascade` deletes or updates the matching rows. - `Restrict` blocks the change. - `SetNull` clears the foreign key. - `SetDefault` writes the foreign key's default value. -- `NoAction` writes the clause out, which does the same thing as writing no clause at all. +- `NoAction` is the same as writing nothing. Two relations between the same pair of models need a name on both sides, such as `@relation("Authored", fields: [authorId], references: [id])` on one side and `@relation("Authored")` on the other. Without the name, `npx prisma contract emit` reports an error. @@ -55,13 +55,15 @@ model Post { } ``` -The foreign key is on the "many" side, `Post.authorId`, because each post points at exactly one user, while a user points at an open-ended list of posts. Use a one-to-many whenever a record belongs to one parent and a parent has many children: comments on a post, line items on an order, employees in a department. A model can also point at itself: a category with a parent category is an ordinary one-to-many whose relation field is typed as the same model. +The foreign key is on the "many" side, `Post.authorId`, because each post points at exactly one user, while a user points at an open-ended list of posts. Use a one-to-many whenever a record belongs to one parent and a parent has many children: comments on a post, line items on an order, employees in a department. A model can also point at itself: a category with a parent category is an ordinary one-to-many whose relation field is typed as the same model. One relation between a model and itself needs no relation name. -`posts Post[]` on `User` is the mirror field, the field on the other side of the relation, which stores nothing in the database. Prisma ORM 7 called it the back-relation, and its error codes still use that word. Prisma ORM 7 required this field. Prisma ORM 8 does not. Leave it out and the relation still works, and you read it from the foreign key side only, with `db.orm.public.Post.where({ authorId: user.id }).all()`. `.all()` is the call that runs the query and returns every matching row. +`posts Post[]` on `User` is the mirror field, the field on the other side of the relation, which stores nothing in the database. You can leave it out. The relation still works, and you read it from the foreign key side with `db.orm.public.Post.where({ authorId: user.id }).all()`. `.all()` is the call that runs the query and returns every matching row. -To connect a post to a user, set the foreign key when you create the post. `db` is the client you create once in `src/prisma/db.ts`, the file `npx prisma orm init` writes. `db.orm` holds your models by model name, and `public` is the PostgreSQL schema your tables live in. Here `user` is a user record you read earlier: +To connect a post to a user, set the foreign key when you create the post. Here `user` is a user record you read earlier: ```typescript +import { db } from "./prisma/db"; + const post = await db.orm.public.Post.create({ title: "Hello", authorId: user.id, @@ -90,11 +92,7 @@ model Profile { } ``` -Declare the relation on the side that holds the foreign key, `Profile` here. You can also add the mirror field `profile Profile?` to `User`, with a type and nothing else, because it needs no `@relation` of its own. It has to be optional, because nothing in the database guarantees a matching profile row. The `@unique` already on `Profile.userId` is what allows the mirror field. With it in place, [the relation reads from either side](/orm/fundamentals/relations-and-joins#one-to-one): - -```typescript -const users = await db.orm.public.User.include("profile").all(); -``` +Declare the relation on the side that holds the foreign key, `Profile` here. You can also add the mirror field `profile Profile?` to `User`, with a type and nothing else, because it needs no `@relation` of its own. It has to be optional, because nothing in the database guarantees a matching profile row. Because `userId` is unique, the mirror field can be a single `Profile?` instead of a list. With it in place you can [read the relation from either side](/orm/fundamentals/relations-and-joins#one-to-one), as in `db.orm.public.User.include("profile").all()`. ### Which side owns the foreign key @@ -135,7 +133,7 @@ model PostTag { } ``` -The composite primary key on `PostTag`, `@@id([postId, tagId])`, allows one record per pair. Note that `Post.tags` is typed `Tag[]`, not `PostTag[]`: you name the model at the other end. The same goes for `Tag.posts`. If a second join table connects the same two models, put the relation name on the list field, `Post.tags`, and on `PostTag.post`, the field that points back at `Post`. +The composite primary key on `PostTag`, `@@id([postId, tagId])`, allows one record per pair. Note that `Post.tags` is typed `Tag[]`, not `PostTag[]`: you name the model at the other end. The same goes for `Tag.posts`. If a second join table connects the same two models, the relation name goes on both sides again, which here means the list field, `Post.tags`, and `PostTag.post`, the field that points back at `Post`. Connect a post to a tag with a nested write on the mirror field: @@ -145,17 +143,13 @@ await db.orm.public.Post.where({ id: post.id }).update({ }); ``` -`t` is the argument of the nested write callback: `t.create([...])` makes new related records and links them, `t.connect([...])` links existing ones, and `t.disconnect([...])` removes the link. You can also write the same row yourself with `db.orm.public.PostTag.create({ postId: post.id, tagId: tag.id })`. +`t` is the argument of the nested write callback: `t.create([...])` makes new related records and links them, `t.connect([...])` links existing ones, and `t.disconnect([...])` removes the link. `Post.tags` and `Tag.posts` are the mirror fields that `t.connect` goes through. Leave them out and you write the `PostTag` rows yourself, with `db.orm.public.PostTag.create({ postId: post.id, tagId: tag.id })`. `PostTag` is an ordinary model, so the join table can hold data about the pairing itself. To record when a tag was added, add `addedAt DateTime @default(now())` to `PostTag`. -`PostTag` is an ordinary model, so the join table can hold data about the pairing itself. To record when a tag was added, add `addedAt DateTime @default(now())` to `PostTag`. - -An implicit many-to-many, with list fields on both sides and no join table, as in Prisma ORM 7, is not supported. Declare the model for the join table yourself. If you skip that model, `npx prisma contract emit` reports an error whose `code` is `PSL_ORPHANED_BACKRELATION`. [Relations and joins](/orm/fundamentals/relations-and-joins#many-to-many) shows how to read across the join table in one query. +An implicit many-to-many, with list fields on both sides and no join table, as in Prisma ORM 7, is not supported. Declare `PostTag` yourself. If you skip it, `npx prisma contract emit` reports an error whose `code` is `PSL_ORPHANED_BACKRELATION`. [Relations and joins](/orm/fundamentals/relations-and-joins#many-to-many) shows how to read across the join table in one query. ## Polymorphic relations -Sometimes several kinds of record share a common core but each has its own extra fields: every `Task` has a title, but a `Bug` also has a severity and a `Feature` has a target release. - -Prisma ORM models this with a base model plus variant models. One field on the base model, the discriminator, records which variant each row is: +Sometimes several kinds of record share a common core but each has its own extra fields: every `Task` has a title, but a `Bug` also has a severity and a `Feature` has a target release. Prisma ORM models this with a base model plus variant models. One field on the base model, the discriminator, records which variant each row is: ```prisma model Task { @@ -183,18 +177,24 @@ model Feature { `@@discriminator(type)` marks the field that records the variant. `@@base(Task, "bug")` declares that `Bug` is the `"bug"` variant of `Task`. `@@discriminator(type)` and the first argument of `@@base` take a field or model name without quotes, while the variant value is a string. A variant has every field of its base plus the ones it declares, and it needs no `@id` of its own, because it uses the base model's primary key. -Read and write variants through the base model. `.variant(...)` limits the query to one variant, and it takes the variant's model name, `"Bug"`. Passing the discriminator value `"bug"` is a TypeScript error, because the only names it accepts are the model names of the base model's variants. On a create, the discriminator value is filled in for you: +Read and write variants through the base model. `.variant(...)` limits the query to one variant. Pass the model name, `"Bug"`, with a capital letter. `"bug"` is a type error. On a create, the discriminator value is filled in for you: ```typescript await db.orm.public.Task.variant("Bug").create({ title: "Crash on save", severity: "high" }); ``` -A read works the same way. A query on `Task` with no `.variant(...)` returns every variant, and each row carries the base fields plus only its own variant's fields, with the discriminator field holding that variant's value, such as `"bug"`. In TypeScript those rows are a union with one member per variant, and each member's discriminator field is that variant's value, so switch on the discriminator field to reach a variant's own fields. +A query on `Task` with no `.variant(...)` returns every variant. Each row carries the base fields plus only its own variant's fields, and the discriminator field holds that variant's value, such as `"bug"`. Switch on `task.type` to reach a variant's own fields: -`@@map` decides how a variant is stored: +```typescript +switch (task.type) { + case "bug": + return task.severity; + case "feature": + return task.targetRelease; +} +``` -- A variant with no `@@map`, like `Bug`, shares the base table. Its extra columns are added to the base table and are nullable there, because a `Feature` row has no severity. -- A variant with its own `@@map`, like `Feature`, gets its own table. Its columns there can be `NOT NULL`, and reading a full variant costs a join. +`@@map` decides how a variant is stored. A variant with no `@@map`, like `Bug`, is stored in the base model's table, and its extra columns are added there and are nullable, because a `Feature` row has no severity. A variant with its own `@@map`, like `Feature`, is split into its own table, named by the string you pass, and its columns there can be `NOT NULL`, though reading a full variant costs a join. In Prisma ORM 7, `@@map` only renamed a table. Start with the shared table. Give a variant its own table when its columns have to be `NOT NULL` in the database, or when the variant has many columns of its own. Use a base model with variants when you query the variants together (one feed of tasks, one stream of events) and each variant has real structure of its own. If you never query them together, separate models are simpler. If the variants differ by one nullable field, a single model is simpler still. @@ -210,5 +210,5 @@ Projects created with `npm create prisma@latest` include the [Prisma ORM skills] ## Next steps - [Query relations](/orm/fundamentals/relations-and-joins) with `.include(...)`, filters on relation data, and reads across the join table. -- To change the database to match the contract, run `npx prisma migration plan --name `, which writes a migration file, and then `npx prisma db migrate`, which applies it. See [Generating a migration](/orm/migrations/generating-a-migration). +- To change the database to match the contract: edit `contract.prisma`, run `npx prisma contract emit`, then `npx prisma migration plan --name `, which writes a migration file, then `npx prisma db migrate`, which applies it. See [Generating a migration](/orm/migrations/generating-a-migration). - To model for MongoDB, see the [MongoDB data modeling guide](/orm/data-modeling/mongodb). diff --git a/docs/orm-docs-audit/c21-data-modeling/index.fix4.md b/docs/orm-docs-audit/c21-data-modeling/index.fix4.md new file mode 100644 index 00000000000..b92ade55e8f --- /dev/null +++ b/docs/orm-docs-audit/c21-data-modeling/index.fix4.md @@ -0,0 +1,45 @@ +Done. Page is at `/Users/will/Projects/prisma/web/.claude/worktrees/pr-conflicts-review-fb93f7/wip/c21/dm/r4/index.mdx`, 257 lines before, **252 after**. `check-plain.sh` is clean, no em-dashes, no extra banned words, no semicolons in prose. + +## What I did, mark by mark + +**Sentences the reader could not restate** + +- **Line 13, four facts in one paragraph.** Split in two. First paragraph ends at "Prisma ORM derives your TypeScript types and your migrations from it." Second: "There is no `datasource` or `generator` block any more, and the connection string is in `prisma.config.ts` instead. [A complete contract](...) shows a whole file, and that page shows the config file too." +- **"coming from Prisma ORM 7" did not cover a 6 user.** Link text now reads "If you are [coming from Prisma ORM 6 or 7](/orm/coming-from-prisma-orm-7), that page covers the rename and the rest of the differences." +- **Line 151 table note.** Now "The PostgreSQL names in the table are the column types Prisma ORM creates." +- **"the ORM filter".** Now `.where()`: "On a `Jsonb` field, `.where()` compares the whole value. You can ask whether the column equals a given document, but not whether a key inside it matches. A `Json` field cannot be compared in `.where()` at all..." +- **`@@type("pg/text@1")` half-explained.** Now "`@@type` is required on every enum and says how the values are stored, here as PostgreSQL `text`. `pg/text@1` is the PostgreSQL type plus a version, always `@1` today. On MongoDB, write `@@type("mongo/string@1")`." +- **BigInt riddle.** Now "Changing `Int` to `BigInt` later needs a migration. Pick `BigInt` now and you skip it." +- **Optional fields with no example.** Now "...only when "absent" means something different from a sensible default, such as `publishedAt DateTime?` where no value means unpublished." +- **Comma splice on relation fields.** Now "A relation uses two kinds of field. The model that holds the link needs both. The model on the other end needs only a relation field:" and the first bullet says "The link field stores the other record's primary key, like `authorId`. Prisma ORM 7 called this the relation scalar field." +- **"are rejected" / implicit many-to-many.** Now "Prisma ORM 7 let you write a list field on both sides with no such model. That is gone, and `npx prisma contract emit` rejects it." + +**"So what do I type?"** + +- **Connection string.** The complete-contract clause now says that page shows the config file too. No config snippet added, per the brief. +- **MongoDB id.** "The field takes no `@default`. Omit `id` when you create a document and MongoDB assigns it." +- **Temporal polyfill.** "...add `import "temporal-polyfill/full/global"` in your app's entry file, before any query runs." +- **One-to-one.** Kept the prose and added the link: "[One-to-one](/orm/data-modeling/relational-databases#one-to-one) has both models written out." I did not add the code block; the cuts did not pay for seven lines, and the target page already shows both models in full. +- **Commands.** Now an ordered sequence: "Edit the contract, then run `npx prisma contract emit` to check it and regenerate your types, then `npx prisma migration plan --name ` to write a migration file, then `npx prisma db migrate` to apply it. `npx prisma contract emit` replaces `prisma generate`." +- **`Decimal` vs `Numeric(10, 2)`.** Table cell is now "`Decimal`, or `Numeric(10, 2)` with a precision and scale", and the duplicate prose about both spellings is gone. +- **"reference tables" before its gloss.** Reordered: "Some tables hold a fixed set of values that other records point at. Natural keys fit these reference tables best." + +**Mechanism instead of instruction** + +- **Primary key paragraph trimmed** to the instruction plus one trailing clause: "Give every model one and mark it with `@id`, because `update` and `delete` fail at runtime on a model that has neither a primary key nor a unique field:" +- **Duplicate "leaks row counts".** Cut "and the value tells nobody how many records you have" from the UUID paragraph; kept the autoincrement sentence. +- **`uuid(4)` equivalence cut.** "Write `@default(uuid(7))` for ids that sort by creation time. `@default(uuid())` is random." + +**Not redefined, by the section's rules:** polymorphic collections and relations (defined on `/orm/data-modeling/mongodb` and `/orm/data-modeling/relational-databases`, both already linked); relation kinds and foreign key placement (linked). + +**Cuts that paid for the additions:** the second `Product`/`sku` example model in "Surrogate keys" (the `User` example already makes the point), and the `Decimal` paragraph merged into the `DateTime`/`Bytes` paragraph. + +## Facts looked up + +- MongoDB enum `@@type("mongo/string@1")` — `wip/prisma-src/examples/mongo-demo/src/contract.prisma:3-4`. +- `#one-to-one` anchor exists and shows `User` plus `Profile` with `userId Int @unique` — `apps/docs/content/docs/orm/data-modeling/relational-databases.mdx:73-91`. + +## Q list + +- **Q1.** The reader asked how to add two `Decimal` values when they arrive as strings. I found no guidance in the rc.10 tree (searched `wip/prisma-src/examples/**` and the demo `main.ts` for decimal handling; only aggregate results such as `avgDecimal` returning a decimal string turned up). The page still says only "use `Int` in the currency's smallest unit for ordinary money, `Decimal` when you need more decimal places". No claim invented. +- **Q2.** I did not add "MySQL and SQLite are not options" to the moved support sentence, even though the reader's biggest surprise was MySQL. The source tree contains `wip/prisma-src/examples/prisma-8-demo-sqlite/`, so naming SQLite as unsupported would have been a new claim I cannot back. The sentence now sits in the intro, before the four-block list, as instructed. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/index.round4.md b/docs/orm-docs-audit/c21-data-modeling/index.round4.md new file mode 100644 index 00000000000..8f18443af88 --- /dev/null +++ b/docs/orm-docs-audit/c21-data-modeling/index.round4.md @@ -0,0 +1,70 @@ +Read once, top to bottom, as a Prisma 6 user who has never seen v8. + +## Sentences I could not restate after one reading + +**Line 13** — "In Prisma ORM, you define this structure in your contract, the `contract.prisma` file that replaced `schema.prisma`." +Stopped by "your contract." The page uses the word as if I should already know why a schema file is now called a contract. Plainest version: "Prisma 8 renames `schema.prisma` to `contract.prisma`. It still holds your models." Also: four separate facts land in one paragraph (rename, types/migrations derived from it, no `datasource`/`generator`, connection string moved). I had to reread to separate them. + +**Line 151** — "Where a row names a PostgreSQL type such as `numeric`, that is the column type on PostgreSQL." +Abstract and self-referential; I read it three times. Plainer: "The PostgreSQL names in the table are the column types Prisma creates." + +**Line 157** — "Filtering on a `Jsonb` field is whole-value equality only: you can ask whether the column equals a given document, not whether a key inside it matches. A `Json` field offers no comparison at all in the ORM filter." +"the ORM filter" — I do not know what that names. The `where` clause? A specific API? Two types, two different restrictions, and a workaround, all in one paragraph. + +**Line 171** — "`@@type(\"pg/text@1\")` says how the values are stored, here as PostgreSQL `text`." +This explains the half I could already guess and skips the half I could not: what `pg/` and `@1` mean, whether it is required on every enum, and what else I am allowed to put there. + +**Line 204** — "Widening a type later is a migration, so picking the wider type now saves you one." +Compressed to the point of riddle. Plainer: "Changing `Int` to `BigInt` later requires a migration. Pick `BigInt` now and you skip it." + +**Line 206** — "Mark a field optional (`?`) only when \"absent\" means something different from a sensible default. A required field with a default is often the clearer model." +I understand the words and cannot picture the case. No example. Everything else in this section has one. + +**Line 212** — "Two kinds of field describe a relation, the link field and the relation field. The model that holds the link needs both of them." +Comma splice, and "the model that holds the link" arrives before I know what "the link" is. Plainer: "A relation uses two fields. One stores the other record's id. The other is typed as the other model and exists only so Prisma can follow the relation." + +**Line 236** — "List fields on both sides with no such model are rejected, so write the model out as many-to-many shows." +Rejected by what, and when — at `contract emit`? I also had to work out on my own that this means implicit many-to-many (`posts Post[]` / `tags Tag[]` with no join model) is gone, which is a significant break from what I have been writing for two years. The page never says so. + +## Words and phrases I had to guess + +- **"contract"** — guessed: just the new name for the schema file, with no behavioral meaning. Still not sure that guess is right, because the word is doing a lot of work (`prisma contract emit`). +- **"link field"** (line 214) — guessed: what I have always called the relation scalar field, e.g. `authorId`. The page never says these are the same thing. +- **"the ORM filter"** (line 157) — guessed: the `where` argument in queries. +- **"reference tables"** (line 63) — guessed correctly from the gloss that follows, but it is stated before the gloss. +- **"polymorphic collections"** / **"polymorphic relations"** (lines 241, 254) — guessed: some way to point at more than one model. Appears only in link text, never explained. +- **"`pg/text@1`"** (line 165) — guessed: PostgreSQL's `text` type, version 1 of some mapping registry. No idea what the `@1` is for. +- **"Prisma ORM skills"** (line 245) — guessed: markdown instruction files a coding agent reads. + +## Where I asked "so what do I actually type?" + +1. **Line 13, the connection string.** "the connection string is in `prisma.config.ts` instead." I have no idea what that file looks like. This is the single thing that stops me writing a working project, and the page moves on. The "complete contract" link may have it, but the sentence does not say so. +2. **Line 51, the MongoDB id.** `id ObjectId @id @map("_id")` — no `@default`. In v6 I wrote `@default(auto())`. Do I add a default or not? "Omit `id` when you create a document. MongoDB assigns it." implies not, but does not say it. +3. **Line 155, the Temporal polyfill.** "add `import \"temporal-polyfill/full/global\"`" — add it where? Top of every file? One entry file? A Next.js `instrumentation.ts`? +4. **Line 153, `Decimal` arithmetic.** "A `Decimal` value reaches your code as a string" — so how do I add two of them? The money advice below implies I should not, but never says what to do if I must. +5. **Line 234, one-to-one.** Prose only. Every other relation shape gets a code block or a link. I wanted the four lines of `contract.prisma`. +6. **Line 165, enums.** Is `@@type` mandatory? What do I write for MongoDB? The block shown would be my template and I do not trust it. +7. **Line 256, the commands.** Three commands in one bullet with no ordering or "run this when": + `npx prisma contract emit`, `npx prisma migration plan --name `, `npx prisma db migrate`. I also cannot tell whether `contract emit` is what replaced `prisma generate` — `generate` is never mentioned, and I have typed it for two years. +8. **Line 143, `Decimal` vs `Numeric(10, 2)`.** One table row, two spellings. Is `Numeric` a distinct type or `Decimal` with arguments? Line 153 implies the latter; the table implies the former. + +## Where it explains the insides when I wanted instructions + +- **Line 40** — "Prisma ORM accepts a model without one, but `update` and `delete` on that model fail at runtime, because they need a primary key or a unique field." Two clauses about validator behavior for one instruction: give every model a primary key. The runtime detail is fine as a trailing note, but it takes over the paragraph. +- **Line 106** — "it leaks row counts and invites guessing if you expose it in URLs." Reasonable, but combined with line 116's "the value tells nobody how many records you have," the same point is made twice in ten lines. +- **Line 116** — "`@default(uuid())` and `@default(uuid(4))` both give you a random UUID." I did not ask which spellings are equivalent. I asked which one to write. Say "write `@default(uuid(7))`" and drop the equivalence. + +## Could I do what the page is for, after one reading? + +Partly. The page is an overview, and I came away able to write a model, choose `@id` versus `@@id`, pick between a natural and a surrogate key, pick scalar types, and write a one-to-many relation. That part works. + +What I still would not know: + +- How to get from this page to a project that runs. The connection string moved to a file the page names once and never shows. Without it nothing I wrote here connects to anything. +- What `contract` means beyond a filename, and therefore what `npx prisma contract emit` does. +- Whether `prisma generate` still exists. +- How to write an enum I can trust, because `@@type("pg/text@1")` is unexplained. +- That implicit many-to-many is gone — I inferred it from "are rejected," and I would have written `Post[]` / `Tag[]` on both sides out of habit and been surprised. +- That MySQL is no longer supported until line 22, which is one sentence deep in a section about building blocks. For a v6 user on MySQL this is the most consequential sentence on the page and it is not treated as such. + +One more, as an existing user: line 13 points to "what changed" for people "coming from Prisma ORM 7." I am coming from 6. The page does not say whether that link covers me. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/mongodb.fix4.md b/docs/orm-docs-audit/c21-data-modeling/mongodb.fix4.md new file mode 100644 index 00000000000..57dcce7e8bc --- /dev/null +++ b/docs/orm-docs-audit/c21-data-modeling/mongodb.fix4.md @@ -0,0 +1,49 @@ +Done. Edited only `wip/c21/dm/r4/mongodb.mdx`. + +## Marks and what I did + +**Sentences the reader could not restate** + +1. **Line 13, three new things in three sentences.** Reordered: models in the contract first, then the connection. "There is no `datasource` block on MongoDB." is now a trailing sentence. Added "That page shows the config file." after the `/orm/contract-authoring/the-data-contract` link. No `prisma.config.ts` snippet, per the brief. +2. **Line 24, eight-sentence paragraph.** Split into three: `_id` naming plus what `@map`/`@@map` do; the key type and the two removed syntaxes; how ids get assigned. +3. **"sets the name on both sides".** Now: "In the contract you call the field `id`. In TypeScript it is always `_id`, never `id`. `@map("_id")` sets the name in the database and in your code, so you write `user._id` and `where({ _id })`." +4. **`ObjectId` type vs string value.** Now: "`ObjectId` is the contract type. In TypeScript ids are plain strings, so there is no `ObjectId` to construct." +5. **`u` unexplained, array unexplained, two forms.** Now: "`update` takes a callback that returns an array, so several changes go in one call. Use `u.fieldName` to change a top-level field. Use `u("path.to.field")` for a field inside an embedded document, written as a string because a dotted path is not a property name." +6. **Pipeline builder not actionable.** Variable renamed `berliners` → `query`, and: "`build()` returns the query, not the rows. See [the pipeline builder](...) for how to run it and what comes back." +7. **"goes stale on its own".** Now: "If the original changes, the copy does not. You update it yourself." + +**Guessed words** + +- "your contract", "pipeline builder", "variant", "discriminator field", `@@base` — defined elsewhere in the section or already defined well by example; unchanged. +- "contract emit" — now said once, in the Next steps bullet, with "which checks your contract and regenerates your types". Removed from the removed-syntax sentence. +- `db.orm` — now: "`db.orm` is the ORM client, one of the query APIs on `db`. See [the ORM reference](/orm/reference)." + +**"So what do I actually type?"** + +1. `prisma.config.ts` — link plus "That page shows the config file". No snippet, per the brief. +2. Import — `import { db } from "./prisma/db";` added once, in the first TypeScript example. +3. Running the pipeline — fixed by the rename and the `build()` clause. +4. Command order — Next steps bullet is now the ordered sequence: `npm create prisma@latest -- my-app` or `npx prisma orm init`, then `contract emit`, `migration plan --name `, `db migrate`. The scattered mentions are cut (`orm init` in the `db` paragraph, `contract emit` in the removed-syntax sentence). +5. Unique index — `email String @unique` added to the first model, plus "`@unique` on `email` becomes a unique index when you migrate." +6. Variant's own fields — nothing added; see the lookup below. +7. `references: [id]` vs `_id` — "In `references: [id]` you write the field's contract-side name, `id`, not `_id`." +8. Reading the other direction — "`.include("posts")` on `db.orm.users` works the other way and gives each user an array of their posts." + +**Internals** + +- Product history on line 13 — kept the section's mandated first-use phrasing, but it is no longer the first clause carrying the connection story. +- "fail at `npx prisma contract emit`" — cut; the sentence is now "The old `String @db.ObjectId` and `@default(auto())` are no longer allowed." + +## Facts looked up + +- `@unique` on a MongoDB field creates a unique index: `wip/prisma-src/packages/2-mongo-family/2-authoring/contract-psl/src/interpreter.ts:908-912` builds `new MongoIndex({ keys: [...], unique: true })`; `wip/prisma-src/packages/2-mongo-family/9-family/src/core/contract-to-schema.ts:17-20` carries `unique` into the schema the migration applies. Spec at `.../contract-psl/src/mongo-attribute-specs.ts:146`. +- `.include` on the one-to-many list side works and returns an array: `.../5-query-builders/orm/src/collection.ts:98` accepts any `ReferenceRelationKeys`; `.../orm/src/types.ts:20-30` includes list-side reference relations; unit test `.../orm/test/collection.test.ts:492` (`User`.include('tasks')) and integration test `.../orm/test/integration/orm-ergonomics.test.ts:231-262` ("include() on 1:N relation returns array of related documents"). +- Variant rows do **not** carry the variant's own fields typed: `.../orm/test/integration/polymorphism.test.ts` only reads base fields (`type`, `title`) off variant rows and casts to `Record` at line 149 to read the discriminator off a create result. The collection row type is keyed on the base model name (`collection.ts:78-80`, `all()` at line 109). So I said nothing about `summary` on a variant read. + +## Q list + +None. + +## Line count + +173 before, 172 after. `check-plain.sh` is clean; no em-dashes, no semicolons joining clauses, no page-conventions banned words. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/mongodb.round4.md b/docs/orm-docs-audit/c21-data-modeling/mongodb.round4.md new file mode 100644 index 00000000000..8b5f0e31b0b --- /dev/null +++ b/docs/orm-docs-audit/c21-data-modeling/mongodb.round4.md @@ -0,0 +1,76 @@ +# Reader review: `wip/c21/dm/r4/mongodb.mdx` + +## Sentences I could not restate after one reading + +**1. Line 13** — "Your models go in your contract, the `contract.prisma` file that replaced `schema.prisma`. There is no `datasource` block on MongoDB. The connection string is in your `prisma.config.ts` file instead, passed to `defineConfig` from `@prisma/orm-mongo/config`." + +What stopped me: three new things in three sentences, none shown. "your contract" is used as a noun for a thing I have never heard of. `defineConfig` and `@prisma/orm-mongo/config` are named but never demonstrated — I cannot picture the file. "There is no `datasource` block" answers a question I had not asked yet and implies other databases still have one. + +Plainer: "Your models go in `contract.prisma` (Prisma 8's replacement for `schema.prisma`). MongoDB connection settings go in `prisma.config.ts`, not in a `datasource` block — [example here]." + +**2. Line 24** — the whole paragraph. It is eight sentences carrying eight unrelated facts: `_id` naming, what `@map` does, what `@@map` does, the default collection name, the key type, two removed syntaxes, the command that validates, and how ids get assigned. I lost the thread by the fourth sentence. Split it. + +**3. Line 24** — "`@map("_id")` maps the model's `id` field to MongoDB's mandatory `_id` field and sets the name on both sides, so you write `user._id` and `where({ _id })`." + +"sets the name on both sides" — which two sides? Database and TypeScript? I had to infer it from the example that follows. And it contradicts my expectation: I declared the field as `id` in the contract, so why is it `_id` in TypeScript? The page states it but never says why, so I do not know whether I can make it come out as `id`. + +Plainer: "In the contract you call the field `id`; in TypeScript it is always `_id`." + +**4. Line 30** — "The id comes back as a string, and you pass it back as a string. There is no `ObjectId` to construct." + +I had just been told to "Write `ObjectId` for the key type." So the type is `ObjectId` but the value is a string. That is not a contradiction once you think about it, but the page sets it up as one and does not close it. Say plainly: "`ObjectId` is the contract type; in TypeScript ids are plain strings." + +**5. Line 81** — "In an `update`, `u` builds the changes you want." + +`u` arrives with no introduction. Looking at the example, `update` takes a function and the function returns an **array** — nothing says why it is an array or that multiple changes go in one call (I only worked that out from the second example). Also unexplained: why `u.tags` (property) and `u("address.city")` (call) are two different forms. I guessed: dotted paths must be strings because they are not valid property names. The page should just say that. + +**6. Line 88** — "`.where()` filters on top-level model fields only. To match on a field inside an embedded document, use the pipeline builder, a chained builder for MongoDB aggregation pipelines." + +The limitation is clear; the replacement is not. "chained builder for MongoDB aggregation pipelines" tells me nothing I can act on, and the example that follows does not run (see below). + +**7. Line 123** — "The copy then goes stale on its own, and keeping it up to date is your job." + +"goes stale on its own" reads oddly, as if the data actively rots. Plainer: "If the original changes, the copy does not. You have to update it yourself." + +## Words and phrases I had to guess + +- **"your contract"** (line 13) — guessed: the Prisma 8 name for the schema file. The page tells me the filename in the same breath but never says the word "contract" is now the general term for the whole thing, and then uses it four more times ("changes to your contract", "checks your contract"). +- **"contract emit"** (line 24, 169) — guessed: the new `prisma generate`. "emit" is doing a lot of work; the inline gloss ("checks your contract and regenerates your types") helped, but it is buried mid-paragraph in a sentence about removed syntax. +- **"pipeline builder"** (line 88) — guessed: a fluent API that produces a MongoDB aggregation pipeline. +- **"variant"** (line 127 onward) — guessed: a subtype. Fine, the page defines it well enough by example. +- **"discriminator field"** (line 127) — guessed correctly, but only because I have seen the term elsewhere. The sentence "its value says which kind each document is" is doing the defining work and is good. +- **`db.orm`** (line 30) — guessed: a namespace on the client. Never explained why there is an `orm` level at all, or what else lives on `db`. +- **`@@base(Post, "article")`** — guessed: "this model extends Post, and its discriminator value is `article`." Line 148 confirms it one sentence later, which is fine. + +## "So what do I actually type?" — places the page does not say + +1. **The `prisma.config.ts` file.** Named, with an import path and a function, and never shown. This is the one thing without which nothing on the page works. It is the biggest hole on the page. +2. **Importing the client.** Line 30 says `npx prisma orm init` writes `src/prisma/db.ts`, then every example uses a bare `db`. No import line anywhere. Do I write `import { db } from "./prisma/db"`? Guessing. +3. **Running the pipeline.** Line 91 assigns `const berliners = db.query.from("users")...build()`. No `await`. So `berliners` is... a pipeline object? An array of Berliners? The name says the latter, the code says the former. The page punts to another page for "running it and what comes back" — but then it should not name the variable as though it holds results. +4. **The order of commands for a new project.** The page names `npm create prisma@latest`, `npx prisma orm init`, `npx prisma contract emit`, `npx prisma migration plan --name `, `npx prisma db migrate`, and `npx prisma skills sync`, scattered across five sections. I finished the page not knowing which of these I run first, or whether `orm init` and `npm create` are alternatives. +5. **Indexes and uniqueness.** Line 169 says migration "creat[es] the collections and indexes," but no example declares an index. If I want `email` unique — the very first model has an `email` field — the page does not tell me how. +6. **Querying a variant's own fields.** `db.orm.posts.variant("Article")` limits the query, but nothing shows reading `summary` off a result, or what happens if I `.all()` the base and want to tell an Article from a Tutorial in TypeScript. +7. **`references: [id]` vs `_id`.** Line 111 writes `references: [id]` using the contract-side name while everything in TypeScript is `_id`. I paused on whether that was a typo. One clause would fix it. +8. **Reading references the other way.** `.include("author")` goes child-to-parent. Nothing shows loading a user's `posts`. + +## Internals where I only wanted instructions + +Very little, which is unusual. Two spots: + +- **Line 13** — "the `contract.prisma` file that replaced `schema.prisma`" is product history, not an instruction. It is useful to me as an upgrader, but it is delivered as the definition of what a contract is, which it is not. +- **Line 24** — "The old `String @db.ObjectId` and `@default(auto())` are no longer allowed and fail at `npx prisma contract emit`." Telling me *which command* rejects them is implementation detail I did not need mid-paragraph; "these no longer work, write `ObjectId`" is enough, and `contract emit` deserves its own introduction elsewhere. + +## Could I do what the page is for, after one reading? + +**Half.** The decision the page exists to help me make — embed or reference — I can now make confidently. The table on lines 44–51 and the blog-comments example on line 53 are the clearest part of the page; I could apply them to my own data immediately. Same for the polymorphic section: the discriminator explanation on line 148, including "write `kind` and not `"kind"`" and `.variant("Article")` not `"article"`, anticipated exactly the two mistakes I would have made. + +**What I still would not know:** + +- How to connect to MongoDB at all. I cannot write `prisma.config.ts` from this page. +- Whether my project is set up correctly, or in what order to run the six commands named. +- How to import `db`. +- How to actually execute a filter on an embedded field — the one example is incomplete. +- How to declare a unique index on `email`, despite the page putting `email` in its very first model. +- Why my `id` field is `_id` in TypeScript, and whether that is changeable. + +The modeling guidance is solid. The surrounding mechanics — set up, connect, run — are named rather than shown, so a reader coming from Prisma 7 cannot get from this page to working code. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/relational-databases.fix4.md b/docs/orm-docs-audit/c21-data-modeling/relational-databases.fix4.md new file mode 100644 index 00000000000..0e0ad3d59ac --- /dev/null +++ b/docs/orm-docs-audit/c21-data-modeling/relational-databases.fix4.md @@ -0,0 +1,48 @@ +Done. Page is at exactly 214 lines (unchanged budget), plain-language check clean, no em-dashes. + +## What I changed, mark by mark + +**Sentences the reader could not restate** + +- **Line 25, "contract" arriving mid-section.** Moved into the intro, before the first heading: "You write your models in your contract, the `contract.prisma` file that replaced `schema.prisma`. After every change to it, run `npx prisma contract emit`, which checks the contract and regenerates your TypeScript types. It does not touch the database." +- **Line 29, two levels of indirection.** Now: "When a `User` row is deleted or its `id` changes, referential actions decide what happens to the `Post` rows that point at it." +- **Line 35, `NoAction`.** Cut to "`NoAction` is the same as writing nothing." +- **Line 93, "allows the mirror field".** Now: "Because `userId` is unique, the mirror field can be a single `Profile?` instead of a list." +- **Line 138, relation names contradicting line 37.** Reconciled: "If a second join table connects the same two models, the relation name goes on both sides again, which here means the list field, `Post.tags`, and `PostTag.post`, the field that points back at `Post`." Verified against `psl-relation-resolution.ts:417-418`. +- **Line 192, union type in prose.** Replaced with a `switch (task.type)` example returning `task.severity` / `task.targetRelease`, introduced by "Switch on `task.type` to reach a variant's own fields:". +- **Lines 196-197, `@@map` surprise.** Now one paragraph, one sentence each: no `@@map` means the variant is stored in the base model's table with nullable extra columns; its own `@@map` splits it into its own table named by the string you pass; "In Prisma ORM 7, `@@map` only renamed a table." + +**Words guessed at** + +- "contract" and "emit" defined in the intro (above). "mirror field", "discriminator", "variant"/"base" stay as defined; the Prisma ORM 7 "back-relation" history is cut. +- **"is rejected"** now names who rejects and when: "If one is optional and the other is required, `npx prisma contract emit` reports an error." +- **"the model for the join table"** appears once, on first use; every later mention is `PostTag` ("Declare `PostTag` yourself. If you skip it, ..."). +- **`public`** explained in the intro before any use. + +**"So what do I type?"** + +1. **`db` import.** Added once, in the first TypeScript example: `import { db } from "./prisma/db";`. +2. **Command order.** Next steps bullet is now the ordered sequence: edit `contract.prisma`, `npx prisma contract emit`, `npx prisma migration plan --name `, `npx prisma db migrate`. The duplicate emit sentence is gone from "How relations are declared". +3. **`public` off PostgreSQL.** `description` and `metaDescription` now say PostgreSQL only; MongoDB keeps its own Next steps link. +4. **Self-relations.** Added "One relation between a model and itself needs no relation name." +5. **Mirror fields in many-to-many.** Added: "`Post.tags` and `Tag.posts` are the mirror fields that `t.connect` goes through. Leave them out and you write the `PostTag` rows yourself, with `db.orm.public.PostTag.create({...})`." +6. **`SetDefault` with no column default.** Nothing added, see Q1. +7. **Naming a variant's table.** Covered by "split into its own table, named by the string you pass". + +**Internals** + +Cut: the Prisma ORM 7 back-relation history, the `NoAction` SQL trivia, and the "why `"bug"` is a TypeScript error" reasoning (now just "Pass the model name, `"Bug"`, with a capital letter. `"bug"` is a type error."). Kept the `ON DELETE` / `ON UPDATE` sentence because page-conventions.md mandates that wording. + +**Ordering.** The `db` / `db.orm` / `public` explanation is now in the intro, ahead of every use. + +## Facts looked up + +- Self-relation needs no name when there is only one: `/Users/will/Projects/prisma/web/.claude/worktrees/pr-conflicts-review-fb93f7/wip/prisma-src/test/integration/test/sql-orm-client/fixtures/contract.ts:141-142` (`invitedUsers` / `invitedBy`, no relation name). The PSL fixture at `.../fixtures/self-relations/contract.prisma` does use names (`"Management"`, `"Partnership"`), but only because that model has several self-relations, which matches the page's existing rule. +- Join model with no list fields on the far models: `psl-relation-resolution.ts:225-275` and `:397-430` only run junction matching for backrelation list fields, and `PSL_ORPHANED_BACKRELATION` / `PSL_AMBIGUOUS_BACKRELATION` are raised from list-field candidates. With no list fields there are no candidates, so nothing rejects it. +- Many-to-many relation name placement: `psl-relation-resolution.ts:417-418` (the diagnostic tells you to add the name to the list field and to the join model's field pointing back). + +## Q list + +- **Q1** — Reader asked what happens with `SetDefault` when the column has no `@default`: error at emit or at runtime? Grepped `SetDefault` and `SET DEFAULT` under `wip/prisma-src/packages/3-targets/3-targets/postgres/src/core/migrations/` and `wip/prisma-src/packages/2-sql/`. The only hits are the attribute spec (`sql-attribute-specs.ts:424`), the two name mappings (`psl-relation-resolution.ts:29`, `relation-inference.ts:17`), and unrelated column-default DDL. No validation of a missing column default exists in the source, so the page says nothing about it. + +Line count: 214 before, 214 after. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/relational-databases.round4.md b/docs/orm-docs-audit/c21-data-modeling/relational-databases.round4.md new file mode 100644 index 00000000000..e51b1aa3d11 --- /dev/null +++ b/docs/orm-docs-audit/c21-data-modeling/relational-databases.round4.md @@ -0,0 +1,76 @@ +Read once, top to bottom, as someone coming off two years of Prisma 6/7. + +## Sentences I could not restate after one reading + +**Line 25** — "Your contract is the `contract.prisma` file that replaced `schema.prisma`. After every change to it, run `npx prisma contract emit`. That command checks the contract and regenerates your types." + +This is the first time the word "contract" appears and it arrives mid-section, bolted onto a paragraph about `@relation`. I don't know what a "contract" *is* in this product — is it just the renamed schema file, or a new concept? "Emit" tells me nothing. I'd have understood: "In Prisma 8 the schema file is called `contract.prisma` instead of `schema.prisma`. After you edit it, run `npx prisma contract emit` to regenerate the TypeScript client. This does not touch the database." + +**Line 29** — "Referential actions decide what the database does to the rows on the foreign key side when the row they point at is deleted or its key changes." + +Two levels of indirection ("the rows on the foreign key side", "the row they point at") in one sentence. I had to re-read to work out which is which. Plainer: "When a `User` row is deleted or its `id` changes, referential actions decide what happens to the `Post` rows that reference it." + +**Line 35** — "`NoAction` writes the clause out, which does the same thing as writing no clause at all." + +Stopped me cold. If it does nothing different, why does it exist and why would I ever type it? The page gives me a menu item that it simultaneously says is pointless. Either say why it's there (I vaguely recall it differs from `Restrict` on some databases) or drop it. + +**Line 93** — "The `@unique` already on `Profile.userId` is what allows the mirror field." + +"Allows" is doing unexplained work. Allows it to exist at all? Allows it to be `Profile?` rather than `Profile[]`? I guessed the latter but the sentence doesn't say. Plainer: "Because `userId` is unique, the mirror field can be a single `Profile?` instead of a list." + +**Line 138** — "If a second join table connects the same two models, put the relation name on the list field, `Post.tags`, and on `PostTag.post`, the field that points back at `Post`." + +This contradicts what line 37 told me. Line 37 said names go on "both sides" of the relation, with `fields:` on one side. Here the name goes on a list field and on a *third* model's field. I cannot reconcile the two rules, and there's no code for this one, just prose describing where to put things. + +**Line 192** — "In TypeScript those rows are a union with one member per variant, and each member's discriminator field is that variant's value, so switch on the discriminator field to reach a variant's own fields." + +Three ideas in one sentence and zero code. I think I know what a discriminated union is, but the page is describing the shape of a result type entirely in words. Show me the five-line `switch (task.type)` block instead. + +**Line 196–197** — `@@map` deciding whether a variant shares the base table or gets its own. + +In every Prisma version I've used, `@@map` renames a table. Nothing more. Discovering it silently controls physical storage layout for variants is a genuine surprise, and the page states it as if it were obvious. I re-read it twice to check I hadn't misread. + +## Words and phrases I had to guess at + +- **"contract"** — guessed: the new name for the schema file. Still don't know if it means more than that. +- **"emit"** — guessed: generate the TypeScript client. +- **"mirror field"** (line 60) — guessed correctly from the definition given, but it's a brand-new coinage. You then tell me Prisma 7 called it "back-relation" and error codes still say that, so I now hold two names for one thing. +- **"is rejected"** (line 27) — guessed: `contract emit` errors. The page doesn't say who rejects it or when I'd find out. +- **"the model for the join table"** — guessed this is what used to be called an explicit many-to-many. The phrase is awkward and repeated four times; your own agent prompt at line 207 says "explicit model for the join table", so the word exists. +- **"discriminator"** — guessed: the column recording which variant a row is. The page does define it, but it's one sentence carrying a new keyword, a new concept, and a code example at once. +- **"variant"** vs **"base"** — guessed subclass/superclass. Works, but I only inferred it. +- **`public`** in `db.orm.public.Post` — told it's the PostgreSQL schema. See below. + +## Where I asked "so what do I actually type?" + +1. **`db` is never imported.** Line 62 says it lives in `src/prisma/db.ts`. Every code block then uses a bare `db`. I don't know the import line. Show `import { db } from "@/prisma/db"` once. + +2. **Three setup commands, no order.** `npm create prisma@latest` (line 203), `npx prisma orm init` (line 62), `npx prisma contract emit` (line 25), `npx prisma migration plan --name ` and `npx prisma db migrate` (line 213). I cannot assemble the actual loop. The full sequence — edit `contract.prisma`, emit, plan, migrate — is never written down in one place, and it's the single thing I most need. + +3. **What is `public` if I'm not on PostgreSQL?** The description says "PostgreSQL and other SQL databases". `db.orm.public.Post` appears in every query. On MySQL or SQLite, what do I type there? The page doesn't say, and this is in literally every example. + +4. **Self-relations (line 58).** "A category with a parent category is an ordinary one-to-many whose relation field is typed as the same model." No code. And a self-relation has two fields on one model pointing at each other — by line 37's rule that looks like it needs a `@relation` name. Does it? I'd have to go try it and see if emit errors. + +5. **Are mirror fields required for many-to-many?** Line 60 tells me mirror fields are optional in Prisma 8. The many-to-many example includes `Post.tags` and `Tag.posts`, and line 143's nested write goes through `tags`. So are they optional here too, and if I omit them do I lose `t.connect`? Not answered. + +6. **`SetDefault` with no default.** "writes the foreign key's default value" — what happens if the column has no `@default`? Error at emit, or at runtime? + +7. **How do I name a variant's own table something specific?** `@@map("features")` on `Feature` both splits it into its own table *and* names it `features`. If I want its own table named `Feature`, do I write `@@map("Feature")`? And what table does `Bug` map to — is it `Task`? Not stated. + +## Where the page explains internals instead of what to do + +- **Line 60**, the history lesson: "Prisma ORM 7 called it the back-relation, and its error codes still use that word. Prisma ORM 7 required this field. Prisma ORM 8 does not." Three sentences of version archaeology in the middle of learning one-to-many. The actionable part — "you can leave it out" — is buried. +- **Line 29**, "They become the foreign key's `ON DELETE` and `ON UPDATE` clauses. Write neither and the foreign key gets neither clause, so the database decides." Useful to some, but it's describing generated SQL when I only wanted to know which action to pick. +- **Line 35**, `NoAction` — pure implementation trivia about what SQL gets written, with no guidance attached. +- **Line 186**, the paragraph explaining that passing `"bug"` instead of `"Bug"` is a TypeScript error, *and why* ("the only names it accepts are the model names of the base model's variants"). That's the type system's reasoning. Just tell me: pass the model name, capital B. +- **Line 192**, the whole union-type paragraph, discussed above. + +## Ordering problem + +Line 60 uses `db.orm.public.Post.where(...).all()` in a sentence. Line 62 — the *next* paragraph — is where `db`, `db.orm`, and `public` are finally explained. I hit the code first and had no idea what I was looking at. + +## Could I do what the page is for, after one reading? + +Partly. I could write a one-to-many and a one-to-one schema — those sections are clear and the "which side owns the foreign key" list at line 103 is the best thing on the page. I could write the many-to-many models, though I'd be unsure whether the list fields are required. + +I could not actually run any of it. I don't know the import for `db`, I don't know the command sequence from "I edited the file" to "the database has the table", and if I'm not on Postgres I don't know what replaces `public`. Polymorphic relations I could copy-paste but not adapt: I don't understand what `@@map` will do to my table layout well enough to choose, and I couldn't write the TypeScript that reads a mixed list of tasks because the page describes that result type only in prose. \ No newline at end of file From 19138ba09113b14f32712a897d7e64b686460de1 Mon Sep 17 00:00:00 2001 From: willbot Date: Mon, 14 Sep 2026 17:35:36 +0200 Subject: [PATCH 07/15] docs(orm): data-modeling final fact re-check against rc.10 Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- .../content/docs/orm/data-modeling/index.mdx | 10 +-- .../docs/orm/data-modeling/mongodb.mdx | 6 +- .../data-modeling/relational-databases.mdx | 2 +- .../c21-data-modeling/factcheck2.index.md | 58 ++++++++++++++ .../c21-data-modeling/factcheck2.mongodb.md | 79 +++++++++++++++++++ .../factcheck2.relational-databases.md | 53 +++++++++++++ 6 files changed, 199 insertions(+), 9 deletions(-) create mode 100644 docs/orm-docs-audit/c21-data-modeling/factcheck2.index.md create mode 100644 docs/orm-docs-audit/c21-data-modeling/factcheck2.mongodb.md create mode 100644 docs/orm-docs-audit/c21-data-modeling/factcheck2.relational-databases.md diff --git a/apps/docs/content/docs/orm/data-modeling/index.mdx b/apps/docs/content/docs/orm/data-modeling/index.mdx index dad45776560..f13b97d0b95 100644 --- a/apps/docs/content/docs/orm/data-modeling/index.mdx +++ b/apps/docs/content/docs/orm/data-modeling/index.mdx @@ -12,9 +12,9 @@ For example, a blog has users, posts, and comments. A user has fields like an em In Prisma ORM, you define this structure in your contract, the `contract.prisma` file that replaced `schema.prisma`. It holds your models, as `schema.prisma` did. Prisma ORM derives your TypeScript types and your migrations from it. -There is no `datasource` or `generator` block any more, and the connection string is in `prisma.config.ts` instead. [A complete contract](/orm/contract-authoring/psl-syntax#a-complete-contract) shows a whole file, and that page shows the config file too. If you are [coming from Prisma ORM 6 or 7](/orm/coming-from-prisma-orm-7), that page covers the rename and the rest of the differences. +There is no `datasource` or `generator` block any more, and the connection string is in `prisma.config.ts` instead. [A complete contract](/orm/contract-authoring/psl-syntax#a-complete-contract) shows a whole file, and that page shows the config file too. If you are [coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7), that page covers the rename and the rest of the differences. -Prisma ORM 8 supports PostgreSQL and MongoDB only. A contract is built from four building blocks: +Prisma ORM 8 supports PostgreSQL and MongoDB today, and [Supported databases](/prisma-orm/supported-databases) lists the ones coming. A contract is built from four building blocks: - [Models](#models): the things your application works with - [Primary keys](#primary-keys): how each record is identified @@ -125,7 +125,7 @@ model UserTag { } ``` -A duplicate `(userId, tagId)` pair would be meaningless, so the pair is the key. The same model is shown with its relation fields under [many-to-many](/orm/data-modeling/relational-databases#many-to-many). Avoid composite keys for ordinary models: everything that points at the model then has to store all of the key's fields. +A duplicate `(userId, tagId)` pair would be meaningless, so the pair is the key. [Many-to-many](/orm/data-modeling/relational-databases#many-to-many) writes a model like this one out with its relation fields. Avoid composite keys for ordinary models: everything that points at the model then has to store all of the key's fields. ## Scalar fields @@ -163,7 +163,7 @@ enum Priority { } ``` -`@@type` is required on every enum and says how the values are stored, here as PostgreSQL `text`. `pg/text@1` is the PostgreSQL type plus a version, always `@1` today. On MongoDB, write `@@type("mongo/string@1")`. [Enums](/orm/contract-authoring/psl-syntax#enums) has the rest of the syntax. +`@@type` says how the values are stored, here as PostgreSQL `text`. Leave it out and Prisma ORM takes the storage from the members, text for string values and an integer for whole numbers. `pg/text@1` is the PostgreSQL type plus a version, always `@1` today. On MongoDB, write `@@type("mongo/string@1")`. [Enums](/orm/contract-authoring/psl-syntax#enums) has the rest of the syntax. ### How to pick a data type @@ -248,5 +248,5 @@ Projects created with `npm create prisma@latest` include the [Prisma ORM skills] - [Model relational data](/orm/data-modeling/relational-databases): one-to-one, one-to-many, many-to-many, and polymorphic relations on PostgreSQL. - [Model MongoDB data](/orm/data-modeling/mongodb): embed or reference, and polymorphic collections. -- [Generate a migration](/orm/migrations/generating-a-migration) to get the models you wrote into the database. Edit the contract, then run `npx prisma contract emit` to check it and regenerate your types, then `npx prisma migration plan --name ` to write a migration file, then `npx prisma db migrate` to apply it. `npx prisma contract emit` replaces `prisma generate`. +- [Generate a migration](/orm/migrations/generating-a-migration) to get the models you wrote into the database. Edit the contract, then run `npx prisma contract emit` to check it and regenerate your types, then `npx prisma migration plan --name ` to write the migration files, then `npx prisma db migrate` to apply them. `npx prisma contract emit` replaces `prisma generate`. - [Query your models](/orm/fundamentals/reading-data) once your contract is in place. diff --git a/apps/docs/content/docs/orm/data-modeling/mongodb.mdx b/apps/docs/content/docs/orm/data-modeling/mongodb.mdx index 83044efad05..6ca64b39b81 100644 --- a/apps/docs/content/docs/orm/data-modeling/mongodb.mdx +++ b/apps/docs/content/docs/orm/data-modeling/mongodb.mdx @@ -75,7 +75,7 @@ A single embedded value models a one-to-one relation. A list of embedded values ```ts const address = { street: "1 Main St", city: "Berlin", country: "DE" }; -await db.orm.users.create({ name: "Ada", address }); +await db.orm.users.create({ name: "Ada", tags: [], address }); ``` In an `update`, `u` builds the changes you want. `update` takes a callback that returns an array, so several changes go in one call. Use `u.fieldName` to change a top-level field. Use `u("path.to.field")` for a field inside an embedded document, written as a string because a dotted path is not a property name. Use `.push(...)` and `.pull(...)` to add to and remove from a list of scalars: @@ -85,7 +85,7 @@ await db.orm.users.where({ _id: user._id }).update((u) => [u("address.city").set await db.orm.users.where({ _id: user._id }).update((u) => [u.tags.push("admin"), u.tags.pull("new")]); ``` -Replace a whole embedded value with `u("address").set({ ... })`, and remove it with `u("address").unset()`. A list of embedded values is replaced as a whole with `u.addresses.set([ ... ])`. `.where()` filters on top-level model fields only. To match on a field inside an embedded document, use the pipeline builder, a chained builder for MongoDB aggregation pipelines: +Replace a whole embedded value with `u.address.set({ ... })`, and remove it with `u.address.unset()`. A list of embedded values is replaced as a whole with `u.addresses.set([ ... ])`. `.where()` filters on top-level model fields only. To match on a field inside an embedded document, use the pipeline builder, a chained builder for MongoDB aggregation pipelines: ```ts const query = db.query.from("users").match((f) => f("address.city").eq("Berlin")).build(); @@ -145,7 +145,7 @@ model Tutorial { } ``` -`@@discriminator(kind)` takes the name of the field that records each document's variant, so write `kind` and not `"kind"`. `@@base(Post, "article")` declares `Article` as the `"article"` variant of `Post`. A variant declares only its own fields. `id`, `title`, and `kind` come from `Post`. A variant declares no `@@map` of its own and uses its base's collection. +`@@discriminator(kind)` takes the name of the field that records each document's variant, so write `kind` and not `"kind"`. `@@base(Post, "article")` declares `Article` as the `"article"` variant of `Post`. A variant declares only its own fields. `id`, `title`, and `kind` come from `Post`. A variant uses its base's collection, so it needs no `@@map` of its own. A query for posts returns articles and tutorials together. You reach a single variant through the base model: `db.orm.posts.variant("Article")` limits the query to one variant. `.variant()` takes the variant's model name, `"Article"`, and not the discriminator value `"article"`. Creating through a variant fills in `kind` for you: diff --git a/apps/docs/content/docs/orm/data-modeling/relational-databases.mdx b/apps/docs/content/docs/orm/data-modeling/relational-databases.mdx index e419df131cb..797cba7b2af 100644 --- a/apps/docs/content/docs/orm/data-modeling/relational-databases.mdx +++ b/apps/docs/content/docs/orm/data-modeling/relational-databases.mdx @@ -210,5 +210,5 @@ Projects created with `npm create prisma@latest` include the [Prisma ORM skills] ## Next steps - [Query relations](/orm/fundamentals/relations-and-joins) with `.include(...)`, filters on relation data, and reads across the join table. -- To change the database to match the contract: edit `contract.prisma`, run `npx prisma contract emit`, then `npx prisma migration plan --name `, which writes a migration file, then `npx prisma db migrate`, which applies it. See [Generating a migration](/orm/migrations/generating-a-migration). +- To change the database to match the contract: edit `contract.prisma`, run `npx prisma contract emit`, then `npx prisma migration plan --name `, which writes the migration files, then `npx prisma db migrate`, which applies them. See [Generating a migration](/orm/migrations/generating-a-migration). - To model for MongoDB, see the [MongoDB data modeling guide](/orm/data-modeling/mongodb). diff --git a/docs/orm-docs-audit/c21-data-modeling/factcheck2.index.md b/docs/orm-docs-audit/c21-data-modeling/factcheck2.index.md new file mode 100644 index 00000000000..cd24fe6a300 --- /dev/null +++ b/docs/orm-docs-audit/c21-data-modeling/factcheck2.index.md @@ -0,0 +1,58 @@ +Fact re-check complete. `wip/c21/dm/fc2/index.mdx`: 252 lines before, 252 after. `check-plain.sh` prints "clean"; the extra banned-word grep hits only "surro**gate**", a substring false positive. + +## Claims checked + +| Line | Claim | Source | Verdict | +| --- | --- | --- | --- | +| 13 | Contract derives TypeScript types and migrations | `skills/prisma-8/SKILL.md:34` (`contract.json` + `contract.d.ts`), `references/migrations.md:95` | Confirmed | +| 15 | No `datasource`/`generator` block; connection string in `prisma.config.ts` | `examples/prisma-8-demo/prisma.config.ts` (`db: { connection: … }`); no `datasource`/`generator` keyword anywhere in `packages/1-framework/2-authoring/psl-parser/src` | Confirmed | +| 15 | `#a-complete-contract` shows a whole file and the config file | `apps/docs/content/docs/orm/contract-authoring/psl-syntax.mdx:13`, and `prisma.config.ts` blocks at lines 108/119 | Confirmed | +| 15 | Link covers a Prisma ORM 6 reader | `apps/docs/content/docs/orm/coming-from-prisma-orm-7.mdx` — the page never mentions version 6 | **Corrected** → "If you are [coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7), that page covers the rename and the rest of the differences." | +| 17 | Prisma ORM 8 supports PostgreSQL and MongoDB **only** | `wip/prisma-src/README.md:98-101` lists SQLite under "Supported databases" as "planned next, a proof of concept today"; `scorecard.md` has a SQLite column throughout; `packages/3-extensions/sqlite` ships at `8.0.0-rc.10` | **Corrected** → "Prisma ORM 8 supports PostgreSQL and MongoDB, and SQLite is still a proof of concept." | +| 32, 46, 86, 99 | `@id`, `@default(autoincrement())` | `packages/3-targets/6-adapters/postgres/src/core/control-mutation-defaults.ts:126-131`; `sql-attribute-specs.ts:212,236` | Confirmed | +| 38 | Model becomes a table / a collection | `packages/2-mongo-family/2-authoring/contract-psl/src/interpreter.ts:1342` (`storage: { collection }`) | Confirmed | +| 42 | `update`/`delete` fail at runtime without a primary key or unique field | `packages/3-extensions/sql-orm-client/src/collection.ts:2413-2425` throws `ORM.ROW_IDENTITY_MISSING`, reached from `update()` (2015) and `delete()` (2191). No emit-time `@id` requirement exists for SQL (no `PSL_MISSING_ID_FIELD` under `packages/2-sql`), so runtime really is where it fails | Confirmed | +| 53, 59 | MongoDB `_id` mandatory and must be `ObjectId`; `@map("_id")` | `packages/2-mongo-family/2-authoring/contract-psl/src/interpreter.ts:1320-1340` (`PSL_MONGO_ID_REQUIRED`, example spelled `id ObjectId @id @map("_id")`) | Confirmed | +| 59 | The field takes no `@default` | `packages/2-mongo-family/2-authoring/contract-psl/src/mongo-attribute-specs.ts:247-261` — the MongoDB field attributes are `id`, `unique`, `map`, `relation`; there is no `default` | Confirmed | +| 59 | Omit `id` on create and MongoDB assigns it | `packages/2-mongo-family/5-query-builders/orm/src/collection.ts:113,369-374` ("server-assigned `_id`") | Confirmed | +| 59 | `@@map` optional; default collection is the model name with a lowercase first letter | `interpreter.ts:334-335` (`?? lowerFirst(model.name)`), `psl-helpers.ts:8` | Confirmed | +| 103 | Auto-increment value known only after the insert | `control-mutation-defaults.ts:48-55` — `autoincrement()` is a storage default, not an execution generator | Confirmed | +| 106 | UUID generated before the insert | `control-mutation-defaults.ts:70-77` returns `executionGenerator('uuidv7'/'uuidv4')`, i.e. client-side | Confirmed | +| 109, 113 | `@default(uuid())` random, `@default(uuid(7))` sorts by creation time | `control-mutation-defaults.ts:73-80` maps `uuid(7)` → `uuidv7` and bare `uuid()` → `uuidv4`; `packages/1-framework/2-authoring/ids/src/generators.ts:5-6` uses `uniku/uuid/v4` and `uniku/uuid/v7`. v7 is time-ordered and v4 random by RFC 9562; the tree states the versions, not the ordering property | Confirmed | +| 109 | `id String @default(uuid())` is valid | `ids/src/index.ts:57-64` — `uuidv4`/`uuidv7` `applicableCodecIds` include `pg/text@1`, which is what `String` maps to | Confirmed | +| 113 | `@default(uuid(7))` and `@default(uuid())` are both accepted spellings | `control-mutation-defaults.ts:141` `usageSignatures: ['uuid()', 'uuid(4)', 'uuid(7)']` | Confirmed | +| 124 | `@@id([userId, tagId])` composite key | `packages/2-sql/2-authoring/contract-psl/src/sql-attribute-specs.ts:236,472` | Confirmed | +| 128 | "The same model" is shown under many-to-many | `apps/docs/content/docs/orm/data-modeling/relational-databases.mdx:127-133` shows `PostTag` with `postId`/`tagId`, not `UserTag` | **Corrected** → "[Many-to-many](…#many-to-many) writes a model like this one out with its relation fields." | +| 136-146 | Scalar table: `String`→text, `Int`→int4, `BigInt`→int8, `Float`→float8, `Decimal`→numeric, `Boolean`→bool, `DateTime`→timestamptz, `Bytes`→bytea, `Json`→json, `Jsonb`→jsonb | `packages/3-targets/6-adapters/postgres/src/core/control-mutation-defaults.ts:157-196` (`postgresScalarAuthoringTypes`) | Confirmed | +| 140 | `Numeric(10, 2)` takes a precision and scale | same file, `postgresNativeAuthoringTypes.Numeric`, args `precision` and `scale` | Confirmed | +| 146, 148 | `ObjectId` is the only MongoDB-only type | `packages/3-mongo-target/2-mongo-adapter/src/exports/control.ts:25-36` — MongoDB scalars are `String`, `Int`, `Boolean`, `DateTime`, `ObjectId`, `Float`; `ObjectId` is the only one absent from the PostgreSQL set | Confirmed | +| 150 | `Decimal` reaches your code as a string | `packages/3-targets/3-targets/postgres/src/core/codecs.ts:896-905` — `PgNumericCodec.decode` returns `string` | Confirmed | +| 150 | `DateTime` arrives as `Temporal.Instant` | `postgresScalarAuthoringTypes.DateTime` → `pg/timestamptz-temporal@1`; `temporal-codecs.ts:143-155` decodes to `Temporal.Instant` | Confirmed | +| 150 | `Temporal` built into Node.js 26.8.2 and later | `skills/prisma-8/references/contract.md:118` | Confirmed | +| 150 | `temporal-polyfill` package, `import "temporal-polyfill/full/global"` before any query | `contract.md:118`, `docs/reference/error-reference.md:779`, `test/integration/test/setup-temporal.ts:1` | Confirmed | +| 150 | `Bytes` arrives as `Uint8Array` | `codecs.ts:1167-1177` | Confirmed | +| 152 | `Json` and `Jsonb` are two separate types | `control-mutation-defaults.ts:186-193` — separate entries, `pg/json@1` / `pg/jsonb@1` | Confirmed | +| 152 | `.where()` on `Jsonb` is whole-value only; `Json` not comparable at all | `codecs.ts:1401` `PgJsonDescriptor.traits = []`; `codecs.ts:1445` `PgJsonbDescriptor.traits = ['equality']` | Confirmed | +| 152 | Raw SQL for a path query | `scorecard/09-filtering.md:34` — JSON-path filtering is `❌` on PostgreSQL | Confirmed | +| 152 | Extensions add vectors or geometry | `README.md:87-88` (pgvector, postgis) | Confirmed | +| 154 | `name String?`, `tags String[]` | `test/integration/test/sql-orm-client/fixtures/scalar-lists/contract.prisma:11-15` has `tags String[]` verbatim | Confirmed | +| 158-163 | Enum block syntax, `@@type("pg/text@1")`, `Low = "low"` | `skills/prisma-8/references/contract.md:274-287`; `examples/prisma-8-cloudflare-worker/src/prisma/contract.prisma:9` | Confirmed | +| 166 | "`@@type` is required on every enum" | `packages/1-framework/1-core/framework-components/src/shared/framework-authoring.ts:326-348` — when `@@type` is absent, `resolveEnumCodecId` infers the type from the members via `classifyEnumMemberType` (all-string → text, all-integer → int), using each target's `enumInferenceCodecs`. Only an un-inferable member set raises `PSL_ENUM_CANNOT_INFER_TYPE` | **Corrected** → "`@@type` says how the values are stored, here as PostgreSQL `text`. Leave it out and Prisma ORM takes the storage from the members, text for string values and an integer for whole numbers." | +| 166 | `pg/text@1` is the type plus a version, always `@1` today | grep over every `'(pg\|sql\|mongo)/…@N'` literal in `packages/` returns only `@1` | Confirmed | +| 166 | `@@type("mongo/string@1")` on MongoDB | `examples/mongo-demo/src/contract.prisma:4`, `examples/retail-store/src/contract.prisma:4` | Confirmed | +| 199 | `BigInt` arrives as a JavaScript `bigint` | `codecs.ts:645-656` — `PgInt8Codec.decode` returns `bigint` (`JSON.stringify` refusing `bigint` is a JavaScript language fact) | Confirmed | +| 209 | Prisma ORM 7 called the link field the relation scalar field | `apps/docs/content/docs/orm/v7/prisma-schema/data-model/relations/index.mdx:126,132` uses that exact term. Attributed to Prisma ORM 7, not claimed of rc.10 | Confirmed | +| 212, 215-225 | `@relation(fields:, references:)`; `User`/`Post` with `posts Post[]` back-relation | `sql-attribute-specs.ts` `relationFieldSpec`; back-relation matching in `psl-relation-resolution.ts:448-480` | Confirmed | +| 229 | One-to-one: `@unique` on the link field, `profile Profile?` not a list | `psl-relation-resolution.ts:452-475` — a singular back-relation with a non-unique FK raises `PSL_NON_UNIQUE_BACKRELATION`, and a required singular back-relation raises `requiredOneToOneBackrelationDiagnostic` | Confirmed | +| 231 | Implicit many-to-many is gone and `npx prisma contract emit` rejects it | `psl-relation-resolution.ts:431-433` — `PSL_ORPHANED_BACKRELATION` for a list back-relation with no join model | Confirmed | +| 240 | `npm create prisma@latest`, `npx prisma skills sync`, `prisma-8` skill | `skills/prisma-8/SKILL.md` exists; `skills sync` kept per `conventions.md` (published CLI has it, this tree does not) | Confirmed | +| 251 | `contract emit` → `migration plan --name` → `db migrate`; emit replaces `prisma generate` | `skills/prisma-8/references/migrations.md:95` (`contract emit && migration plan --name add_feature`), `quickstart.md:265`, `packages/1-framework/3-tooling/cli/src/migration-cli.ts:6` (`prisma db migrate`), `docs/architecture docs/adrs/ADR 032:150` ("Replaces `prisma generate`") | Confirmed | +| all | Every link target and anchor | `#models`/`#primary-keys`/`#scalar-fields`/`#relations` exist on the page; `psl-syntax.mdx:13` `## A complete contract` and `:183` `## Enums`; `relational-databases.mdx:75` `## One-to-one` and `:107` `## Many-to-many`; `url:` front matter matches for `/orm/extensions`, `/orm/migrations/generating-a-migration`, `/orm/coming-from-prisma-orm-7`, `/orm/reference/raw-queries`, `/orm/fundamentals/reading-data`; `ai/tools/skills.mdx:100` pins `[#available-skills-for-prisma-8]` | Confirmed | + +## Q list + +None. Every claim resolved for or against. + +## Note not acted on + +`page-conventions.md` says type ids such as `pg/text@1` do not belong in prose on these pages. Line 166 explains `pg/text@1` in prose. That is a convention question, not a fact question, so I left it for the operator. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/factcheck2.mongodb.md b/docs/orm-docs-audit/c21-data-modeling/factcheck2.mongodb.md new file mode 100644 index 00000000000..2e60039c5e1 --- /dev/null +++ b/docs/orm-docs-audit/c21-data-modeling/factcheck2.mongodb.md @@ -0,0 +1,79 @@ +Done. Report below. + +## Claims checked + +| Line | Claim | Source that decides it | Verdict | +|---|---|---|---| +| 13 | Contract is `contract.prisma`; a model maps to a collection, a record is a document | `examples/mongo-demo/src/contract.prisma`; `prisma.config.ts` `contract: './src/contract.prisma'` | Confirmed | +| 13 | Connection string in `prisma.config.ts`, passed to `defineConfig` from `@prisma/orm-mongo/config` | `examples/mongo-demo/prisma.config.ts` lines 1-11 (and `mongo-blog-leaderboard/prisma.config.ts`) | Confirmed | +| 13 | No `datasource` block on MongoDB | No `datasource` token anywhere in `packages/1-framework/2-authoring/psl-parser/src/` or `packages/2-mongo-family/2-authoring/` | Confirmed | +| 13 | The data contract page shows the config file | `apps/docs/content/docs/orm/contract-authoring/the-data-contract.mdx:53-55` | Confirmed | +| 16-21 | `id ObjectId @id @map("_id")`, `email String @unique`, `@@map("users")` all valid | `packages/1-framework/3-tooling/cli/src/commands/init/templates/code-templates.ts:196-206` (the mongo starter contract is character-for-character this shape) | Confirmed | +| 24 | In TypeScript the key is always `_id` | `examples/mongo-demo/src/contract.d.ts` `FieldOutputTypes`/`FieldInputTypes`: `User._id`, `Post._id` | Confirmed | +| 24 | `@map("_id")` sets the name in the database and in code | same, plus `contract.json` relation `targetFields: ["_id"]` | Confirmed | +| 24 | `@@map` optional; default is the model name with a lowercase first letter | `packages/2-mongo-family/2-authoring/contract-psl/src/interpreter.ts:254` (`return name ?? lowerFirst(model.name)`), `psl-helpers.ts:6-8` | Confirmed | +| 24 | `@unique` becomes a unique index when you migrate | `interpreter.ts:894-915` builds a `MongoIndex({ unique: true })` per `@unique` field; `examples/mongo-demo/migrations/app/20260409T1030_migration/migration.ts:11` `createIndex('users', [{field:'email'}], { unique: true })` | Confirmed | +| 26 | `ObjectId` is the contract type; ids are plain strings in TypeScript | `packages/3-mongo-target/1-mongo-target/src/core/codec-types.ts:2` — `'mongo/objectId@1': { input: string; output: string }` | Confirmed | +| 26 | `String @db.ObjectId` no longer allowed | `packages/2-mongo-family/2-authoring/contract-psl/test/interpreter.attribute-specs.test.ts:105-118` — `PSL_UNSUPPORTED_FIELD_ATTRIBUTE`, `"@db.ObjectId"` | Confirmed | +| 26 | `@default(auto())` no longer allowed | same file lines 92-98 — `Field "Item.createdAt" uses unsupported attribute "@default"`; `mongo-attribute-specs.ts:249-262` has no `default` in the field namespace | Confirmed | +| 28 | Omit `id` in `create`, MongoDB assigns it | `packages/2-mongo-family/5-query-builders/orm/src/types.ts:223-232` (`Omit<..., '_id'> & Partial>`); `collection.ts:113` doc comment "Returns the input data with the server-assigned `_id`" | Confirmed | +| 28, 31 | `npx prisma orm init` writes `src/prisma/db.ts`; `import { db } from "./prisma/db"` | `packages/1-framework/3-tooling/cli/test/orm/init-scaffold.test.ts:234`; `skills/prisma-8/references/quickstart.md:36,73,84` | Confirmed | +| 28 | `db.orm` is the ORM client, one of the query APIs on `db` | `packages/3-extensions/mongo/src/runtime/mongo.ts:31-32,190-191` (`orm` and `query`) | Confirmed | +| 32-33 | `create({...})` then `.where({ _id }).first()` | `collection.ts:112-116`; `test/integration/orm-ergonomics.test.ts` uses exactly this shape | Confirmed | +| 36 | The name after `db.orm.` is the collection name | `mongo-orm.ts:20-25` keys the client by `TContract['roots']`; `interpreter.ts:1361` `roots[collectionName] = ...`; `examples/mongo-demo/src/contract.json` roots are `users`, `posts` | Confirmed | +| 36 | No `data:` wrapper (Prisma ORM 7 had one) | `create` takes the row directly — confirmed. The Prisma ORM 7 half is not in this tree — see Q1 | Confirmed / Q1 | +| 36 | `.first()` returns one document or `null`; `.all()` returns every matching document | `collection.ts:110-113`; `framework-components/src/execution/async-iterable-result.ts:3` (`PromiseLike`, so `await` gives an array) | Confirmed | +| 40, 42 | Embed vs reference trade-offs, decision table, comments example | General MongoDB modeling advice, nothing in the source contradicts it | Confirmed (not a source claim) | +| 57 | `type` block for embedded data; embedded value has no `_id` and no collection | `interpreter.ts:1364-1380` builds `valueObjects`; `examples/mongo-demo/src/contract.d.ts` `AddressOutput` has no `_id` | Confirmed | +| 74 | `addresses Address[]` stores every address inside the user document | `contract-psl/test/interpreter.test.ts:943-965` "emits many: true for value object array fields" | Confirmed | +| 74, 77-78 | Embedded value passed to `create` as a plain nested object | `orm/test/integration/orm-ergonomics.test.ts:173` `homeAddress: { city: 'SF', country: 'US' }` | **Corrected** | +| 81 | `update` takes a callback returning an array; `u.fieldName` for a top-level field | `collection.ts:130-133`, `field-accessor.ts:222-236`; `orm-ergonomics.test.ts:189` `[u.tags.push('admin'), u.loginCount.inc(5)]` | Confirmed | +| 81 | `u("path.to.field")` for a field inside an embedded document | `field-accessor.ts` `DotPath` (call signature); `orm-ergonomics.test.ts:177` `u('homeAddress.city').set('NYC')` | Confirmed | +| 84-85 | `u("address.city").set(...)`, `u.tags.push(...)`, `u.tags.pull(...)` | `orm-ergonomics.test.ts:141,152,177` | Confirmed | +| 88 | Replace / remove a whole embedded value | `field-accessor.ts` `DotPath` only yields `address.` paths, never bare `address`; whole-value writes go through property access — `orm/test/collection.test.ts:798` `u.homeAddress.set({...})`, `:758` `u.name.unset()` | **Corrected** | +| 88 | `u.addresses.set([ ... ])` replaces a list of embedded values | `field-accessor.ts` `FieldAccessor` includes `ValueObjectFieldKeys` with `FieldExpression.set(value: T)` where `T` is the array | Confirmed | +| 88 | `.where()` filters on top-level model fields only | `orm/src/types.ts:188-201` — `MongoWhereFilter` is keyed by `[ModelName]['fields']`, no dotted paths | Confirmed | +| 91 | `db.query.from("users").match((f) => f("address.city").eq("Berlin")).build()` | `query-builder/src/state-classes.ts:632-650` (`from` takes a root name = collection name); `query-builder/test/builder.test.ts:79` `f('address.city').eq('NYC')`; `builder.ts:817` `build()` | Confirmed | +| 94 | `build()` returns the query, not the rows | `builder.ts:817-840` returns a `MongoQueryPlan`; `skills/prisma-8/references/queries-mongo.md:12,147` "run via `(await db.runtime()).query(plan)`" | Confirmed | +| 94 | 16 MB limit, a write that exceeds it fails | General MongoDB; nothing in the source contradicts it | Confirmed (kept per brief) | +| 100-114 | Reference example: `authorId ObjectId`, `@relation(fields: [authorId], references: [id])`, `posts Post[]` | `examples/mongo-demo/src/contract.prisma` and `mongo-blog-leaderboard/src/contract.prisma` use exactly this | Confirmed | +| 116 | There are no nested writes on MongoDB | `orm/src/types.ts:223-232` — the create input is built from `FieldInputTypes` only; `examples/mongo-demo/src/contract.d.ts` `Post` input has `authorId` and no `author` key | Confirmed | +| 116 | `references: [id]` uses the contract-side name, not `_id` | `examples/mongo-demo/src/contract.prisma` writes `references: [id]`; the emitted `contract.json` relation is `targetFields: ["_id"]` | Confirmed | +| 116, 120 | `.include("author")` adds the related record to every row | `collection.ts:256-322` (reference relations only; embed relations throw `ORM.INCLUDE_UNSUPPORTED`) | Confirmed | +| 119 | `db.orm.posts.create({ title, authorId: user._id })` | Post's input fields in that code block are `title` and `authorId`; matches `CreateInput` | Confirmed | +| 123 | `.include("posts")` on `db.orm.users` gives each user an array of their posts | `examples/mongo-demo/src/contract.json` — `User.relations.posts` is `cardinality: "1:N"` with an `on` clause, so it is includable | Confirmed | +| 123 | Resolving a reference costs a second read; copying fields is a manual trade-off | `skills/prisma-8/references/queries-mongo.md:21` (`.include` lowers to `$lookup`); the rest is general advice | Confirmed | +| 129-146 | Polymorphic contract: `@@discriminator(kind)`, `@@base(Post, "article")` | `examples/mongo-demo/src/contract.prisma`; `mongo-attribute-specs.ts:157-166` — `discriminator` positional is `fieldRef()` (unquoted), `base` is `entityRef()` plus `str()` | Confirmed | +| 148 | `@@discriminator(kind)` takes the field name unquoted | `mongo-attribute-specs.ts:157-159` `fieldRef()` | Confirmed | +| 148 | A variant declares only its own fields | `examples/mongo-demo/src/contract.d.ts` — `Article` carries only `summary` | Confirmed | +| 148 | A variant has no `@@map` of its own | `interpreter.ts:460-470` — a variant *may* carry `@@map`, but only if it names the base's collection; a different one is `PSL_MONGO_VARIANT_SEPARATE_COLLECTION` | **Corrected** | +| 150 | A base query returns all variants; `.variant("Article")` limits to one | `orm/test/integration/polymorphism.test.ts:79-135`; `collection.ts:203-232` | Confirmed | +| 150 | `.variant()` takes the model name, not the discriminator value | `collection.ts:216-229` looks up `model.variants[variantName]` and filters on `variantEntry.value`; `contract.json` `variants: { Article: { value: "article" } }` | Confirmed | +| 150 | All variants sit in one collection | `interpreter.ts:463-468` "Mongo only supports single-collection polymorphism" | Confirmed | +| 150, 153 | Creating through a variant fills in `kind` | `orm/src/types.ts:247-260` (`VariantCreateInput` omits the discriminator field); `polymorphism.test.ts:143-155` asserts the stored doc has `type: 'bug'` | Confirmed | +| 160 | `npx prisma skills sync` exists | `skills/prisma-8/SKILL.md:33,36` | Confirmed | +| 160 | `npm create prisma@latest` projects include the ORM skills | `wip/prisma-src/README.md:37-40` "the agent skills already installed" | Confirmed | +| 169 | `npx prisma contract emit` checks the contract and regenerates types | `templates/quick-reference-mongo.md:71` "Update contract.json and contract.d.ts" | Confirmed | +| 169 | `npx prisma migration plan --name `, `npx prisma db migrate` | `cli/test/orm/migration-plan.test.ts:81`; `examples/mongo-demo/package.json` scripts `"migration:plan": "prisma migration plan"`, `"migration:apply": "prisma db migrate"` | Confirmed | +| 169 | Migrating creates the collections and indexes | `packages/3-mongo-target/1-mongo-target/src/core/migration-factories.ts:212` `createCollection`, `:357`; `examples/mongo-demo/migrations/app/*/migration.ts` `createIndex(...)` | Confirmed | +| 169 | `npm create prisma@latest -- my-app` (the `-- my-app` argument) | Not in the tree — see Q2 | Q | +| 9, 94, 123, 169-172 | All link targets and anchors | Every path resolves under `apps/docs/content/docs/`; `orm/fundamentals/advanced-queries.mdx:292` `## MongoDB: Pipeline builder` → `#mongodb-pipeline-builder`; `ai/tools/skills.mdx:100` pins `[#available-skills-for-prisma-8]` | Confirmed | + +## Corrections made (3) + +1. **Line 88** — `u("address").set({ ... })` / `u("address").unset()` → **`u.address.set({ ... })` / `u.address.unset()`**. The callable form of the field accessor is typed by `DotPath`, which only produces paths *into* an embedded value (`address.city`), never the bare field name. Whole-value writes go through property access, which is what every test uses. +2. **Line 78** — `create({ name: "Ada", address })` → **`create({ name: "Ada", tags: [], address })`**. The create input is `Omit` with no `Partial`, so every non-`_id` field is a required key. The `tags String[]` field added to that code block's model in an earlier round made the example a type error. +3. **Line 148** — "A variant declares no `@@map` of its own and uses its base's collection." → **"A variant uses its base's collection, so it needs no `@@map` of its own."** A variant may carry `@@map`; it is only rejected when it names a *different* collection than the base. + +## Q list + +- **Q1 (line 36)**: "without the `data:` wrapper Prisma ORM 7 used". The Prisma ORM 8 half is confirmed. Nothing in the rc.10 export describes Prisma ORM 7's client shape. Searches: `grep -rn "data: {" skills/`, `grep -rn "prisma 7|Prisma 7" -i skills/prisma-8/references/upgrade-app.md`, `grep -n "data:" skills/prisma-8/references/upgrade-app.md` — all empty. Left as-is. +- **Q2 (line 169)**: the `-- my-app` argument in `npm create prisma@latest -- my-app`. `wip/prisma-src/README.md:37` shows `npm create prisma` with no argument, and `create-prisma` is a separate repo (`projects/consolidate-clis/current-state.md:13` says so explicitly). Searches: `grep -rn "npm create prisma@latest" .`, `grep -rn "create prisma|create-prisma" --include='*.md' --include='*.ts' --include='*.json' .`. Left as-is. + +## No test coverage noted + +- The "no nested writes on MongoDB" claim rests on the create-input type (`orm/src/types.ts:223-232`) and the emitted `contract.d.ts`, not on a test that asserts a rejection. + +## Line count + +`wip/c21/dm/fc2/mongodb.mdx`: **172 before, 172 after**. Plain-language check prints `clean`; the extra banned-word grep is clear (the one hit is "Aggregate" matching `gate`). \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/factcheck2.relational-databases.md b/docs/orm-docs-audit/c21-data-modeling/factcheck2.relational-databases.md new file mode 100644 index 00000000000..1bb9f8a482e --- /dev/null +++ b/docs/orm-docs-audit/c21-data-modeling/factcheck2.relational-databases.md @@ -0,0 +1,53 @@ +Fact re-check complete for `wip/c21/dm/fc2/relational-databases.mdx`. One correction made. + +## Claims checked + +| Line | Claim | Source that decides it | Verdict | +| --- | --- | --- | --- | +| 11 | `contract.prisma` is the contract source; `contract emit` checks it and regenerates TypeScript types; does not touch the database | `skills/prisma-8/references/quickstart.md:283,285,287`; `SKILL.md:34` | Confirmed | +| 11 | `db` created once in `src/prisma/db.ts`, written by `prisma orm init` | `skills/prisma-8/references/quickstart.md:36,107,133`; `references/contract.md:69`; `cli/src/commands/init/templates/readme-postgres.md:29` | Confirmed | +| 11 | `db.orm` holds models by model name; `public` is the PostgreSQL schema | `sql-orm-client/test/model-types.test-d.ts:23`; namespace fixtures in `sql-orm-client/test/orm-namespaced.test.ts` | Confirmed | +| 15-22 | FK side declares scalar + relation field with `@relation(fields:, references:)` | `contract-psl/src/sql-attribute-specs.ts:434`; `references/contract.md:97-110` | Confirmed | +| 27 | Both fields must agree in nullability, else emit reports an error | `contract-psl/src/interpreter.ts:702-716` (`PSL_RELATION_NULLABILITY_MISMATCH`) | Confirmed | +| 29 | `onDelete`/`onUpdate` become `ON DELETE`/`ON UPDATE`; write neither and neither clause is emitted | `postgres/test/migrations/planner.referential-actions.test.ts:120-124,165-173` | Confirmed | +| 31-35 | The five names `Cascade`, `Restrict`, `SetNull`, `SetDefault`, `NoAction` and their meanings | `contract-psl/src/psl-relation-resolution.ts:25-29`; `sql-attribute-specs.ts:420-424`; planner test `:165-173` | Confirmed | +| 35 | `NoAction` is the same as writing nothing | `2-sql/9-family/src/core/psl-contract-infer/relation-inference.ts:9-10,180-181` (`DEFAULT_ON_DELETE`/`DEFAULT_ON_UPDATE = 'noAction'`, omitted on round-trip) | Confirmed | +| 37 | Two relations between the same pair need a name on both sides, `@relation("Name")` form accepted | `psl-relation-resolution.ts:439-444` (`PSL_AMBIGUOUS_BACKRELATION`, message names both forms); `contract-psl/test/interpreter.relations.test.ts:467-494` | Confirmed | +| 43-56 | One-to-many model shapes, `Int @id @default(autoincrement())`, `String @unique` | `test/integration/test/authoring/side-by-side/postgres/contract.prisma:4` | Confirmed | +| 58 | Self-relation is an ordinary one-to-many; one relation between a model and itself needs no name | `psl-relation-resolution.ts:389-398` (matching is by model pair; a single match needs no name). No test covers exactly one unnamed self-relation — the two tests present (`interpreter.relations.test.ts:428,467`) use a name or two relations. Stated from the code. | Confirmed | +| 60 | Mirror field stores nothing and can be left out | `references/contract.md:97` ("Add the relation only on the owning side") | Confirmed | +| 60 | `db.orm.public.Post.where({ authorId: user.id }).all()`; `.all()` runs the query and returns every row | `test/integration/test/sql-orm-client/mn-nested-write.test.ts:291-296`; `include.test.ts:85` | Confirmed | +| 65-70 | `import { db } from "./prisma/db"`; `create` with no `data:` wrapper | `quickstart.md:84`; `mn-nested-write.test.ts:91-96` | Confirmed | +| 73 | `.include("author").all()` nests the related record | `include.test.ts:105-124` | Confirmed | +| 79-93 | One-to-one: `@unique` on the foreign key | `psl-relation-resolution.ts:452-462` (`PSL_NON_UNIQUE_BACKRELATION`) | Confirmed | +| 95 | Mirror field `profile Profile?` needs no `@relation`, must be optional, is singular because `userId` is unique; `.include("profile")` | `psl-relation-resolution.ts:452-473`; `include.test.ts:124` | Confirmed | +| 111-133 | Explicit join model, `@@id([postId, tagId])` | `fixtures/mn-psl/contract.prisma` (identical shape); `psl-relation-resolution.ts:232-289` | Confirmed | +| 136 | `Post.tags` typed `Tag[]` not `PostTag[]` | `fixtures/mn-psl/contract.prisma:25,33` | Confirmed | +| 136 | Second join table needs the name on `Post.tags` and on `PostTag.post` | `psl-relation-resolution.ts:237-239` ("A relation name on the list field pins the parent-side FK relation"); `:417-419` | Confirmed | +| 141-143 | `.where(...).update({ tags: (t) => t.connect([...]) })` | `mn-nested-write.test.ts:291-296` | Confirmed | +| 146 | `t.create` / `t.connect` / `t.disconnect`, each accepting an array | `mn-nested-write.test.ts:95,255,296` | Confirmed | +| 146 | Leave the list fields out and write `PostTag` rows yourself via `db.orm.public.PostTag.create(...)` | `sql-orm-client/test/model-types.test-d.ts:23` (junction is its own collection) | Confirmed | +| 146 | `addedAt DateTime @default(now())` on the join model | `postgres/test/scalar-type-parity.test.ts:98-127` (`DateTime` is a PostgreSQL scalar); `examples/prisma-8-demo/.../contract.prisma:31`; extra non-key columns do not break join-table recognition (`psl-relation-resolution.ts:255-289`) | Confirmed | +| 148 | Implicit many-to-many rejected with `PSL_ORPHANED_BACKRELATION` | `psl-relation-resolution.ts:430-435`; `references/contract.md:139` | Confirmed | +| 155-176 | `@@discriminator(type)` and `@@base(Task, "bug")` quoting; variant declares no `@id`; variant has every base field | `sql-attribute-specs.ts:381-389`; `fixtures/polymorphism/contract.prisma` | Confirmed | +| 180 | `.variant("Bug")` takes the model name; `"bug"` is a type error; create fills in the discriminator | `sql-orm-client/src/types.ts:553-562` (`VariantNames` = variant model-name keys); `src/collection.ts:394-412` | Confirmed | +| 186-195 | Base read returns every variant, each row carrying only its own variant's fields; `switch (task.type)` reaches `task.severity` / `task.targetRelease` | `sql-orm-client/src/types.ts:526-546` (`VariantRow` is a discriminated union over the discriminator literal); `test/integration/.../polymorphism.test.ts:74` | Confirmed | +| 197 | No `@@map` shares the base table with nullable columns; own `@@map` gives its own table named by the string, columns can be `NOT NULL`, a join to read | `contract-psl/src/interpreter.ts:1975-1990` and `:1895-1970`; `polymorphism.test.ts:29-50,53-56` | Confirmed | +| 197 | In Prisma ORM 7, `@@map` only renamed a table | Prisma ORM 7 behaviour, stated as a version contrast. Not in the rc.10 tree. | Q (see below) | +| 203 | Skills note, `npx prisma skills sync`, `prisma-8` skill covers relation modeling | `skills/prisma-8/SKILL.md:61`; conventions.md (rc.13 CLI) | Confirmed | +| 212-214 | Link targets and anchors | `apps/docs/content/docs/orm/fundamentals/relations-and-joins.mdx:47,98,162`; `orm/data-modeling/index.mdx`, `mongodb.mdx`; `orm/migrations/generating-a-migration.mdx`; `ai/tools/skills.mdx:100` | Confirmed | +| 213 | `migration plan --name ` then `db migrate` applies it | `quickstart.md:265`; `references/migrations.md:12,48,96` | **Corrected** | + +## Correction made + +Line 213: `migration plan` writes a migration package (a directory with `migration.ts`, `ops.json`, `migration.json` under `migrations/app/_/`), not a single file (`references/migrations.md:45,73,116`; `quickstart.md:57`). Text now reads "which writes the migration files". + +## Q list + +- **"In Prisma ORM 7, `@@map` only renamed a table."** Searched the rc.10 tree for any Prisma 7 comparison of `@@map`: `grep -rn "@@map" skills/prisma-8/references/contract.md`, `grep -rn -i "prisma 7" skills/prisma-8/`, `grep -rn "@@map" skills/prisma-8/upgrading/`. The tree documents rc.10's own `@@map` behaviour (variant storage choice, `references/contract.md:215`) but says nothing about Prisma ORM 7's. Left as written. + +## Note for the operator + +`skills/prisma-8/references/contract.md:117` says "the default is `Restrict`" for `onDelete`/`onUpdate`. The code contradicts it: no clause is emitted when neither is given (`planner.referential-actions.test.ts:120-124`) and the round-trip default is `noAction` (`relation-inference.ts:9-10`). The page follows the code. The skill prose looks stale. + +Line count: 214 before, 214 after. \ No newline at end of file From b80e23df024b98781bc8920d2bf6d35faafd1388 Mon Sep 17 00:00:00 2001 From: willbot Date: Mon, 14 Sep 2026 17:35:36 +0200 Subject: [PATCH 08/15] docs: remove the data-modeling review reports before merge They stay in the branch history at 19138ba09. Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- .../c21-data-modeling/conventions.md | 24 ---- .../c21-data-modeling/factcheck1.index.md | 80 ------------- .../c21-data-modeling/factcheck1.mongodb.md | 53 --------- .../factcheck1.relational-databases.md | 83 ------------- .../c21-data-modeling/factcheck2.index.md | 58 --------- .../c21-data-modeling/factcheck2.mongodb.md | 79 ------------- .../factcheck2.relational-databases.md | 53 --------- .../c21-data-modeling/index.fix1.md | 48 -------- .../c21-data-modeling/index.fix2.md | 56 --------- .../c21-data-modeling/index.fix3.md | 28 ----- .../c21-data-modeling/index.fix4.md | 45 ------- .../c21-data-modeling/index.round1.md | 67 ----------- .../c21-data-modeling/index.round2.md | 111 ------------------ .../c21-data-modeling/index.round3.md | 86 -------------- .../c21-data-modeling/index.round4.md | 70 ----------- .../c21-data-modeling/mongodb.fix1.md | 54 --------- .../c21-data-modeling/mongodb.fix2.md | 45 ------- .../c21-data-modeling/mongodb.fix3.md | 35 ------ .../c21-data-modeling/mongodb.fix4.md | 49 -------- .../c21-data-modeling/mongodb.round1.md | 65 ---------- .../c21-data-modeling/mongodb.round2.md | 90 -------------- .../c21-data-modeling/mongodb.round3.md | 71 ----------- .../c21-data-modeling/mongodb.round4.md | 76 ------------ .../c21-data-modeling/page-conventions.md | 15 --- .../relational-databases.fix1.md | 56 --------- .../relational-databases.fix2.md | 54 --------- .../relational-databases.fix3.md | 48 -------- .../relational-databases.fix4.md | 48 -------- .../relational-databases.round1.md | 69 ----------- .../relational-databases.round2.md | 73 ------------ .../relational-databases.round3.md | 72 ------------ .../relational-databases.round4.md | 76 ------------ 32 files changed, 1937 deletions(-) delete mode 100644 docs/orm-docs-audit/c21-data-modeling/conventions.md delete mode 100644 docs/orm-docs-audit/c21-data-modeling/factcheck1.index.md delete mode 100644 docs/orm-docs-audit/c21-data-modeling/factcheck1.mongodb.md delete mode 100644 docs/orm-docs-audit/c21-data-modeling/factcheck1.relational-databases.md delete mode 100644 docs/orm-docs-audit/c21-data-modeling/factcheck2.index.md delete mode 100644 docs/orm-docs-audit/c21-data-modeling/factcheck2.mongodb.md delete mode 100644 docs/orm-docs-audit/c21-data-modeling/factcheck2.relational-databases.md delete mode 100644 docs/orm-docs-audit/c21-data-modeling/index.fix1.md delete mode 100644 docs/orm-docs-audit/c21-data-modeling/index.fix2.md delete mode 100644 docs/orm-docs-audit/c21-data-modeling/index.fix3.md delete mode 100644 docs/orm-docs-audit/c21-data-modeling/index.fix4.md delete mode 100644 docs/orm-docs-audit/c21-data-modeling/index.round1.md delete mode 100644 docs/orm-docs-audit/c21-data-modeling/index.round2.md delete mode 100644 docs/orm-docs-audit/c21-data-modeling/index.round3.md delete mode 100644 docs/orm-docs-audit/c21-data-modeling/index.round4.md delete mode 100644 docs/orm-docs-audit/c21-data-modeling/mongodb.fix1.md delete mode 100644 docs/orm-docs-audit/c21-data-modeling/mongodb.fix2.md delete mode 100644 docs/orm-docs-audit/c21-data-modeling/mongodb.fix3.md delete mode 100644 docs/orm-docs-audit/c21-data-modeling/mongodb.fix4.md delete mode 100644 docs/orm-docs-audit/c21-data-modeling/mongodb.round1.md delete mode 100644 docs/orm-docs-audit/c21-data-modeling/mongodb.round2.md delete mode 100644 docs/orm-docs-audit/c21-data-modeling/mongodb.round3.md delete mode 100644 docs/orm-docs-audit/c21-data-modeling/mongodb.round4.md delete mode 100644 docs/orm-docs-audit/c21-data-modeling/page-conventions.md delete mode 100644 docs/orm-docs-audit/c21-data-modeling/relational-databases.fix1.md delete mode 100644 docs/orm-docs-audit/c21-data-modeling/relational-databases.fix2.md delete mode 100644 docs/orm-docs-audit/c21-data-modeling/relational-databases.fix3.md delete mode 100644 docs/orm-docs-audit/c21-data-modeling/relational-databases.fix4.md delete mode 100644 docs/orm-docs-audit/c21-data-modeling/relational-databases.round1.md delete mode 100644 docs/orm-docs-audit/c21-data-modeling/relational-databases.round2.md delete mode 100644 docs/orm-docs-audit/c21-data-modeling/relational-databases.round3.md delete mode 100644 docs/orm-docs-audit/c21-data-modeling/relational-databases.round4.md diff --git a/docs/orm-docs-audit/c21-data-modeling/conventions.md b/docs/orm-docs-audit/c21-data-modeling/conventions.md deleted file mode 100644 index 75c20b9c746..00000000000 --- a/docs/orm-docs-audit/c21-data-modeling/conventions.md +++ /dev/null @@ -1,24 +0,0 @@ -# Shared conventions for the C21 plain-language pass (carried from the fundamentals and reference sections) - -Verified facts (rc.10 source at wip/prisma-src/, commit cfccb09be2). Use these words on every page; do not restate them differently. - -- "contract": on first use on a page, say: "your contract, the `contract.prisma` file that replaced `schema.prisma`". After that, "contract" alone. -- `db`: "`db` is the client you create once in `src/prisma/db.ts`; `prisma orm init` writes that file." Examples import it as `import { db } from "./prisma/db"` from a file directly inside `src/`. -- `db.orm.public.User`: "`db.orm` holds your models by model name; `public` is the PostgreSQL schema (the namespace your tables live in, `public` unless you set one)." On MongoDB there is no schema segment and the accessor is the collection name: `db.orm.users`. -- `db.sql.public.user`: the SQL query builder, keyed by table name. The table name is the model name with a lowercase first letter unless the model sets `@@map`. Column names are the field names unless `@map`. Say this once where `db.sql` first appears. -- `db.raw.sql`: raw SQL as a template string. `db.runtime()` is the connection that runs a built query: `db.runtime().query(plan)` for rows, `db.runtime().execute(plan)` for a count. On PostgreSQL `db.runtime()` is synchronous; on MongoDB it must be awaited: `(await db.runtime()).query(plan)`. State this where `db.runtime()` first appears on a page. -- "plan": avoid the word. Say "the built query" or show the code. Every raw or builder query ends with `.build()` and is then passed to `query()` or `execute()`. -- `.all()`, `.first()`, `.create(...)`: "you `await` the whole chain; the last call says what you want back". Do not use "terminal". -- `.first()` returns `null` when nothing matches. `.all().firstOrThrow()` throws an error with code `RUNTIME.NO_ROWS`. -- `emit`: "`prisma contract emit`, which you run after every change to the contract". Never a bare "emit". -- Scaffold command: `npm create prisma@latest -- my-app` (never `npx create-prisma@latest` or the bare package name). Bun: `bun create prisma@latest my-app`. -- Skills note at the bottom of pages: keep, but say "Projects created with `npm create prisma@latest` include the Prisma ORM skills for your coding agent". -- `conflictOn: { email: "..." }` takes a unique column with its value; if a row with that value exists, `update` is applied to it. Leave it out to use the primary key. -- Nested writes that work: `create`, `connect`, `disconnect` (`posts: (p) => p.create([...])`). `connectOrCreate`, nested update/upsert/delete, and `set` do not exist; link /orm/coming-from-prisma-orm-7#not-available-yet. -- Transaction options (`isolationLevel`, `timeout`, `maxWait`) and nested transactions do not exist. Prisma ORM 7's interactive `$transaction(async (tx) => ...)` maps directly to `db.transaction(async (tx) => ...)`. -- `create-prisma` mentions of "skills": "Prisma ORM skills, instruction files for coding agents". -- Type names in raw SQL (`pg/int4@1`): "the PostgreSQL type plus a version, always `@1` today"; prefer taking the type from the table: `db.sql.public.user.columns.id`. There is no published list; the raw-queries page shows the ids a bare JavaScript value gets. -- Errors: "an error with code `X`" (it is `error.code`). -- Do not use: terminal, codec, envelope, facade, lane, junction model/table (say "join table" / "model for the join table"), surface, plan, "rides", "fan out", "drop one level", "field proxy" (say "the `p` argument"), "lambda" (say "callback"). -- `npx prisma skills sync` exists in the published CLI (8.0.0-rc.13, newer than the rc.9 source tree): "in an existing project, run `npx prisma skills sync`". Do not drop it because the rc.9 tree lacks it. -- The serverless client (`postgresServerless(...)` from `@prisma/orm-postgres/serverless`) has no `db.orm`, `runtime()`, or `transaction()`; it runs SQL builder and raw queries only, reading rows 100 at a time. Never show `db.orm` on it. diff --git a/docs/orm-docs-audit/c21-data-modeling/factcheck1.index.md b/docs/orm-docs-audit/c21-data-modeling/factcheck1.index.md deleted file mode 100644 index 29e6fabb95e..00000000000 --- a/docs/orm-docs-audit/c21-data-modeling/factcheck1.index.md +++ /dev/null @@ -1,80 +0,0 @@ -Done. Page: `/Users/will/Projects/prisma/web/.claude/worktrees/pr-conflicts-review-fb93f7/apps/docs/content/docs/orm/data-modeling/index.mdx` - -## 1. Wording changes (banned terms + page conventions) - -| Line | Old | New | -|---|---|---| -| 13 | "you define this structure in a `contract.prisma` file. This file becomes the shared contract between your application code, your database migrations, and your developer tools." | "you define this structure in your contract, the `contract.prisma` file that replaced `schema.prisma`. Prisma ORM derives your TypeScript types, your migrations, and the client's configuration from it." | -| 15 | "If you are coming from Prisma ORM 7, `contract.prisma` plays a similar role to the `schema.prisma` file you used before." | deleted (the required first-use phrasing now covers it) | -| 17 | "This page introduces the four building blocks of every Prisma ORM contract:" | "Every contract is built from four things:" | -| 24 | "...the [relational] and [MongoDB] guides cover that in depth." | "All four apply to every database. How you model relations is where databases differ, so continue with [relational data modeling] or [MongoDB data modeling]." | -| 38 | "its own lifecycle" | "its own history" | -| 61 | "`ObjectId` is the idiomatic type for it" | "it must be an `ObjectId`" | -| 76 | "an opaque number" | "a meaningless number" | -| 124 (code comment) | "the idiomatic MongoDB key" | "the MongoDB key" | -| 143 | "Avoid composite keys on ordinary models ... has to carry all of the key's fields" | "Avoid composite keys for ordinary models ... has to store all of the key's fields" (`keys on` and `carry` both hit the checker) | -| 205 | "Widening a type later is a migration; the wider type today is free." | split into two sentences | -| 215–216 | "a real column or document field"; "It stores nothing itself; it tells Prisma ORM how to navigate" | "The field that stores the other record's primary key..."; "It stores nothing in the database. It tells Prisma ORM how to follow the connection in a query." | -| 236 | "junction tables" | "the model for the join table" | -| 241 | "Projects scaffolded with `create-prisma@latest` install..." | "Projects created with `npm create prisma@latest` include the [Prisma ORM skills](...) for your coding agent. In an existing project, run `npx prisma skills sync`." | -| 251 | "single-collection polymorphism" | "polymorphic collections" (matches the target page's heading) | -| 252 | "once the schema is in place" | "once your contract is in place" | - -No heading was renamed, so no anchors were pinned. - -## 2. Claims checked - -| Line | Claim | Source | Verdict | -|---|---|---|---| -| 13 | contract is the shared contract for app code, migrations, tools | `skills/prisma-8/references/contract.md:5` ("derives types, migrations, and runtime configuration") | **Corrected** → now names TypeScript types, migrations, client configuration | -| all | examples need `// use prisma-8` | `packages/1-framework/3-tooling/language-server/src/schema-directive.ts:1`; no reference in `psl-parser/src/` or either `contract-psl/src/` | **Confirmed** (not needed). It is a language-server marker. The contract-authoring pages show it only on whole-file blocks (`psl-syntax.mdx:15,59`); this page's blocks are fragments, matching that convention. No change. | -| 32,48 | `Int @id @default(autoincrement())` | `cli/src/commands/init/templates/code-templates.ts:106,172` | Confirmed | -| 32 | `autoincrement()` exists as a default function | `3-targets/6-adapters/postgres/src/core/control-mutation-defaults.ts:131-137` | Confirmed | -| — | what `autoincrement()` becomes on PostgreSQL | `postgres/src/core/migrations/planner-ddl-builders.ts:61-71` → `SERIAL` (int4), `BIGSERIAL`, `SMALLSERIAL`; no `DEFAULT` clause | Confirmed. Page's "the database assigns the value, so you only know it after the insert" holds. | -| 34 | `String?` optional modifier | `code-templates.ts:172-180` | Confirmed | -| 55,127 | `ObjectId @id @map("_id")` | `2-mongo-family/2-authoring/contract-psl/src/interpreter.ts:1322-1335` (model *must* declare it) | Confirmed | -| 57,129 | `@@map("users")` for the collection | `mongo-attribute-specs.ts:143,249`; `code-templates.ts:196-210` | Confirmed | -| 44 | "Every model needs one" | SQL: `contract-psl/src/interpreter.ts:763` allows no primary key, but `sql-orm-client/src/collection.ts:2422` — update/delete require a primary key or unique. MongoDB: required. | **Corrected** → "Give every model one, because Prisma ORM needs a primary key to update or delete a record." | -| 61 | MongoDB primary key is `_id`, type `ObjectId` | `mongo interpreter.ts:1327-1335` | Confirmed (strengthened from "idiomatic" to "must be") | -| 71 | `String @id` (natural key) | `contract-psl/src/psl-field-resolution.ts:642,651` (only lists and optionals are rejected) | Confirmed | -| 89,94 | `String @unique` | `code-templates.ts:173` | Confirmed | -| 117 | `String @id @default(uuid())` | `control-mutation-defaults.ts:140-141` (`uuid()`, `uuid(4)`, `uuid(7)`); `1-framework/2-authoring/ids/src/index.ts:62-64` — `uuidv4` applies to `pg/text@1` | Confirmed | -| 117 | UUID "generated before the insert" | `control-mutation-defaults.ts:73-80` returns an *execution* generator, not a column default | Confirmed | -| 141 | `@@id([userId, tagId])` | `interpreter.ts:944-953`; `examples/prisma-8-demo/.../contract.prisma` `PostTag` | Confirmed | -| 172 | `String[]` scalar list on PostgreSQL | `psl-field-resolution.ts:497-500` requires the `scalarList` capability; `3-targets/6-adapters/postgres/src/core/adapter.ts:38` and `descriptor-meta.ts:192` report `scalarList: true` | Confirmed | -| 151 | `String` → text | `control-mutation-defaults.ts:166-169` | Confirmed | -| 152 | `Int` 32-bit | same, `pg/int4@1` / `int4` | Confirmed | -| 153 | `BigInt` 64-bit | same, `pg/int8@1` / `int8` | Confirmed | -| 154 | `Float` | same, `pg/float8@1` / `float8` | Confirmed | -| 155 | `Decimal` = PostgreSQL `numeric`, value is a decimal string | `control-mutation-defaults.ts:184-187`; `postgres/src/core/codecs.ts:896-901` (app type `string`) | Confirmed | -| 155 | `Numeric(10, 2)` written as the type | `control-mutation-defaults.ts:228-244` (`Numeric` native type, `precision`/`scale` args) | Confirmed | -| 155 | PostgreSQL rounds to scale and rejects over precision | Not in the Prisma source (PostgreSQL behaviour) | **Q1** — kept, see below | -| 156 | `Boolean` | `control-mutation-defaults.ts:170-173` (`pg/bool@1` / `bool`) | Confirmed | -| 157 | `DateTime` = timestamp | `control-mutation-defaults.ts:188-191` → `pg/timestamptz-temporal@1` / `timestamptz`; `temporal-codecs.ts:139-143` app type `Temporal.Instant` | **Corrected** → cell now says "Timestamp, PostgreSQL `timestamptz`" and the note says the value reaches your code as a `Temporal.Instant` | -| 158 | `Bytes` = `bytea`, value is `Uint8Array` | `codecs.ts:1164-1178` | Confirmed | -| 159 | `Json` = native `json` | `control-mutation-defaults.ts:192-195` | Confirmed | -| 160 | `Jsonb` = `jsonb`, used by `field.json()` and value objects | `control-mutation-defaults.ts:196-199`; `postgres/src/core/authoring.ts:721-726` (`json` field preset → `pg/jsonb@1`); `6-adapters/postgres/src/exports/control.ts:15` `valueObjectStorageType: 'Jsonb'` | Confirmed | -| 160 | "what a `Json` field meant before 0.17" | page-conventions bans pre-release version references | **Corrected** → clause removed | -| 161 | `ObjectId` = MongoDB document identifier | `mongo contract-psl/src/interpreter.ts:981` | Confirmed | -| 165 | `Jsonb` filters by whole-value equality; `Json` has no comparison | `postgres/src/core/codecs.ts:1417-1421` (`Jsonb` traits `['equality']`) and `1373-1377` (`Json` traits `[]`); `sql-orm-client/src/types.ts:361-395` maps `eq`/`neq`/`in`/`notIn` to `equality`, `gt`/`lt`/… to `order`, `like` to `textual`; `sql-orm-client/src/filters.ts:81-85` throws without the trait | Confirmed | -| 167 | extensions add types such as vectors or geometry | `examples/prisma-8-demo/.../contract.prisma` `pgvector.Vector(1536)`; `examples/prisma-8-postgis-demo/.../contract.prisma` `postgis.Geometry(4326)` | Confirmed | -| 171 | `?` optional | `code-templates.ts:173-175` | Confirmed | -| 218,224 | `@relation(fields:, references:)` and what the two arguments mean | `code-templates.ts:186-188`; `2-sql/2-authoring/contract-psl/src/psl-relation-resolution.ts:431-432` | Confirmed | -| 228-232 | relations come in three shapes, incl. many-to-many | `psl-relation-resolution.ts:308-323,400-418` — many-to-many is recognised, but only through an explicit join model whose `@@id([...])` lists exactly its two foreign-key columns; implicit many-to-many is rejected with `PSL_ORPHANED_BACKRELATION` ("or use an explicit join model for many-to-many") | Confirmed as a shape. The requirement is carried by the wording change on line 236 ("the model for the join table") and belongs to the relational page. | -| 241 | `create-prisma@latest` scaffolds and installs skills | conventions.md mandates `npm create prisma@latest`; `ai/tools/skills.mdx:104` | **Corrected** to the mandated sentence | -| 241 | `prisma-8` skill covers contract authoring | `wip/prisma-src/skills/` contains exactly one skill, `prisma-8`, with `references/contract.md` | Confirmed | -| 241 | `/ai/tools/skills#available-skills-for-prisma-8` | `apps/docs/content/docs/ai/tools/skills.mdx:100` declares `[#available-skills-for-prisma-8]` | Confirmed | -| 24,236,250 | `/orm/data-modeling/relational-databases` | file exists, `url:` matches | Confirmed | -| 24,237,251 | `/orm/data-modeling/mongodb` | file exists, `url:` matches | Confirmed | -| 165 | `/orm/reference/raw-queries` | file exists, `url:` matches | Confirmed | -| 252 | `/orm/fundamentals/reading-data` | file exists, `url:` matches | Confirmed | - -## 3. Q list - -**Q1 (line 155, now in the note under the table):** "PostgreSQL then rounds stored values to that scale and rejects values that exceed the precision." This is PostgreSQL server behaviour, not Prisma behaviour, and the Prisma source neither states nor contradicts it. Searches run inside `wip/prisma-src`: `grep -rn "precision" packages/3-targets/6-adapters/postgres/src/core/control-mutation-defaults.ts` (only the arg schema, `minimum: 1`/`minimum: 0`), `grep -rn "class PgNumericCodec" -A 14 packages/3-targets/3-targets/postgres/src/core/codecs.ts` (encode passes the string through unchanged, no rounding), `grep -rn "numeric" packages/3-targets/3-targets/postgres/src/core/psl-infer/postgres-type-map.ts`. Left in the page for the operator to decide. - -Also worth flagging, not a Q: `Decimal` and `DateTime` have no entry in `postgres-type-map.ts`, which is the *introspection* direction only (`numeric` → `Numeric`, `timestamptz` → `Timestamptz`). The authoring direction in `control-mutation-defaults.ts` does define both, which is what the page describes. - -## 4. Line count - -252 before, 252 after. `check-plain.sh` prints `plain-language check: clean`, and the extra page-conventions word list has no hits. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/factcheck1.mongodb.md b/docs/orm-docs-audit/c21-data-modeling/factcheck1.mongodb.md deleted file mode 100644 index bf8e62407b2..00000000000 --- a/docs/orm-docs-audit/c21-data-modeling/factcheck1.mongodb.md +++ /dev/null @@ -1,53 +0,0 @@ -Done. Page: `/Users/will/Projects/prisma/web/.claude/worktrees/pr-conflicts-review-fb93f7/apps/docs/content/docs/orm/data-modeling/mongodb.mdx` - -## 1. Wording replacements - -| Line | Old | New | -| --- | --- | --- | -| 11 | "…is the core of MongoDB data modeling, and it is what this page focuses on." | "…is the core of MongoDB data modeling." (commentary about the docs cut) | -| 15 | "`ObjectId` is the idiomatic type for it" | "`ObjectId` is the type MongoDB expects for it" | -| 32 | "You cannot query it on its own, and it rides along on every read of the parent." | "You cannot read it without its parent, and it comes back on every read of the parent." | -| 51 | semicolon joining two clauses | split into two sentences | -| 85 | "`Cart.items` lives inside the cart document… items; every read" | "`Cart.items` is stored inside the cart document… items. Every read" | -| 118 | "a real cost on hot paths, so document models often denormalize: embed a small copy…" | "a real cost on reads that run often, so document models often keep a small copy of the fields a read needs inside the parent. A comment stores its author's name, while the full author record stays in its own collection." | -| 122 | "so it is idiomatic to store documents of different kinds…" | "The usual choice is to store documents of different kinds…" | -| 150 | "you can narrow to one variant in queries" | "`db.orm.posts.variant(\"Article\")` limits the query to one variant." | -| 156 | "Projects scaffolded with `create-prisma@latest` install…" | "Projects created with `npm create prisma@latest` include the [Prisma ORM skills](…) for your coding agent. In an existing project, run `npx prisma skills sync`." | - -No headings renamed, no anchors changed. - -## 2. Claims checked - -| Line | Claim | Source | Verdict | -| --- | --- | --- | --- | -| 15–24 | PSL example needs no `// use prisma-8` or provider marker | `apps/docs/content/docs/orm/contract-authoring/psl-syntax.mdx:16,60` puts the marker only on the two full-contract tab examples, not on fragments | Confirmed | -| 19 | `id ObjectId @id @map("_id")` | `wip/prisma-src/examples/mongo-demo/src/contract.prisma`; `psl-syntax.mdx:157-165` | Confirmed | -| 22 | `@@map("users")` names the collection | `2-mongo-family/2-authoring/contract-psl/src/interpreter.ts:240-254` | Confirmed | -| 26 | `@@map` requirement / default collection name | same, `return name ?? lowerFirst(model.name)`; `psl-helpers.ts:6-9` | **Corrected** — now says "`@@map` is optional. Without it the collection takes the model name with a lowercase first letter, so `user`." | -| 32 | embedded data cannot be read on its own | see line 87 evidence | Corrected (wording; see below) | -| 51–67 | `type Address` used as `address Address?` | `examples/mongo-demo/src/contract.prisma`, identical model | Confirmed | -| 71–83 | `type CartItem` used as `items CartItem[]` | `contract-psl/test/derive-json-schema.test.ts` "handles value object array field"; `mongo-attribute-specs.ts` allows list value-object fields | Confirmed | -| 87 | "An embedded `Address` has no `_id` and cannot be queried on its own" | no `_id`: `derive-json-schema.test.ts:373` asserts `expect(nestedProps).not.toHaveProperty('_id')`. Filtering: the pipeline builder **does** accept paths into embedded documents — `5-query-builders/query-builder/test/field-accessor.test-d.ts:70,108,216` (`f('address.city').eq('London')`); the ORM `.where()` object form keys are top-level model fields only (`orm/src/types.ts:188-202`) | **Corrected** — "has no `_id` and no collection of its own, so you cannot read it without its parent… You can still match on its fields through the parent, with a path such as `address.city` in the pipeline builder." | -| 91 | 16 MB document limit | general MongoDB fact, attributed to MongoDB not Prisma ORM | Confirmed (kept) | -| 97, 111 | `@relation(fields:, references:)` on MongoDB | `2-mongo-family/2-authoring/contract-psl/src/mongo-attribute-specs.ts:148-156` | Confirmed | -| 100–113 | `authorId ObjectId`, `posts Post[]` back-relation | `examples/mongo-blog-leaderboard/src/contract.prisma` — same models, same field names | Confirmed | -| 116 | include resolves with `$lookup` | `2-mongo-family/5-query-builders/orm/src/collection.ts:97` ("Adds a `$lookup` for a reference relation"); `skills/prisma-8/references/queries-mongo.md:21` | Confirmed | -| 118 | denormalization advice | general advice; does not contradict the ORM | Confirmed (reworded only) | -| 127–148 | `@@discriminator(kind)`, `@@base(Post, "article")`, `kind String` on the base | `examples/mongo-blog-leaderboard/src/contract.prisma` and `mongo-demo` carry exactly this shape | Confirmed | -| 124, 150 | all variants share one collection | `interpreter.ts:472-482` sets each variant's `storage.collection` to the base's; `interpreter.ts:462-468` rejects a variant `@@map` naming a different collection with `PSL_MONGO_VARIANT_SEPARATE_COLLECTION` | Confirmed | -| 150 | a base query returns all variants | `collection.ts:203-232` — the discriminator filter is added only by `.variant()`, never by the base collection | Confirmed | -| 150 | `.variant(...)` exists, takes the variant **model name** | `collection.ts:83-86,203-232`; `orm/test/integration/polymorphism.test.ts:100` (`orm.tasks.variant('Bug').all()`); `queries-mongo.md:57` (`db.orm.posts.variant('Article').all()`) | Confirmed | -| — | does `@@map` on a variant mean anything on MongoDB | `interpreter.ts:462-468` — only permitted if it names the base's collection, otherwise an error. The page's variants carry no `@@map`, matching the real contracts | Confirmed (no page change needed) | -| 156 | `create-prisma@latest` and the skills link | `conventions.md` scaffold command; `apps/docs/content/docs/ai/tools/skills.mdx:100` pinned anchor `[#available-skills-for-prisma-8]` | **Corrected** — scaffold command and first sentence per page-conventions | -| 156 | "the `prisma-8` skill covers document modeling" | `skills/prisma-8/references/queries-mongo.md`, `contract.md` | Confirmed | -| 165–167 | link targets | all files exist: `orm/data-modeling/index.mdx`, `fundamentals/relations-and-joins.mdx`, `fundamentals/reading-data.mdx`, `fundamentals/advanced-queries.mdx` (heading `## MongoDB: Pipeline builder` → `#mongodb-pipeline-builder`), `data-modeling/relational-databases.mdx` | Confirmed | - -## 3. Q list - -None. Every claim resolved against the source. - -## 4. Line count - -167 before, 167 after. - -`check-plain.sh` prints "clean". The extra page-conventions word grep is clear (the single hit is "Aggregate", a false positive on the `gate` substring). \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/factcheck1.relational-databases.md b/docs/orm-docs-audit/c21-data-modeling/factcheck1.relational-databases.md deleted file mode 100644 index f6ad8c6fac2..00000000000 --- a/docs/orm-docs-audit/c21-data-modeling/factcheck1.relational-databases.md +++ /dev/null @@ -1,83 +0,0 @@ -Done. Both passes are complete; `check-plain.sh` prints **clean** and the page-conventions extra-word grep is empty. - -## 1. Banned-term and wording replacements - -No headings changed, so no anchors pinned. Line numbers are the final file. - -| Line | Old | New | -|---|---|---| -| 11 | "This page shows how to model each relation shape and, for each one, where the foreign key belongs. If you are new to models and keys, start with the [data modeling overview]…" | "If you are new to models and keys, start with the [data modeling overview](/orm/data-modeling)." (docs-about-docs commentary cut) | -| 15 | "…carries two fields: a scalar field for the foreign key column, and a relation field that navigates it." | "…has two fields. One is a scalar field for the foreign key column. The other is a relation field, typed as the other model." ("carries" is banned as a verb) | -| 25 | "The side that carries the `fields` argument…" | "The side that declares the `fields` argument…" | -| 46 | "The foreign key lives on the "many" side" | "The foreign key is on the "many" side" ("lives on" banned) | -| 81 | "…a bare optional model-typed field is enough…; …or emit rejects the field with `PSL_NON_UNIQUE_BACKRELATION`." | semicolon split into sentences; "emit rejects" → "`npx prisma contract emit` rejects the field with an error whose `code` is `PSL_NON_UNIQUE_BACKRELATION`" | -| 87 | "So `Profile` carries `userId`" | "So `Profile` holds `userId`" | -| 111 | "The answer is a junction model" | "The answer is the model for the join table" | -| 136 | "…and the schema compiler connects them through `PostTag`" | "…and `npx prisma contract emit` connects them through `PostTag`" | -| 146 (comment) | "// Or, equivalently, create the junction record yourself" | "// Or create the join record yourself" | -| 150 | "The junction is an ordinary model, so it can carry data… add the field to the junction model." | "`PostTag` is an ordinary model, so it can hold data… add the field to `PostTag`." | -| 164 | "…with no junction model…: the schema compiler rejects it and asks for an explicit join model. Model the junction explicitly; [Relations and joins]… traverse it" | rewritten (see Corrected table); "junction" gone, error code named, semicolon removed | -| 168 | "each carries its own extra fields" | "each has its own extra fields" | -| 198 | "narrow the base collection with `.variant(...)`…; the discriminator is injected for you" | "limit the query to one variant with `.variant(...)`… The discriminator value is filled in for you" ("narrow" as a bare verb and "injected" banned) | -| 204 | "Chaining `.variant(...)` on a read narrows the same way, applying the discriminator filter and giving the collection the variant's row shape." | "On a read, `.variant(...)` works the same way. It limits the query to one variant by filtering on the discriminator, and the rows come back with that variant's fields." | -| 208 | "Its extra columns live alongside the base columns and must be nullable at the storage level" | "Its extra columns are added to the base table and have to be nullable there" | -| 211 | "Use polymorphism when…" | "Use a base model with variants when…" ("polymorphism" as a bare noun banned) | -| 215 | "Projects scaffolded with `create-prisma@latest` install [Prisma ORM skills]…; the `prisma-8` skill…" | "Projects created with `npm create prisma@latest` include the [Prisma ORM skills]… In an existing project, run `npx prisma skills sync`. The `prisma-8` skill…" (required first-sentence rule) | -| 219 | "an explicit junction model" | "an explicit model for the join table" | -| 224 | "relation predicates, and junction traversal" | "filters on relation data, and reads across the join table" | -| 226 | "the emitter lowers them to the foreign key's `ON DELETE` / `ON UPDATE` clause" | "They become the foreign key's `ON DELETE` and `ON UPDATE` clauses." | - -Note on a conventions conflict: `page-conventions.md` says to write "the join model" after first use, but `references/banned-terms.md` bans "join model" and `check-plain.sh` fails on it. I used "the model for the join table", "the join table", and the model's own name `PostTag` instead, so the check passes. - -## 2. Claims checked - -All paths under `wip/prisma-src/`. - -| Line | Claim | Deciding source | Verdict | -|---|---|---|---| -| 17–23, 31–44, 67–79, 89–95, 113–134, 152–162, 172–194 | PSL examples need no `// use prisma-8` line | `apps/docs/content/docs/orm/contract-authoring/psl-syntax.mdx:147,159,175,222,235,262` — fragment snippets carry no marker; only whole-file `title="prisma/contract.prisma"` blocks do (lines 15–16, 59–60) | Confirmed, no change | -| 9 | Model → table, scalar field → column | `packages/2-sql/2-authoring/contract-psl/src/interpreter.ts` (`buildModelNodeFromPsl`, `storage.table` / `storage.fields`) | Confirmed | -| 19, 33, 39, 69, 74, 91, 115, 121, 174 | `Int @id @default(autoincrement())` is valid PSL on PostgreSQL | `packages/3-targets/3-targets/postgres/src/core/psl-infer/infer-model-blocks.ts:283-290`, `core/migrations/planner-ddl-builders.ts:61,151` | Confirmed | -| 25 | "The side that declares the `fields` argument owns the foreign key" | `packages/1-framework/2-authoring/contract/src/relation-nullability.ts:20-31` (`ownsReference`), `psl-relation-resolution.ts:102-150` (`indexFkRelations` builds `N:1` from the declaring side) | Confirmed | -| 46 | `posts Post[]` is a back-relation, stores nothing, matched to the FK on `Post` | `psl-relation-resolution.ts:380-430` (`applyBackrelationCandidates` matches by `fkRelationPairKey`, emits `1:N` with no storage of its own) | Confirmed | -| 50 | (was) "Because the list field is virtual, you never write to it." | `test/integration/test/sql-orm-client/nested-mutations.test.ts:19-30` (`users.create({ posts: (posts) => … })`) and `mn-nested-write.test.ts:59,95` — list fields **do** accept nested writes, and this page itself shows one at line 142 | **Corrected** — sentence removed; the paragraph now reads "To connect a post to a user, set the foreign key when you create the post:" | -| 52–57 | `db.orm.public.Post.create({ title, authorId })` — `create` takes a foreign key scalar directly, and the page's `Post` has both fields | `test/integration/test/sql-orm-client/create.test.ts:187-190` (`createAll([{ id, name, email, invitedById: null }])` sets an FK scalar in a create input); page's `Post` (lines 38–43) declares `title` and `authorId` | Confirmed | -| 65, 76, 92 | `@unique` on the foreign key makes it one-to-one | `psl-relation-resolution.ts:352-366` (`fkColumnsAreUnique`: the FK columns must exactly match a unique column set) | Confirmed | -| 81 | Mirror field `profile Profile?` needs no `@relation` | `psl-relation-resolution.ts:487-525` (`validateBackrelationFieldAttributes` skips `relation` and rejects any other attribute; a bare field is accepted) | Confirmed | -| 81 | The mirror field must be optional | `packages/1-framework/2-authoring/psl-parser/src/relation-backrelations.ts:37-51` — a required singular back-relation gets `PSL_REQUIRED_ONE_TO_ONE_BACKRELATION`; `psl-relation-resolution.ts:461-474` calls it | **Corrected** (addition): the page now states "It has to be optional, because nothing in the database guarantees a matching profile row." | -| 81 | "emit rejects the field with `PSL_NON_UNIQUE_BACKRELATION`" — exact code and what it says | `psl-relation-resolution.ts:451-458`: code `PSL_NON_UNIQUE_BACKRELATION`, message "Backrelation field … is singular, but the matching FK on … is not unique… add @unique (or @@unique([…])) to the FK fields, or make … a list." | Confirmed (code exact); wording changed to the required "`npx prisma contract emit` rejects … with an error whose `code` is …" | -| (checked, not on page) | Required relation field over an optional FK rejected with `PSL_RELATION_NULLABILITY_MISMATCH` in rc.10 | `packages/2-sql/2-authoring/contract-psl/src/interpreter.ts:702-717` (code and both message directions), `relation-nullability.ts:26-33`; tests `contract-psl/test/interpreter.relations.test.ts:167,195,271` | Confirmed — the coming-from-prisma-orm-7 page is right, and every relation field on this page pairs a required relation field with a required FK scalar, so all examples are consistent | -| 113–136 | `Post.tags Tag[]` + `Tag.posts Post[]` + `PostTag` with `@@id([postId, tagId])` resolves to many-to-many in rc.10 (not `PSL_ORPHANED_BACKRELATION`) | `psl-relation-resolution.ts:225-275` (`findJunctionFkPairs`) and `:404-420` — a bare list back-relation with no direct FK match is resolved through a model whose `@@id` covers exactly the two FK columns and whose target-side FK references the target's full `@id`. Both hold here. Corroborated by `apps/docs/content/docs/orm/contract-authoring/psl-syntax.mdx:235-256` and `examples/retail-store/src/contract.prisma` (`Post.tags Tag[]` / `Tag.posts Post[]`) | Confirmed — the example is valid | -| 136 | `@@id([postId, tagId])` enforces one record per pair, and emit connects the list fields through `PostTag` so `post.tags` reads directly | `psl-relation-resolution.ts:155-175` (`idColumnsAreExactlyFkPair`), `:277-310` (`manyToManyRelationNode` emits `cardinality: 'N:M'` with `through`) | Confirmed | -| 138 | Both writes hit the same row; running both inserts the same composite key twice and the second fails | `test/integration/test/sql-orm-client/mn-nested-write.test.ts:145-170` — "connect to an already-linked tag rejects", `rejects.toThrow(/violated a unique constraint on junction "user_tags"/)` | Confirmed | -| 142–144 | `.where({ id }).update({ tags: (t) => t.connect([{ id }]) })` — `update` takes a relation callback and `connect` exists through a join model | `mn-nested-write.test.ts:124-130` (`users.where({ id: 1 }).…update({ tags: (t) => t.connect({ id: TAG_RUST }) })`); array form at `:95` | Confirmed | -| 147 | `db.orm.public.PostTag.create({ postId, tagId })` | join model is an ordinary model in `domain.namespaces..models`; `packages/3-extensions/sql-orm-client/src/orm.ts:70-80` keys `db.orm.` by model name | Confirmed | -| 152–162 | `addedAt DateTime @default(now())` on the join model | `packages/3-targets/3-targets/postgres/src/core/default-normalizer.ts:12,33,41`; `examples/retail-store/src/contract.prisma` uses `createdAt DateTime @default(now())` | Confirmed | -| 164 | Implicit many-to-many is rejected, with an explicit join model asked for | `psl-relation-resolution.ts:428-436`: code `PSL_ORPHANED_BACKRELATION`, message "Backrelation field "X.y" has no matching FK-side relation on model "Z". Add @relation(fields: […], references: […]) on the FK-side relation **or use an explicit join model for many-to-many**." | **Corrected** — the page now names the code and drops "yet": "…is not supported. `npx prisma contract emit` rejects it with an error whose `code` is `PSL_ORPHANED_BACKRELATION`, and the message tells you to add an explicit model for the join table." Nothing in the source says "not supported *yet*"; `skills/prisma-8/references/contract.md:424` likewise states it is rejected | -| 178 | `@@discriminator(type)` | `packages/2-sql/2-authoring/contract-psl/src/sql-attribute-specs.ts:381-383` — `modelAttribute('discriminator', { positional: [{ key: 'field', type: fieldRef() }] })` | Confirmed | -| 184, 190 | `@@base(Task, "bug")` | `sql-attribute-specs.ts:384-389` — positional `entityRef()` then `str()` | Confirmed | -| 192 | `@@map("features")` on a variant | `interpreter.ts:1864-1875, 2562-2572` — an explicit `@@map` on a variant is the multi-table signal | Confirmed | -| 198 | `db.orm.public.Bug` exists alongside `db.orm.public.Task` | emitted fixture `test/integration/test/sql-orm-client/fixtures/polymorphism/generated/contract.json` lists `Bug`, `Epic`, `Feature`, `Task` as separate models in `domain.namespaces.public.models`; `orm.ts:70-80` | Confirmed | -| 198 | A query on the base model returns every variant | `test/integration/test/sql-orm-client/polymorphism.test.ts:74-128` — a bare `Task` query returns both bug and feature rows, each with its own variant field | Confirmed | -| 198, 201 | `.variant("Bug").create({...})` fills in the discriminator | `packages/3-extensions/sql-orm-client/src/collection.ts:394-449` (doc comment "Insert under a variant — discriminator is injected automatically", and `LiteralExpr.of(variantEntry.value)`) | Confirmed | -| 204 | `.variant(...)` on a read applies the discriminator filter and gives the variant's row shape | `collection.ts:394-470`; `polymorphism.test.ts:137-200` | Confirmed | -| 208 | Shared-table variant: extra columns in the base table, nullable | `interpreter.ts:1975-1995` (`materializeStiVariantStorageColumns`: "The materialised columns are always nullable in storage"); `polymorphism.test.ts:31-40` (`severity text` nullable in `tasks`) | Confirmed | -| 209 | Own-table variant: own table linked by the base primary key, constraints kept, at the cost of a join | `interpreter.ts:1890-1935` (`materializeMtiVariantStorageLinks`, "joins that table to the base on the shared primary key"); `polymorphism.test.ts:43-47` (`create table features (id integer primary key references tasks(id), priority integer not null, …)`) and `:179` ("INNER JOINs") | **Corrected** (precision) — "holding only its extra columns, linked back to the base table's primary key" became "That table holds its extra columns plus the base model's primary key, which links it back to the base table", because the variant table does carry the base PK column | -| 215 | `create-prisma@latest` / skills link | `skills/prisma-8/SKILL.md` exists; conventions fix the scaffold command to `npm create prisma@latest` | **Corrected** to the conventions wording | -| 215 | "the `prisma-8` skill covers relation modeling" | `skills/prisma-8/references/contract.md:95-142` ("Workflow — Edit a model / field / relation (PSL)"), `:213-245` (polymorphism) | Confirmed | -| 224 | `.include(...)` and filters on relation data exist | `test/integration/test/sql-orm-client/include.test.ts`, `mn-filter.test.ts` | Confirmed | -| 226 | `onDelete` / `onUpdate` accept `Cascade`, `Restrict`, `SetNull`, `SetDefault`, `NoAction` | `sql-attribute-specs.ts:418-425` (`referentialActionArgument`), `psl-relation-resolution.ts:24-34` (`REFERENTIAL_ACTION_MAP`) | Confirmed — exactly these five | -| 226 | They become the FK's `ON DELETE` / `ON UPDATE` clauses | `packages/3-targets/3-targets/postgres/src/core/migrations/operations/constraints.ts:22-45` — `sql += \`\nON DELETE ${action}\`` and the same for `ON UPDATE` | Confirmed | -| 11, 59, 81, 164, 215, 224, 225 | Every link target and anchor | `apps/docs/content/docs/orm/data-modeling/index.mdx` (`url: /orm/data-modeling`), `orm/data-modeling/mongodb.mdx`, `orm/fundamentals/relations-and-joins.mdx` headings `## One-to-one`, `## One-to-many`, `## Many-to-many`, `ai/tools/skills.mdx:100` `## Available skills for Prisma ORM 8 [#available-skills-for-prisma-8]` | Confirmed — all resolve | - -## 3. Q list - -None. Every claim on the page resolved for or against in the source. - -Two notes rather than Qs: -- No test covers the `PSL_ORPHANED_BACKRELATION` implicit-many-to-many path specifically from a docs-shaped contract; the code at `psl-relation-resolution.ts:428-436` is what the page now states. -- `PSL_REQUIRED_ONE_TO_ONE_BACKRELATION` (the required-mirror-field rejection) is a different code from `PSL_NON_UNIQUE_BACKRELATION`. I stated the optionality requirement in plain words without naming the second code, to keep the page from growing. - -## 4. Line count - -226 before, 226 after. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/factcheck2.index.md b/docs/orm-docs-audit/c21-data-modeling/factcheck2.index.md deleted file mode 100644 index cd24fe6a300..00000000000 --- a/docs/orm-docs-audit/c21-data-modeling/factcheck2.index.md +++ /dev/null @@ -1,58 +0,0 @@ -Fact re-check complete. `wip/c21/dm/fc2/index.mdx`: 252 lines before, 252 after. `check-plain.sh` prints "clean"; the extra banned-word grep hits only "surro**gate**", a substring false positive. - -## Claims checked - -| Line | Claim | Source | Verdict | -| --- | --- | --- | --- | -| 13 | Contract derives TypeScript types and migrations | `skills/prisma-8/SKILL.md:34` (`contract.json` + `contract.d.ts`), `references/migrations.md:95` | Confirmed | -| 15 | No `datasource`/`generator` block; connection string in `prisma.config.ts` | `examples/prisma-8-demo/prisma.config.ts` (`db: { connection: … }`); no `datasource`/`generator` keyword anywhere in `packages/1-framework/2-authoring/psl-parser/src` | Confirmed | -| 15 | `#a-complete-contract` shows a whole file and the config file | `apps/docs/content/docs/orm/contract-authoring/psl-syntax.mdx:13`, and `prisma.config.ts` blocks at lines 108/119 | Confirmed | -| 15 | Link covers a Prisma ORM 6 reader | `apps/docs/content/docs/orm/coming-from-prisma-orm-7.mdx` — the page never mentions version 6 | **Corrected** → "If you are [coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7), that page covers the rename and the rest of the differences." | -| 17 | Prisma ORM 8 supports PostgreSQL and MongoDB **only** | `wip/prisma-src/README.md:98-101` lists SQLite under "Supported databases" as "planned next, a proof of concept today"; `scorecard.md` has a SQLite column throughout; `packages/3-extensions/sqlite` ships at `8.0.0-rc.10` | **Corrected** → "Prisma ORM 8 supports PostgreSQL and MongoDB, and SQLite is still a proof of concept." | -| 32, 46, 86, 99 | `@id`, `@default(autoincrement())` | `packages/3-targets/6-adapters/postgres/src/core/control-mutation-defaults.ts:126-131`; `sql-attribute-specs.ts:212,236` | Confirmed | -| 38 | Model becomes a table / a collection | `packages/2-mongo-family/2-authoring/contract-psl/src/interpreter.ts:1342` (`storage: { collection }`) | Confirmed | -| 42 | `update`/`delete` fail at runtime without a primary key or unique field | `packages/3-extensions/sql-orm-client/src/collection.ts:2413-2425` throws `ORM.ROW_IDENTITY_MISSING`, reached from `update()` (2015) and `delete()` (2191). No emit-time `@id` requirement exists for SQL (no `PSL_MISSING_ID_FIELD` under `packages/2-sql`), so runtime really is where it fails | Confirmed | -| 53, 59 | MongoDB `_id` mandatory and must be `ObjectId`; `@map("_id")` | `packages/2-mongo-family/2-authoring/contract-psl/src/interpreter.ts:1320-1340` (`PSL_MONGO_ID_REQUIRED`, example spelled `id ObjectId @id @map("_id")`) | Confirmed | -| 59 | The field takes no `@default` | `packages/2-mongo-family/2-authoring/contract-psl/src/mongo-attribute-specs.ts:247-261` — the MongoDB field attributes are `id`, `unique`, `map`, `relation`; there is no `default` | Confirmed | -| 59 | Omit `id` on create and MongoDB assigns it | `packages/2-mongo-family/5-query-builders/orm/src/collection.ts:113,369-374` ("server-assigned `_id`") | Confirmed | -| 59 | `@@map` optional; default collection is the model name with a lowercase first letter | `interpreter.ts:334-335` (`?? lowerFirst(model.name)`), `psl-helpers.ts:8` | Confirmed | -| 103 | Auto-increment value known only after the insert | `control-mutation-defaults.ts:48-55` — `autoincrement()` is a storage default, not an execution generator | Confirmed | -| 106 | UUID generated before the insert | `control-mutation-defaults.ts:70-77` returns `executionGenerator('uuidv7'/'uuidv4')`, i.e. client-side | Confirmed | -| 109, 113 | `@default(uuid())` random, `@default(uuid(7))` sorts by creation time | `control-mutation-defaults.ts:73-80` maps `uuid(7)` → `uuidv7` and bare `uuid()` → `uuidv4`; `packages/1-framework/2-authoring/ids/src/generators.ts:5-6` uses `uniku/uuid/v4` and `uniku/uuid/v7`. v7 is time-ordered and v4 random by RFC 9562; the tree states the versions, not the ordering property | Confirmed | -| 109 | `id String @default(uuid())` is valid | `ids/src/index.ts:57-64` — `uuidv4`/`uuidv7` `applicableCodecIds` include `pg/text@1`, which is what `String` maps to | Confirmed | -| 113 | `@default(uuid(7))` and `@default(uuid())` are both accepted spellings | `control-mutation-defaults.ts:141` `usageSignatures: ['uuid()', 'uuid(4)', 'uuid(7)']` | Confirmed | -| 124 | `@@id([userId, tagId])` composite key | `packages/2-sql/2-authoring/contract-psl/src/sql-attribute-specs.ts:236,472` | Confirmed | -| 128 | "The same model" is shown under many-to-many | `apps/docs/content/docs/orm/data-modeling/relational-databases.mdx:127-133` shows `PostTag` with `postId`/`tagId`, not `UserTag` | **Corrected** → "[Many-to-many](…#many-to-many) writes a model like this one out with its relation fields." | -| 136-146 | Scalar table: `String`→text, `Int`→int4, `BigInt`→int8, `Float`→float8, `Decimal`→numeric, `Boolean`→bool, `DateTime`→timestamptz, `Bytes`→bytea, `Json`→json, `Jsonb`→jsonb | `packages/3-targets/6-adapters/postgres/src/core/control-mutation-defaults.ts:157-196` (`postgresScalarAuthoringTypes`) | Confirmed | -| 140 | `Numeric(10, 2)` takes a precision and scale | same file, `postgresNativeAuthoringTypes.Numeric`, args `precision` and `scale` | Confirmed | -| 146, 148 | `ObjectId` is the only MongoDB-only type | `packages/3-mongo-target/2-mongo-adapter/src/exports/control.ts:25-36` — MongoDB scalars are `String`, `Int`, `Boolean`, `DateTime`, `ObjectId`, `Float`; `ObjectId` is the only one absent from the PostgreSQL set | Confirmed | -| 150 | `Decimal` reaches your code as a string | `packages/3-targets/3-targets/postgres/src/core/codecs.ts:896-905` — `PgNumericCodec.decode` returns `string` | Confirmed | -| 150 | `DateTime` arrives as `Temporal.Instant` | `postgresScalarAuthoringTypes.DateTime` → `pg/timestamptz-temporal@1`; `temporal-codecs.ts:143-155` decodes to `Temporal.Instant` | Confirmed | -| 150 | `Temporal` built into Node.js 26.8.2 and later | `skills/prisma-8/references/contract.md:118` | Confirmed | -| 150 | `temporal-polyfill` package, `import "temporal-polyfill/full/global"` before any query | `contract.md:118`, `docs/reference/error-reference.md:779`, `test/integration/test/setup-temporal.ts:1` | Confirmed | -| 150 | `Bytes` arrives as `Uint8Array` | `codecs.ts:1167-1177` | Confirmed | -| 152 | `Json` and `Jsonb` are two separate types | `control-mutation-defaults.ts:186-193` — separate entries, `pg/json@1` / `pg/jsonb@1` | Confirmed | -| 152 | `.where()` on `Jsonb` is whole-value only; `Json` not comparable at all | `codecs.ts:1401` `PgJsonDescriptor.traits = []`; `codecs.ts:1445` `PgJsonbDescriptor.traits = ['equality']` | Confirmed | -| 152 | Raw SQL for a path query | `scorecard/09-filtering.md:34` — JSON-path filtering is `❌` on PostgreSQL | Confirmed | -| 152 | Extensions add vectors or geometry | `README.md:87-88` (pgvector, postgis) | Confirmed | -| 154 | `name String?`, `tags String[]` | `test/integration/test/sql-orm-client/fixtures/scalar-lists/contract.prisma:11-15` has `tags String[]` verbatim | Confirmed | -| 158-163 | Enum block syntax, `@@type("pg/text@1")`, `Low = "low"` | `skills/prisma-8/references/contract.md:274-287`; `examples/prisma-8-cloudflare-worker/src/prisma/contract.prisma:9` | Confirmed | -| 166 | "`@@type` is required on every enum" | `packages/1-framework/1-core/framework-components/src/shared/framework-authoring.ts:326-348` — when `@@type` is absent, `resolveEnumCodecId` infers the type from the members via `classifyEnumMemberType` (all-string → text, all-integer → int), using each target's `enumInferenceCodecs`. Only an un-inferable member set raises `PSL_ENUM_CANNOT_INFER_TYPE` | **Corrected** → "`@@type` says how the values are stored, here as PostgreSQL `text`. Leave it out and Prisma ORM takes the storage from the members, text for string values and an integer for whole numbers." | -| 166 | `pg/text@1` is the type plus a version, always `@1` today | grep over every `'(pg\|sql\|mongo)/…@N'` literal in `packages/` returns only `@1` | Confirmed | -| 166 | `@@type("mongo/string@1")` on MongoDB | `examples/mongo-demo/src/contract.prisma:4`, `examples/retail-store/src/contract.prisma:4` | Confirmed | -| 199 | `BigInt` arrives as a JavaScript `bigint` | `codecs.ts:645-656` — `PgInt8Codec.decode` returns `bigint` (`JSON.stringify` refusing `bigint` is a JavaScript language fact) | Confirmed | -| 209 | Prisma ORM 7 called the link field the relation scalar field | `apps/docs/content/docs/orm/v7/prisma-schema/data-model/relations/index.mdx:126,132` uses that exact term. Attributed to Prisma ORM 7, not claimed of rc.10 | Confirmed | -| 212, 215-225 | `@relation(fields:, references:)`; `User`/`Post` with `posts Post[]` back-relation | `sql-attribute-specs.ts` `relationFieldSpec`; back-relation matching in `psl-relation-resolution.ts:448-480` | Confirmed | -| 229 | One-to-one: `@unique` on the link field, `profile Profile?` not a list | `psl-relation-resolution.ts:452-475` — a singular back-relation with a non-unique FK raises `PSL_NON_UNIQUE_BACKRELATION`, and a required singular back-relation raises `requiredOneToOneBackrelationDiagnostic` | Confirmed | -| 231 | Implicit many-to-many is gone and `npx prisma contract emit` rejects it | `psl-relation-resolution.ts:431-433` — `PSL_ORPHANED_BACKRELATION` for a list back-relation with no join model | Confirmed | -| 240 | `npm create prisma@latest`, `npx prisma skills sync`, `prisma-8` skill | `skills/prisma-8/SKILL.md` exists; `skills sync` kept per `conventions.md` (published CLI has it, this tree does not) | Confirmed | -| 251 | `contract emit` → `migration plan --name` → `db migrate`; emit replaces `prisma generate` | `skills/prisma-8/references/migrations.md:95` (`contract emit && migration plan --name add_feature`), `quickstart.md:265`, `packages/1-framework/3-tooling/cli/src/migration-cli.ts:6` (`prisma db migrate`), `docs/architecture docs/adrs/ADR 032:150` ("Replaces `prisma generate`") | Confirmed | -| all | Every link target and anchor | `#models`/`#primary-keys`/`#scalar-fields`/`#relations` exist on the page; `psl-syntax.mdx:13` `## A complete contract` and `:183` `## Enums`; `relational-databases.mdx:75` `## One-to-one` and `:107` `## Many-to-many`; `url:` front matter matches for `/orm/extensions`, `/orm/migrations/generating-a-migration`, `/orm/coming-from-prisma-orm-7`, `/orm/reference/raw-queries`, `/orm/fundamentals/reading-data`; `ai/tools/skills.mdx:100` pins `[#available-skills-for-prisma-8]` | Confirmed | - -## Q list - -None. Every claim resolved for or against. - -## Note not acted on - -`page-conventions.md` says type ids such as `pg/text@1` do not belong in prose on these pages. Line 166 explains `pg/text@1` in prose. That is a convention question, not a fact question, so I left it for the operator. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/factcheck2.mongodb.md b/docs/orm-docs-audit/c21-data-modeling/factcheck2.mongodb.md deleted file mode 100644 index 2e60039c5e1..00000000000 --- a/docs/orm-docs-audit/c21-data-modeling/factcheck2.mongodb.md +++ /dev/null @@ -1,79 +0,0 @@ -Done. Report below. - -## Claims checked - -| Line | Claim | Source that decides it | Verdict | -|---|---|---|---| -| 13 | Contract is `contract.prisma`; a model maps to a collection, a record is a document | `examples/mongo-demo/src/contract.prisma`; `prisma.config.ts` `contract: './src/contract.prisma'` | Confirmed | -| 13 | Connection string in `prisma.config.ts`, passed to `defineConfig` from `@prisma/orm-mongo/config` | `examples/mongo-demo/prisma.config.ts` lines 1-11 (and `mongo-blog-leaderboard/prisma.config.ts`) | Confirmed | -| 13 | No `datasource` block on MongoDB | No `datasource` token anywhere in `packages/1-framework/2-authoring/psl-parser/src/` or `packages/2-mongo-family/2-authoring/` | Confirmed | -| 13 | The data contract page shows the config file | `apps/docs/content/docs/orm/contract-authoring/the-data-contract.mdx:53-55` | Confirmed | -| 16-21 | `id ObjectId @id @map("_id")`, `email String @unique`, `@@map("users")` all valid | `packages/1-framework/3-tooling/cli/src/commands/init/templates/code-templates.ts:196-206` (the mongo starter contract is character-for-character this shape) | Confirmed | -| 24 | In TypeScript the key is always `_id` | `examples/mongo-demo/src/contract.d.ts` `FieldOutputTypes`/`FieldInputTypes`: `User._id`, `Post._id` | Confirmed | -| 24 | `@map("_id")` sets the name in the database and in code | same, plus `contract.json` relation `targetFields: ["_id"]` | Confirmed | -| 24 | `@@map` optional; default is the model name with a lowercase first letter | `packages/2-mongo-family/2-authoring/contract-psl/src/interpreter.ts:254` (`return name ?? lowerFirst(model.name)`), `psl-helpers.ts:6-8` | Confirmed | -| 24 | `@unique` becomes a unique index when you migrate | `interpreter.ts:894-915` builds a `MongoIndex({ unique: true })` per `@unique` field; `examples/mongo-demo/migrations/app/20260409T1030_migration/migration.ts:11` `createIndex('users', [{field:'email'}], { unique: true })` | Confirmed | -| 26 | `ObjectId` is the contract type; ids are plain strings in TypeScript | `packages/3-mongo-target/1-mongo-target/src/core/codec-types.ts:2` — `'mongo/objectId@1': { input: string; output: string }` | Confirmed | -| 26 | `String @db.ObjectId` no longer allowed | `packages/2-mongo-family/2-authoring/contract-psl/test/interpreter.attribute-specs.test.ts:105-118` — `PSL_UNSUPPORTED_FIELD_ATTRIBUTE`, `"@db.ObjectId"` | Confirmed | -| 26 | `@default(auto())` no longer allowed | same file lines 92-98 — `Field "Item.createdAt" uses unsupported attribute "@default"`; `mongo-attribute-specs.ts:249-262` has no `default` in the field namespace | Confirmed | -| 28 | Omit `id` in `create`, MongoDB assigns it | `packages/2-mongo-family/5-query-builders/orm/src/types.ts:223-232` (`Omit<..., '_id'> & Partial>`); `collection.ts:113` doc comment "Returns the input data with the server-assigned `_id`" | Confirmed | -| 28, 31 | `npx prisma orm init` writes `src/prisma/db.ts`; `import { db } from "./prisma/db"` | `packages/1-framework/3-tooling/cli/test/orm/init-scaffold.test.ts:234`; `skills/prisma-8/references/quickstart.md:36,73,84` | Confirmed | -| 28 | `db.orm` is the ORM client, one of the query APIs on `db` | `packages/3-extensions/mongo/src/runtime/mongo.ts:31-32,190-191` (`orm` and `query`) | Confirmed | -| 32-33 | `create({...})` then `.where({ _id }).first()` | `collection.ts:112-116`; `test/integration/orm-ergonomics.test.ts` uses exactly this shape | Confirmed | -| 36 | The name after `db.orm.` is the collection name | `mongo-orm.ts:20-25` keys the client by `TContract['roots']`; `interpreter.ts:1361` `roots[collectionName] = ...`; `examples/mongo-demo/src/contract.json` roots are `users`, `posts` | Confirmed | -| 36 | No `data:` wrapper (Prisma ORM 7 had one) | `create` takes the row directly — confirmed. The Prisma ORM 7 half is not in this tree — see Q1 | Confirmed / Q1 | -| 36 | `.first()` returns one document or `null`; `.all()` returns every matching document | `collection.ts:110-113`; `framework-components/src/execution/async-iterable-result.ts:3` (`PromiseLike`, so `await` gives an array) | Confirmed | -| 40, 42 | Embed vs reference trade-offs, decision table, comments example | General MongoDB modeling advice, nothing in the source contradicts it | Confirmed (not a source claim) | -| 57 | `type` block for embedded data; embedded value has no `_id` and no collection | `interpreter.ts:1364-1380` builds `valueObjects`; `examples/mongo-demo/src/contract.d.ts` `AddressOutput` has no `_id` | Confirmed | -| 74 | `addresses Address[]` stores every address inside the user document | `contract-psl/test/interpreter.test.ts:943-965` "emits many: true for value object array fields" | Confirmed | -| 74, 77-78 | Embedded value passed to `create` as a plain nested object | `orm/test/integration/orm-ergonomics.test.ts:173` `homeAddress: { city: 'SF', country: 'US' }` | **Corrected** | -| 81 | `update` takes a callback returning an array; `u.fieldName` for a top-level field | `collection.ts:130-133`, `field-accessor.ts:222-236`; `orm-ergonomics.test.ts:189` `[u.tags.push('admin'), u.loginCount.inc(5)]` | Confirmed | -| 81 | `u("path.to.field")` for a field inside an embedded document | `field-accessor.ts` `DotPath` (call signature); `orm-ergonomics.test.ts:177` `u('homeAddress.city').set('NYC')` | Confirmed | -| 84-85 | `u("address.city").set(...)`, `u.tags.push(...)`, `u.tags.pull(...)` | `orm-ergonomics.test.ts:141,152,177` | Confirmed | -| 88 | Replace / remove a whole embedded value | `field-accessor.ts` `DotPath` only yields `address.` paths, never bare `address`; whole-value writes go through property access — `orm/test/collection.test.ts:798` `u.homeAddress.set({...})`, `:758` `u.name.unset()` | **Corrected** | -| 88 | `u.addresses.set([ ... ])` replaces a list of embedded values | `field-accessor.ts` `FieldAccessor` includes `ValueObjectFieldKeys` with `FieldExpression.set(value: T)` where `T` is the array | Confirmed | -| 88 | `.where()` filters on top-level model fields only | `orm/src/types.ts:188-201` — `MongoWhereFilter` is keyed by `[ModelName]['fields']`, no dotted paths | Confirmed | -| 91 | `db.query.from("users").match((f) => f("address.city").eq("Berlin")).build()` | `query-builder/src/state-classes.ts:632-650` (`from` takes a root name = collection name); `query-builder/test/builder.test.ts:79` `f('address.city').eq('NYC')`; `builder.ts:817` `build()` | Confirmed | -| 94 | `build()` returns the query, not the rows | `builder.ts:817-840` returns a `MongoQueryPlan`; `skills/prisma-8/references/queries-mongo.md:12,147` "run via `(await db.runtime()).query(plan)`" | Confirmed | -| 94 | 16 MB limit, a write that exceeds it fails | General MongoDB; nothing in the source contradicts it | Confirmed (kept per brief) | -| 100-114 | Reference example: `authorId ObjectId`, `@relation(fields: [authorId], references: [id])`, `posts Post[]` | `examples/mongo-demo/src/contract.prisma` and `mongo-blog-leaderboard/src/contract.prisma` use exactly this | Confirmed | -| 116 | There are no nested writes on MongoDB | `orm/src/types.ts:223-232` — the create input is built from `FieldInputTypes` only; `examples/mongo-demo/src/contract.d.ts` `Post` input has `authorId` and no `author` key | Confirmed | -| 116 | `references: [id]` uses the contract-side name, not `_id` | `examples/mongo-demo/src/contract.prisma` writes `references: [id]`; the emitted `contract.json` relation is `targetFields: ["_id"]` | Confirmed | -| 116, 120 | `.include("author")` adds the related record to every row | `collection.ts:256-322` (reference relations only; embed relations throw `ORM.INCLUDE_UNSUPPORTED`) | Confirmed | -| 119 | `db.orm.posts.create({ title, authorId: user._id })` | Post's input fields in that code block are `title` and `authorId`; matches `CreateInput` | Confirmed | -| 123 | `.include("posts")` on `db.orm.users` gives each user an array of their posts | `examples/mongo-demo/src/contract.json` — `User.relations.posts` is `cardinality: "1:N"` with an `on` clause, so it is includable | Confirmed | -| 123 | Resolving a reference costs a second read; copying fields is a manual trade-off | `skills/prisma-8/references/queries-mongo.md:21` (`.include` lowers to `$lookup`); the rest is general advice | Confirmed | -| 129-146 | Polymorphic contract: `@@discriminator(kind)`, `@@base(Post, "article")` | `examples/mongo-demo/src/contract.prisma`; `mongo-attribute-specs.ts:157-166` — `discriminator` positional is `fieldRef()` (unquoted), `base` is `entityRef()` plus `str()` | Confirmed | -| 148 | `@@discriminator(kind)` takes the field name unquoted | `mongo-attribute-specs.ts:157-159` `fieldRef()` | Confirmed | -| 148 | A variant declares only its own fields | `examples/mongo-demo/src/contract.d.ts` — `Article` carries only `summary` | Confirmed | -| 148 | A variant has no `@@map` of its own | `interpreter.ts:460-470` — a variant *may* carry `@@map`, but only if it names the base's collection; a different one is `PSL_MONGO_VARIANT_SEPARATE_COLLECTION` | **Corrected** | -| 150 | A base query returns all variants; `.variant("Article")` limits to one | `orm/test/integration/polymorphism.test.ts:79-135`; `collection.ts:203-232` | Confirmed | -| 150 | `.variant()` takes the model name, not the discriminator value | `collection.ts:216-229` looks up `model.variants[variantName]` and filters on `variantEntry.value`; `contract.json` `variants: { Article: { value: "article" } }` | Confirmed | -| 150 | All variants sit in one collection | `interpreter.ts:463-468` "Mongo only supports single-collection polymorphism" | Confirmed | -| 150, 153 | Creating through a variant fills in `kind` | `orm/src/types.ts:247-260` (`VariantCreateInput` omits the discriminator field); `polymorphism.test.ts:143-155` asserts the stored doc has `type: 'bug'` | Confirmed | -| 160 | `npx prisma skills sync` exists | `skills/prisma-8/SKILL.md:33,36` | Confirmed | -| 160 | `npm create prisma@latest` projects include the ORM skills | `wip/prisma-src/README.md:37-40` "the agent skills already installed" | Confirmed | -| 169 | `npx prisma contract emit` checks the contract and regenerates types | `templates/quick-reference-mongo.md:71` "Update contract.json and contract.d.ts" | Confirmed | -| 169 | `npx prisma migration plan --name `, `npx prisma db migrate` | `cli/test/orm/migration-plan.test.ts:81`; `examples/mongo-demo/package.json` scripts `"migration:plan": "prisma migration plan"`, `"migration:apply": "prisma db migrate"` | Confirmed | -| 169 | Migrating creates the collections and indexes | `packages/3-mongo-target/1-mongo-target/src/core/migration-factories.ts:212` `createCollection`, `:357`; `examples/mongo-demo/migrations/app/*/migration.ts` `createIndex(...)` | Confirmed | -| 169 | `npm create prisma@latest -- my-app` (the `-- my-app` argument) | Not in the tree — see Q2 | Q | -| 9, 94, 123, 169-172 | All link targets and anchors | Every path resolves under `apps/docs/content/docs/`; `orm/fundamentals/advanced-queries.mdx:292` `## MongoDB: Pipeline builder` → `#mongodb-pipeline-builder`; `ai/tools/skills.mdx:100` pins `[#available-skills-for-prisma-8]` | Confirmed | - -## Corrections made (3) - -1. **Line 88** — `u("address").set({ ... })` / `u("address").unset()` → **`u.address.set({ ... })` / `u.address.unset()`**. The callable form of the field accessor is typed by `DotPath`, which only produces paths *into* an embedded value (`address.city`), never the bare field name. Whole-value writes go through property access, which is what every test uses. -2. **Line 78** — `create({ name: "Ada", address })` → **`create({ name: "Ada", tags: [], address })`**. The create input is `Omit` with no `Partial`, so every non-`_id` field is a required key. The `tags String[]` field added to that code block's model in an earlier round made the example a type error. -3. **Line 148** — "A variant declares no `@@map` of its own and uses its base's collection." → **"A variant uses its base's collection, so it needs no `@@map` of its own."** A variant may carry `@@map`; it is only rejected when it names a *different* collection than the base. - -## Q list - -- **Q1 (line 36)**: "without the `data:` wrapper Prisma ORM 7 used". The Prisma ORM 8 half is confirmed. Nothing in the rc.10 export describes Prisma ORM 7's client shape. Searches: `grep -rn "data: {" skills/`, `grep -rn "prisma 7|Prisma 7" -i skills/prisma-8/references/upgrade-app.md`, `grep -n "data:" skills/prisma-8/references/upgrade-app.md` — all empty. Left as-is. -- **Q2 (line 169)**: the `-- my-app` argument in `npm create prisma@latest -- my-app`. `wip/prisma-src/README.md:37` shows `npm create prisma` with no argument, and `create-prisma` is a separate repo (`projects/consolidate-clis/current-state.md:13` says so explicitly). Searches: `grep -rn "npm create prisma@latest" .`, `grep -rn "create prisma|create-prisma" --include='*.md' --include='*.ts' --include='*.json' .`. Left as-is. - -## No test coverage noted - -- The "no nested writes on MongoDB" claim rests on the create-input type (`orm/src/types.ts:223-232`) and the emitted `contract.d.ts`, not on a test that asserts a rejection. - -## Line count - -`wip/c21/dm/fc2/mongodb.mdx`: **172 before, 172 after**. Plain-language check prints `clean`; the extra banned-word grep is clear (the one hit is "Aggregate" matching `gate`). \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/factcheck2.relational-databases.md b/docs/orm-docs-audit/c21-data-modeling/factcheck2.relational-databases.md deleted file mode 100644 index 1bb9f8a482e..00000000000 --- a/docs/orm-docs-audit/c21-data-modeling/factcheck2.relational-databases.md +++ /dev/null @@ -1,53 +0,0 @@ -Fact re-check complete for `wip/c21/dm/fc2/relational-databases.mdx`. One correction made. - -## Claims checked - -| Line | Claim | Source that decides it | Verdict | -| --- | --- | --- | --- | -| 11 | `contract.prisma` is the contract source; `contract emit` checks it and regenerates TypeScript types; does not touch the database | `skills/prisma-8/references/quickstart.md:283,285,287`; `SKILL.md:34` | Confirmed | -| 11 | `db` created once in `src/prisma/db.ts`, written by `prisma orm init` | `skills/prisma-8/references/quickstart.md:36,107,133`; `references/contract.md:69`; `cli/src/commands/init/templates/readme-postgres.md:29` | Confirmed | -| 11 | `db.orm` holds models by model name; `public` is the PostgreSQL schema | `sql-orm-client/test/model-types.test-d.ts:23`; namespace fixtures in `sql-orm-client/test/orm-namespaced.test.ts` | Confirmed | -| 15-22 | FK side declares scalar + relation field with `@relation(fields:, references:)` | `contract-psl/src/sql-attribute-specs.ts:434`; `references/contract.md:97-110` | Confirmed | -| 27 | Both fields must agree in nullability, else emit reports an error | `contract-psl/src/interpreter.ts:702-716` (`PSL_RELATION_NULLABILITY_MISMATCH`) | Confirmed | -| 29 | `onDelete`/`onUpdate` become `ON DELETE`/`ON UPDATE`; write neither and neither clause is emitted | `postgres/test/migrations/planner.referential-actions.test.ts:120-124,165-173` | Confirmed | -| 31-35 | The five names `Cascade`, `Restrict`, `SetNull`, `SetDefault`, `NoAction` and their meanings | `contract-psl/src/psl-relation-resolution.ts:25-29`; `sql-attribute-specs.ts:420-424`; planner test `:165-173` | Confirmed | -| 35 | `NoAction` is the same as writing nothing | `2-sql/9-family/src/core/psl-contract-infer/relation-inference.ts:9-10,180-181` (`DEFAULT_ON_DELETE`/`DEFAULT_ON_UPDATE = 'noAction'`, omitted on round-trip) | Confirmed | -| 37 | Two relations between the same pair need a name on both sides, `@relation("Name")` form accepted | `psl-relation-resolution.ts:439-444` (`PSL_AMBIGUOUS_BACKRELATION`, message names both forms); `contract-psl/test/interpreter.relations.test.ts:467-494` | Confirmed | -| 43-56 | One-to-many model shapes, `Int @id @default(autoincrement())`, `String @unique` | `test/integration/test/authoring/side-by-side/postgres/contract.prisma:4` | Confirmed | -| 58 | Self-relation is an ordinary one-to-many; one relation between a model and itself needs no name | `psl-relation-resolution.ts:389-398` (matching is by model pair; a single match needs no name). No test covers exactly one unnamed self-relation — the two tests present (`interpreter.relations.test.ts:428,467`) use a name or two relations. Stated from the code. | Confirmed | -| 60 | Mirror field stores nothing and can be left out | `references/contract.md:97` ("Add the relation only on the owning side") | Confirmed | -| 60 | `db.orm.public.Post.where({ authorId: user.id }).all()`; `.all()` runs the query and returns every row | `test/integration/test/sql-orm-client/mn-nested-write.test.ts:291-296`; `include.test.ts:85` | Confirmed | -| 65-70 | `import { db } from "./prisma/db"`; `create` with no `data:` wrapper | `quickstart.md:84`; `mn-nested-write.test.ts:91-96` | Confirmed | -| 73 | `.include("author").all()` nests the related record | `include.test.ts:105-124` | Confirmed | -| 79-93 | One-to-one: `@unique` on the foreign key | `psl-relation-resolution.ts:452-462` (`PSL_NON_UNIQUE_BACKRELATION`) | Confirmed | -| 95 | Mirror field `profile Profile?` needs no `@relation`, must be optional, is singular because `userId` is unique; `.include("profile")` | `psl-relation-resolution.ts:452-473`; `include.test.ts:124` | Confirmed | -| 111-133 | Explicit join model, `@@id([postId, tagId])` | `fixtures/mn-psl/contract.prisma` (identical shape); `psl-relation-resolution.ts:232-289` | Confirmed | -| 136 | `Post.tags` typed `Tag[]` not `PostTag[]` | `fixtures/mn-psl/contract.prisma:25,33` | Confirmed | -| 136 | Second join table needs the name on `Post.tags` and on `PostTag.post` | `psl-relation-resolution.ts:237-239` ("A relation name on the list field pins the parent-side FK relation"); `:417-419` | Confirmed | -| 141-143 | `.where(...).update({ tags: (t) => t.connect([...]) })` | `mn-nested-write.test.ts:291-296` | Confirmed | -| 146 | `t.create` / `t.connect` / `t.disconnect`, each accepting an array | `mn-nested-write.test.ts:95,255,296` | Confirmed | -| 146 | Leave the list fields out and write `PostTag` rows yourself via `db.orm.public.PostTag.create(...)` | `sql-orm-client/test/model-types.test-d.ts:23` (junction is its own collection) | Confirmed | -| 146 | `addedAt DateTime @default(now())` on the join model | `postgres/test/scalar-type-parity.test.ts:98-127` (`DateTime` is a PostgreSQL scalar); `examples/prisma-8-demo/.../contract.prisma:31`; extra non-key columns do not break join-table recognition (`psl-relation-resolution.ts:255-289`) | Confirmed | -| 148 | Implicit many-to-many rejected with `PSL_ORPHANED_BACKRELATION` | `psl-relation-resolution.ts:430-435`; `references/contract.md:139` | Confirmed | -| 155-176 | `@@discriminator(type)` and `@@base(Task, "bug")` quoting; variant declares no `@id`; variant has every base field | `sql-attribute-specs.ts:381-389`; `fixtures/polymorphism/contract.prisma` | Confirmed | -| 180 | `.variant("Bug")` takes the model name; `"bug"` is a type error; create fills in the discriminator | `sql-orm-client/src/types.ts:553-562` (`VariantNames` = variant model-name keys); `src/collection.ts:394-412` | Confirmed | -| 186-195 | Base read returns every variant, each row carrying only its own variant's fields; `switch (task.type)` reaches `task.severity` / `task.targetRelease` | `sql-orm-client/src/types.ts:526-546` (`VariantRow` is a discriminated union over the discriminator literal); `test/integration/.../polymorphism.test.ts:74` | Confirmed | -| 197 | No `@@map` shares the base table with nullable columns; own `@@map` gives its own table named by the string, columns can be `NOT NULL`, a join to read | `contract-psl/src/interpreter.ts:1975-1990` and `:1895-1970`; `polymorphism.test.ts:29-50,53-56` | Confirmed | -| 197 | In Prisma ORM 7, `@@map` only renamed a table | Prisma ORM 7 behaviour, stated as a version contrast. Not in the rc.10 tree. | Q (see below) | -| 203 | Skills note, `npx prisma skills sync`, `prisma-8` skill covers relation modeling | `skills/prisma-8/SKILL.md:61`; conventions.md (rc.13 CLI) | Confirmed | -| 212-214 | Link targets and anchors | `apps/docs/content/docs/orm/fundamentals/relations-and-joins.mdx:47,98,162`; `orm/data-modeling/index.mdx`, `mongodb.mdx`; `orm/migrations/generating-a-migration.mdx`; `ai/tools/skills.mdx:100` | Confirmed | -| 213 | `migration plan --name ` then `db migrate` applies it | `quickstart.md:265`; `references/migrations.md:12,48,96` | **Corrected** | - -## Correction made - -Line 213: `migration plan` writes a migration package (a directory with `migration.ts`, `ops.json`, `migration.json` under `migrations/app/_/`), not a single file (`references/migrations.md:45,73,116`; `quickstart.md:57`). Text now reads "which writes the migration files". - -## Q list - -- **"In Prisma ORM 7, `@@map` only renamed a table."** Searched the rc.10 tree for any Prisma 7 comparison of `@@map`: `grep -rn "@@map" skills/prisma-8/references/contract.md`, `grep -rn -i "prisma 7" skills/prisma-8/`, `grep -rn "@@map" skills/prisma-8/upgrading/`. The tree documents rc.10's own `@@map` behaviour (variant storage choice, `references/contract.md:215`) but says nothing about Prisma ORM 7's. Left as written. - -## Note for the operator - -`skills/prisma-8/references/contract.md:117` says "the default is `Restrict`" for `onDelete`/`onUpdate`. The code contradicts it: no clause is emitted when neither is given (`planner.referential-actions.test.ts:120-124`) and the round-trip default is `noAction` (`relation-inference.ts:9-10`). The page follows the code. The skill prose looks stale. - -Line count: 214 before, 214 after. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/index.fix1.md b/docs/orm-docs-audit/c21-data-modeling/index.fix1.md deleted file mode 100644 index cdd3888ceda..00000000000 --- a/docs/orm-docs-audit/c21-data-modeling/index.fix1.md +++ /dev/null @@ -1,48 +0,0 @@ -Done. Every mark fixed; page is 258 lines (was 252), budget met, `check-plain.sh` clean, no em-dashes. - -**File:** `/Users/will/Projects/prisma/web/.claude/worktrees/pr-conflicts-review-fb93f7/wip/c21/dm/r1/index.mdx` - -## Marks and what I did - -**"contract" (line 13) / "the client's configuration"** — kept the conventions.md wording, added "You still describe your models in it", dropped the configuration clause (the source does not back it), and added the version-7 pointer: "If you are coming from Prisma ORM 7, [what changed](/orm/coming-from-prisma-orm-7) covers the rename and the rest of the differences." That also answers "so what do I type" item 1 (migrating an existing project) with a link rather than a new section. - -**Databases other than PostgreSQL** — "All four apply to both databases Prisma ORM supports today, PostgreSQL and MongoDB." After the type table: "The column names in the table are PostgreSQL columns. MongoDB has no such mapping, and `ObjectId` is the only type in the table that is specific to it." No mention of MySQL or SQLite. - -**MongoDB `_id`, and `@@map` unexplained** — one sentence added to the primary-keys paragraph: "`@map("_id")` gives the field that name in the database, and `@@map("users")` names the collection the model is stored in. Leave `id` out of the `create` call and MongoDB assigns the `ObjectId` for you." - -**Reference / lookup tables** — defined at first use ("A reference table, one that holds a fixed set of values the rest of your data points at"), and the second term is now "other reference tables" instead of "lookup tables". - -**Random UUIDs (line 119)** — index internals cut, the sortable option named: "`@default(uuid())` gives you a random UUID. Write `@default(uuid(7))` instead for a UUID that starts with the time it was created, so new ids sort after older ones." - -**The four-fact paragraph (line 163)** — split into two paragraphs. The `field.json()` and value-objects sentence is gone. `Numeric` now has syntax: "Write `price Decimal` when any precision will do, and `price Numeric(10, 2)` when you want an explicit precision and scale. Both become a PostgreSQL `numeric` column." `Temporal.Instant` is named as "the standard JavaScript type for a point in time" with a clause linking `/orm/reference/raw-queries` for Node.js versions and the polyfill. - -**Extensions (line 167)** — architecture claim cut, one linked sentence: "[Extensions](/orm/extensions) add more types, such as vectors or geometry." - -**Enums** — one sentence after the modifiers: "A field can also hold one of a fixed set of values, which you declare as an [enum](/orm/contract-authoring/psl-syntax#enums)." - -**Decimal versus integer cents** — reconciled in the money paragraph: use `Decimal` when amounts need more decimal places than the currency's smallest unit, `Int` otherwise, because a plain number is easier to work with than a decimal string. - -**"The wider type today is free"** — replaced with the real cost: `BigInt` reaches your code as a JavaScript `bigint`, which `JSON.stringify` refuses to serialize, so convert it to a string first. - -**The other side of a relation** — the example now shows `User` with `posts Post[]` above `Post`, the lead-in reads "each model in it needs the fields for its own side", and one sentence follows: "`posts Post[]` on `User` is the other end of the same relation, and like `author` it stores nothing in the database." - -**After I write the models** — a Next steps bullet: "[Generate a migration](/orm/migrations/generating-a-migration) to get the models you wrote into the database: run `npx prisma contract emit` after every change to the contract, then plan and apply a migration." No flags restated. - -**"skills" (line 241)** — unchanged; the reader accepted it and it is linked. - -## Cuts to stay in budget - -- The `ObjectId` code block in "Which surrogate type to pick" duplicated the MongoDB primary-key tab. It is now one prose sentence pointing at that tab. -- One comment line trimmed in the `Invoice` example. -- Also replaced "shape"/"shapes" for relations with "kinds" (banned unexplained), and "a field's shape" with "a field". - -## Facts looked up - -- `uuid()`, `uuid(4)`, `uuid(7)` accepted: `/Users/will/Projects/prisma/web/.claude/worktrees/pr-conflicts-review-fb93f7/wip/prisma-src/packages/3-targets/6-adapters/postgres/src/core/control-mutation-defaults.ts:141`. The generator table at `wip/prisma-src/packages/1-framework/2-authoring/ids/src/index.ts:57-64` names `uuidv7` and `uuidv4` but says nothing beyond storage width, so the page claims only that a version 7 value starts with its creation time and therefore sorts after older ids. It makes no indexing claim. -- Link targets verified to exist: `apps/docs/content/docs/orm/coming-from-prisma-orm-7.mdx`, `orm/extensions/index.mdx`, `orm/migrations/generating-a-migration.mdx`, and the `## Enums` heading in `orm/contract-authoring/psl-syntax.mdx`. - -## Q list - -None. Every mark was resolvable from the verified facts in the task or the source. - -Line count: 252 before, 258 after. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/index.fix2.md b/docs/orm-docs-audit/c21-data-modeling/index.fix2.md deleted file mode 100644 index 131699fef88..00000000000 --- a/docs/orm-docs-audit/c21-data-modeling/index.fix2.md +++ /dev/null @@ -1,56 +0,0 @@ -Round two edits are done on `/Users/will/Projects/prisma/web/.claude/worktrees/pr-conflicts-review-fb93f7/wip/c21/dm/r2/index.mdx`. Line count 258 before, 258 after. `check-plain.sh` clean, no em-dashes, no page-conventions banned words. - -## Marks and fixes - -**Contradiction, line 22 vs 241.** Removed "and other SQL databases" from the Next steps bullet and from the Relations bullet; the polymorphic bullet now says "on PostgreSQL". Only PostgreSQL and MongoDB are named anywhere. - -**"identity and its own history".** Abstract sentence deleted. Now: "Two posts with the same title are still two different records, and a user who changes their email address is still the same user." - -**MongoDB `id`.** "The `create` input type makes `id` optional, so leaving it out type-checks and MongoDB assigns the `ObjectId` for you." Same sentence now also covers `@@map`: "names the collection and is optional: without it, the collection is the model name with a lowercase first letter." - -**Reference table / ISO code.** "Natural keys fit reference tables best: tables that hold a fixed set of values other records point at. You can identify a country by its ISO code, and the code never changes." - -**"The column names in the table are PostgreSQL columns".** Now: "Where a row above names a PostgreSQL type such as `numeric` or `jsonb`, that is the column type Prisma ORM uses on PostgreSQL." - -**`Decimal` "so" backwards, and `Numeric` unintroduced.** "A `Decimal` value reaches your code as a string, so nothing is rounded through a JavaScript number. Write `price Decimal` when any precision will do. Write `price Numeric(10, 2)` when you want an explicit precision and scale. `Numeric` is `Decimal` with the number of digits written out. Both become a PostgreSQL `numeric` column." - -**Temporal / raw-queries pointer.** Replaced with "`Temporal` is built into Node.js 26.8.2 and later. On older versions, install the `temporal-polyfill` package." - -**Money paragraph.** Two sentences: "For ordinary money, use `Int` and store the amount in the currency's smallest unit, such as cents, because you get a plain JavaScript number. Use `Decimal` only when you need more decimal places than the currency has, such as a per-unit rate." - -**"each model needs the fields for its own side".** Now matches the example: "Two kinds of field describe a relation. The model that holds the link needs both of them. The model on the other end needs only a relation field." - -**"Skills".** Now "the [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8), instruction files for coding agents." - -**"four things" claimed completeness.** Now "A contract is built from four building blocks:". - -**The whole file.** Added to the intro: "There is no `datasource` or `generator` block any more, and the connection string is in `prisma.config.ts` instead. [A complete contract](/orm/contract-authoring/psl-syntax#a-complete-contract) shows a whole file next to its config." - -**Enums.** Kept as a sentence plus link rather than a block: the smallest valid enum needs `@@type("pg/text@1")`, and page-conventions bans type ids on these pages. Now: "You declare those values in an `enum` block that lists each member and says how the members are stored, and [enums](...) shows the block to copy." - -**`uuid(7)`.** "`@default(uuid())` and `@default(uuid(4))` both give you a random UUID." - -**Migration commands.** Next steps bullet now names all three: `npx prisma contract emit`, then `npx prisma migration plan --name `, then `npx prisma db migrate`. - -**No primary key.** "`npx prisma contract emit` accepts a model without a primary key. The ORM client does not: `update` and `delete` need a primary key or a unique field, and on a model with neither they fail with an error whose `code` is `ORM.ROW_IDENTITY_MISSING`." - -**`Json` vs `Jsonb`.** "so use `Jsonb` unless you specifically need PostgreSQL's plain `json` column." - -**`UserTag` links nothing.** Added the clause and link: "The two relation fields that point back at `User` and `Tag` are left out here to keep the key in view, and [many-to-many](/orm/data-modeling/relational-databases#many-to-many) shows the complete model." - -**One-to-one and many-to-many.** One clause each in the list: one-to-one is a one-to-many with `@unique` on the field that stores the link; many-to-many needs a model for the join table. - -**Mechanism cuts (to stay within budget).** Dropped "which MongoDB generates without asking any other server and which carries its own creation time", and the second "it stores nothing in the database" on `posts Post[]`. Kept the first one and kept the sequential-id warning the reader said earned its place. - -**Not changed.** Title stays "Overview" (per your instruction). "contract", "PSL", "emit", "polymorphic collections" left to the pages that define them; the contract sentence already carries the standard first-use gloss, and polymorphic collections keep their link to the MongoDB page. - -## Facts looked up - -- No primary key: `wip/prisma-src/packages/3-extensions/sql-orm-client/src/collection.ts:2422` — `update()`/`delete()` throw `ORM.ROW_IDENTITY_MISSING` when the table has no primary key or unique constraint. -- Enum syntax requires `@@type`: `apps/docs/content/docs/orm/contract-authoring/psl-syntax.mdx:183-196`. -- Complete contract plus `prisma.config.ts`, no `datasource`/`generator`: `apps/docs/content/docs/orm/contract-authoring/psl-syntax.mdx:13-125`. -- `#many-to-many` anchor exists: `apps/docs/content/docs/orm/data-modeling/relational-databases.mdx:105`. - -## Q list - -None. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/index.fix3.md b/docs/orm-docs-audit/c21-data-modeling/index.fix3.md deleted file mode 100644 index ce6e884e582..00000000000 --- a/docs/orm-docs-audit/c21-data-modeling/index.fix3.md +++ /dev/null @@ -1,28 +0,0 @@ -Done. Edited only `/Users/will/Projects/prisma/web/.claude/worktrees/pr-conflicts-review-fb93f7/wip/c21/dm/r3/index.mdx`. 258 lines before, 257 after. `check-plain.sh` is clean, no em-dashes, no page-conventions banned words. - -**Marks and fixes** - -- "contract" undefined: kept the conventions wording and replaced "You still describe your models in it" with "It holds your models, as `schema.prisma` did." -- "both databases Prisma ORM supports today": now "Prisma ORM 8 supports PostgreSQL and MongoDB only. All four blocks apply to both." -- "Two posts with the same title..." sentence: cut. -- Primary key paragraph: "Prisma ORM accepts a model without one, but `update` and `delete` on that model fail at runtime, because they need a primary key or a unique field." The error code is gone, and `prisma contract emit` no longer appears before it is explained. -- "the `create` input type": "Omit `id` when you create a document. MongoDB assigns it." -- Composite key commentary: "The same model is shown with its relation fields under [many-to-many](/orm/data-modeling/relational-databases#many-to-many)." -- How to read the type table: one sentence pair, "Where a row names a PostgreSQL type such as `numeric`, that is the column type on PostgreSQL. `ObjectId` is the only MongoDB-only type." -- `Numeric(10, 2)`: now introduced in the table's `Decimal` row (`` `Decimal`, `Numeric(10, 2)` ``); the "`Numeric` is `Decimal` with the number of digits written out" sentence and the redundant "Both become a PostgreSQL `numeric` column" are cut. -- `Json` vs `Jsonb`: the paragraph opens "`Json` and `Jsonb` are two separate types." -- Enum: added the five-line block with `@@type("pg/text@1")` plus one clause, "says how the values are stored, here as PostgreSQL `text`", and the link for the rest. -- Relation field naming: named once as "the link field, the one that stores the other record's primary key", and `fields` is now explained as "names the link field". The lead-in names both kinds before the bullets. "It stores nothing in the database" became "You do not get a column for it." "the connecting field" later in the section became "the link field". -- One-to-one: "The model that holds the link gets `@unique` on its link field, and the field on `User` is `profile Profile?`, not a list." -- Many-to-many: added "List fields on both sides with no such model are rejected, so write the model out as [many-to-many](...) shows." -- `temporal-polyfill`: "install the `temporal-polyfill` package and add `import "temporal-polyfill/full/global"`." -- `BigInt`: "Call `.toString()` on it first." -- Next steps commands: each gets its own short sentence (emit checks the contract and regenerates your types; `migration plan --name ` writes a migration file; `db migrate` applies it). -- Skills sentence moved to the conventions wording; "The `prisma-8` skill covers contract authoring" cut for room. -- Not changed, as instructed: no `prisma.config.ts` snippet, no project-setup section; "polymorphic collections" still links to the MongoDB page rather than being redefined. - -**Line budget paid for** by cutting the "two posts" sentence, folding the MongoDB surrogate-key sentence into the UUID paragraph, folding the Extensions sentence into the `Jsonb` paragraph, folding "`posts Post[]` is the other end" into the `@relation` paragraph, and collapsing the two modifier bullets into one line. - -**Facts checked in-repo** (both already verified for me, confirmed against the docs source): enum block at `apps/docs/content/docs/orm/contract-authoring/psl-syntax.mdx:188-196`; polyfill import at `apps/docs/content/docs/orm/reference/raw-queries.mdx:94`. - -**Q list:** none. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/index.fix4.md b/docs/orm-docs-audit/c21-data-modeling/index.fix4.md deleted file mode 100644 index b92ade55e8f..00000000000 --- a/docs/orm-docs-audit/c21-data-modeling/index.fix4.md +++ /dev/null @@ -1,45 +0,0 @@ -Done. Page is at `/Users/will/Projects/prisma/web/.claude/worktrees/pr-conflicts-review-fb93f7/wip/c21/dm/r4/index.mdx`, 257 lines before, **252 after**. `check-plain.sh` is clean, no em-dashes, no extra banned words, no semicolons in prose. - -## What I did, mark by mark - -**Sentences the reader could not restate** - -- **Line 13, four facts in one paragraph.** Split in two. First paragraph ends at "Prisma ORM derives your TypeScript types and your migrations from it." Second: "There is no `datasource` or `generator` block any more, and the connection string is in `prisma.config.ts` instead. [A complete contract](...) shows a whole file, and that page shows the config file too." -- **"coming from Prisma ORM 7" did not cover a 6 user.** Link text now reads "If you are [coming from Prisma ORM 6 or 7](/orm/coming-from-prisma-orm-7), that page covers the rename and the rest of the differences." -- **Line 151 table note.** Now "The PostgreSQL names in the table are the column types Prisma ORM creates." -- **"the ORM filter".** Now `.where()`: "On a `Jsonb` field, `.where()` compares the whole value. You can ask whether the column equals a given document, but not whether a key inside it matches. A `Json` field cannot be compared in `.where()` at all..." -- **`@@type("pg/text@1")` half-explained.** Now "`@@type` is required on every enum and says how the values are stored, here as PostgreSQL `text`. `pg/text@1` is the PostgreSQL type plus a version, always `@1` today. On MongoDB, write `@@type("mongo/string@1")`." -- **BigInt riddle.** Now "Changing `Int` to `BigInt` later needs a migration. Pick `BigInt` now and you skip it." -- **Optional fields with no example.** Now "...only when "absent" means something different from a sensible default, such as `publishedAt DateTime?` where no value means unpublished." -- **Comma splice on relation fields.** Now "A relation uses two kinds of field. The model that holds the link needs both. The model on the other end needs only a relation field:" and the first bullet says "The link field stores the other record's primary key, like `authorId`. Prisma ORM 7 called this the relation scalar field." -- **"are rejected" / implicit many-to-many.** Now "Prisma ORM 7 let you write a list field on both sides with no such model. That is gone, and `npx prisma contract emit` rejects it." - -**"So what do I type?"** - -- **Connection string.** The complete-contract clause now says that page shows the config file too. No config snippet added, per the brief. -- **MongoDB id.** "The field takes no `@default`. Omit `id` when you create a document and MongoDB assigns it." -- **Temporal polyfill.** "...add `import "temporal-polyfill/full/global"` in your app's entry file, before any query runs." -- **One-to-one.** Kept the prose and added the link: "[One-to-one](/orm/data-modeling/relational-databases#one-to-one) has both models written out." I did not add the code block; the cuts did not pay for seven lines, and the target page already shows both models in full. -- **Commands.** Now an ordered sequence: "Edit the contract, then run `npx prisma contract emit` to check it and regenerate your types, then `npx prisma migration plan --name ` to write a migration file, then `npx prisma db migrate` to apply it. `npx prisma contract emit` replaces `prisma generate`." -- **`Decimal` vs `Numeric(10, 2)`.** Table cell is now "`Decimal`, or `Numeric(10, 2)` with a precision and scale", and the duplicate prose about both spellings is gone. -- **"reference tables" before its gloss.** Reordered: "Some tables hold a fixed set of values that other records point at. Natural keys fit these reference tables best." - -**Mechanism instead of instruction** - -- **Primary key paragraph trimmed** to the instruction plus one trailing clause: "Give every model one and mark it with `@id`, because `update` and `delete` fail at runtime on a model that has neither a primary key nor a unique field:" -- **Duplicate "leaks row counts".** Cut "and the value tells nobody how many records you have" from the UUID paragraph; kept the autoincrement sentence. -- **`uuid(4)` equivalence cut.** "Write `@default(uuid(7))` for ids that sort by creation time. `@default(uuid())` is random." - -**Not redefined, by the section's rules:** polymorphic collections and relations (defined on `/orm/data-modeling/mongodb` and `/orm/data-modeling/relational-databases`, both already linked); relation kinds and foreign key placement (linked). - -**Cuts that paid for the additions:** the second `Product`/`sku` example model in "Surrogate keys" (the `User` example already makes the point), and the `Decimal` paragraph merged into the `DateTime`/`Bytes` paragraph. - -## Facts looked up - -- MongoDB enum `@@type("mongo/string@1")` — `wip/prisma-src/examples/mongo-demo/src/contract.prisma:3-4`. -- `#one-to-one` anchor exists and shows `User` plus `Profile` with `userId Int @unique` — `apps/docs/content/docs/orm/data-modeling/relational-databases.mdx:73-91`. - -## Q list - -- **Q1.** The reader asked how to add two `Decimal` values when they arrive as strings. I found no guidance in the rc.10 tree (searched `wip/prisma-src/examples/**` and the demo `main.ts` for decimal handling; only aggregate results such as `avgDecimal` returning a decimal string turned up). The page still says only "use `Int` in the currency's smallest unit for ordinary money, `Decimal` when you need more decimal places". No claim invented. -- **Q2.** I did not add "MySQL and SQLite are not options" to the moved support sentence, even though the reader's biggest surprise was MySQL. The source tree contains `wip/prisma-src/examples/prisma-8-demo-sqlite/`, so naming SQLite as unsupported would have been a new claim I cannot back. The sentence now sits in the intro, before the four-block list, as instructed. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/index.round1.md b/docs/orm-docs-audit/c21-data-modeling/index.round1.md deleted file mode 100644 index 6b5542923e1..00000000000 --- a/docs/orm-docs-audit/c21-data-modeling/index.round1.md +++ /dev/null @@ -1,67 +0,0 @@ -Read once, top to bottom, as someone coming from Prisma 6. Report below. - -## Sentences I could not restate after one reading - -**Line 13** — "In Prisma ORM, you define this structure in your contract, the `contract.prisma` file that replaced `schema.prisma`." -Stopped by: "your contract." The page uses it as a noun I already own before it ever says what a contract is. "The file that replaced `schema.prisma`" tells me the filename changed but not whether the contents changed, whether my existing file still works, or whether something renames it for me. Plainest version I'd have understood: "Prisma 8 renames `schema.prisma` to `contract.prisma`. The file is called your contract. It holds the same model definitions you wrote before, plus X." - -**Line 13, second half** — "Prisma ORM derives your TypeScript types, your migrations, and the client's configuration from it." -Stopped by: "the client's configuration." In v6 my `generator` and `datasource` blocks lived in `schema.prisma`. Is "the client's configuration" those blocks? A new thing? The page shows four building blocks and none of them is configuration, so I can't place this. - -**Line 163** — "A `Decimal` value reaches your code as a decimal string, so binary floating point never loses digits. Write the type as `Numeric(10, 2)` when you want an explicit precision and scale. A `DateTime` value reaches your code as a `Temporal.Instant`, and a `Bytes` value as a `Uint8Array`. `Jsonb` is also the storage the TypeScript builder's `field.json()` and value objects use." -Four unrelated facts in one paragraph, and the last one I cannot resolve at all. "The TypeScript builder" is never introduced anywhere on the page and is not linked. "Value objects" likewise. `field.json()` is a method on something I have not been told exists. I got nothing from that sentence. -Also inside it: "Write the type as `Numeric(10, 2)`" — `Numeric` is not in the type table two lines above, which lists `Decimal`. Are they the same type? Two types? Is this `@db.Decimal(10, 2)` renamed? I could not tell. - -**Line 167** — "Prisma ORM's type system is extensible, so extensions can add more types, such as vectors or geometry." -"Extensions" here could mean Prisma Client extensions (which I used in v6 for query middleware, and which had nothing to do with schema types) or something new. No link. I'd cut it or link it. - -**Line 205** — "Widening a type later is a migration. The wider type today is free." -"Is free" I could not restate honestly. `BigInt` is not free in the client — it comes back as a JS `bigint`, which does not `JSON.stringify`. That's the cost I actually hit in v6, and the page says there isn't one. - -**Line 119** — "A UUID is wider than an integer and random UUIDs index a little worse, but independent services never collide and nothing is leaked." -"Random UUIDs" implies non-random ones exist and are better. The example right above is `@default(uuid())`. Which kind do I get? If there's a sortable option, the page tells me to want it and not how to get it. - -## Words and phrases I had to guess - -- **"contract"** — guessed: the new name for the schema, same concept. -- **"the client's configuration"** — guessed: the `generator` block. -- **"the TypeScript builder"** — guessed: some way of defining models in TypeScript instead of `.prisma` files? Genuinely a guess. -- **"value objects"** — guessed: no idea. Some Prisma 8 concept. -- **"`Temporal.Instant`"** — guessed: the TC39 Temporal API. Do I need Node 22+? A polyfill? Not said. -- **"`ObjectId`"** as a Prisma type (line 53) — guessed: replaces v6's `String @db.ObjectId`. -- **"`@@map("users")`"** (line 55) — guessed: table/collection name override. Appears in a code sample with zero mention in prose; it's the only attribute in the page that's shown without being explained. -- **"reference tables" / "lookup tables"** (lines 65, 74) — guessed from context; fine, but they're used as if already defined. -- **"skills"** (line 241) — guessed: agent instruction files. At least it's linked. - -## "So what do I actually type?" - -1. **Migrating an existing project.** The page says `contract.prisma` "replaced `schema.prisma`" and then never says what I do about it. Do I `git mv`? Does a codemod exist? Does Prisma read the old name? This is the single biggest gap for the audience that has a v6 app. -2. **Explicit precision.** "Write the type as `Numeric(10, 2)`" — write it *where*? `price Numeric(10, 2)`? `price Decimal @db.Numeric(10, 2)`? No code sample, and this is the one paragraph that tells me to type something and shows nothing. -3. **After I write the models.** The page ends at "Query your models." Nothing says how the contract reaches the database — no `prisma migrate dev`, no `prisma generate`, not even a link labeled as that step. -4. **MongoDB `_id`.** Line 53 shows `id ObjectId @id @map("_id")` with no `@default`. In v6 I wrote `@default(auto())`. Does Prisma 8 generate the ObjectId for me, or must I supply one on every `create`? The page states `_id` is mandatory and then hands me a model with no way to fill it. -5. **The other side of a relation.** The `Post` example (line 221) has `author User @relation(...)`. v6 required a matching `posts Post[]` on `User` or validation failed. The page never shows `User`, never mentions the back-relation, and says "Two kinds of field describe a relation" — which reads like two fields total, not two per side. I would have typed only the `Post` half and hit an error. -6. **Enums.** Line 245 tells my coding agent to find "fields that should be an enum," and the page never mentions enums — not in the type table, not in the "four things." Where do I declare one? -7. **Databases other than PostgreSQL.** The type table annotates every type with its PostgreSQL mapping only. I'm on MySQL. Does `Decimal` map? Does `Jsonb` exist? The page says "All four apply to every database" and then the only concrete table is PostgreSQL-specific. -8. **Decimal versus integer cents.** Line 155 offers `Decimal` as "exact decimal." Line 187 says "Money is not a `Float`" and tells me to store `priceCents Int`. So when do I use `Decimal`? The page gives me two answers for money and never reconciles them. - -## Internals I did not ask for - -- **Line 163**, `Jsonb` "is also the storage the TypeScript builder's `field.json()` and value objects use." That's a note about how another Prisma feature is implemented. I'm picking a column type. -- **Line 119**, "random UUIDs index a little worse." Useful as a tradeoff, but without a way to act on it it's just index internals. -- **Line 167**, the type system being "extensible." That's a statement about Prisma's architecture; it doesn't tell me to do anything. -- **Line 109**, "it leaks row counts and invites guessing" is fine — it changes my decision. - -## Could I do what the page is for, after one reading? - -Partly. I could write a plain PostgreSQL model with a surrogate key, pick sensible scalar types, and make a decent call between a natural and a surrogate key. The primary-key section is the strongest part and I'd have made the right choice from it. - -I could not: - -- Say what a "contract" is beyond a renamed file, or convert my existing v6 project to one. -- Write a working relation, because I was never shown the `User` side. -- Write a MongoDB model, because the `_id` example has no way to populate the id. -- Declare a decimal with precision, because the instruction has no syntax. -- Get from a finished contract to a database, because the page never names the command. -- Explain `Jsonb`'s relationship to whatever the "TypeScript builder" is — I still don't know what that is. - -The page's own framing is "four things." It covers three of them well enough to act on and leaves relations, the thing I actually came for, with half an example. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/index.round2.md b/docs/orm-docs-audit/c21-data-modeling/index.round2.md deleted file mode 100644 index c48037cc823..00000000000 --- a/docs/orm-docs-audit/c21-data-modeling/index.round2.md +++ /dev/null @@ -1,111 +0,0 @@ -Read once, top to bottom, as a Prisma 6 user who has never seen this version. - -## Sentences I could not restate after one reading - -**Line 22 vs line 241 — direct contradiction.** -> "All four apply to both databases Prisma ORM supports today, PostgreSQL and MongoDB." - -then - -> "[Relational data modeling](...) for PostgreSQL and other SQL databases" - -Which is it? I have a MySQL app. Line 22 says the ORM supports exactly two databases now; line 241 says "other SQL databases" exist. I stopped reading to figure out whether my database was dropped. The page never says. - -**Line 36.** -> "Every record of a model has its own identity and its own history." - -"Its own history" stopped me cold. Does Prisma now store record history/versions? I re-read the following sentence and concluded it's a metaphor for "it's the same record over time," but I genuinely could not tell on first pass. Plainest version: "Two posts with the same title are still two different records, and a user who changes their email is still the same user." — i.e. delete the abstract sentence, keep the concrete one. - -**Line 59.** -> "Leave `id` out of the `create` call and MongoDB assigns the `ObjectId` for you." - -The model above it is `id ObjectId @id @map("_id")` — required, no `@default`. In Prisma 6 I wrote `@default(auto())`. So will the generated TypeScript type actually let me omit `id`? The schema as printed says it's required. I could not restate this without guessing that ObjectId gets special-cased. - -**Line 65.** -> "A reference table, one that holds a fixed set of values the rest of your data points at, is the classic fit." - -A definition wedged inside a sentence, followed by "is the classic fit" — fit for what? (Natural keys, three sentences back.) Plainer: "Natural keys fit reference tables best — tables holding a fixed set of values that other records point at." - -**Line 66.** "A country is its ISO code, and the code never changes." A country is not its ISO code. You mean "identify a country by its ISO code." - -**Line 155.** -> "The column names in the table are PostgreSQL columns." - -The table's columns are "Type" and "Stores". I first read this as being about the table's own headers. I think you mean: "Where the table names a type like `numeric` or `jsonb`, that's the PostgreSQL column type Prisma uses." - -**Line 157 — two problems.** -> "A `Decimal` value reaches your code as a decimal string, so binary floating point never loses digits." - -The "so" is backwards. Binary floating point isn't involved at all; that's the point. Plainer: "Decimal values arrive as strings, so nothing is rounded through a JavaScript number." - -> "Write `price Decimal` when any precision will do, and `price Numeric(10, 2)` when you want an explicit precision and scale." - -`Numeric` is not in the type table directly above. Is it a type? A variant of `Decimal`? Where did it come from? This is the single most confusing line on the page. - -**Line 159.** -> "[Raw queries](/orm/reference/raw-queries) says which Node.js versions have `Temporal` built in and how to add it to older ones." - -I could not resolve why a raw-queries reference page would tell me about Node versions and Temporal polyfills. That's a runtime-requirements fact, and I'd never look there. - -**Line 185.** Too many ideas in one sentence, and it argues against itself: -> "Store an integer number of the smallest unit instead. `Decimal` is exact as well, so use it when your amounts need more decimal places than the currency's smallest unit, and use `Int` otherwise because a plain JavaScript number is easier to work with than a decimal string" - -First it tells me to use integer cents. Then it tells me `Decimal` is also exact. Then it gives a rule that requires me to hold "smallest unit," "decimal places," and "decimal string ergonomics" in my head at once. I read it three times. Plainer as two sentences: "Use `Int` and store cents for ordinary money — you get a plain JavaScript number. Use `Decimal` only when you need more decimal places than the currency has, like per-unit rates." - -**Line 211.** -> "Two kinds of field describe a relation, and each model in it needs the fields for its own side." - -Then the example gives `User` exactly one field (`posts Post[]`) and `Post` two. So "the fields" (plural) for its own side is wrong for `User`. I couldn't reconcile the rule with the example. - -**Line 246.** -> "Projects created with `npm create prisma@latest` include the [Prisma ORM skills](...) for your coding agent." - -"Skills" is used as an established noun and I don't know what it is. I guessed: some package of instructions an AI agent reads. - -## Words and phrases I had to guess - -- **"contract"** (line 13). I guessed: the new name for the schema file, and also a general noun for "the thing in that file." It's used both ways ("in your contract", "Every contract is built from") and I was never sure whether a contract is the file, the content, or a broader concept. -- **"PSL"** — never appears spelled out, but the link at line 170 is `/orm/contract-authoring/psl-syntax`. Guessed: Prisma Schema Language. -- **"emit"** (line 257, `npx prisma contract emit`). Guessed: the replacement for `prisma generate`. The page never says what it emits. -- **"surrogate key" / "natural key"** — these I know, but you define them, which is fine. -- **"Jsonb"** as a distinct Prisma type. In Prisma 6 there was only `Json`. Guessed: `Json` maps to `json`, `Jsonb` maps to `jsonb`, and I should almost always use `Jsonb`. The page never tells me which to choose, even though it says `Json` "offers no comparison at all" — so why would anyone pick it? -- **"polymorphic collections"** (line 242). Guessed: storing different shapes in one collection. Never defined. -- **"reference table"** — defined mid-sentence, see above. - -## "So what do I actually type?" - -1. **The whole file.** The page never shows a complete `contract.prisma`. Coming from Prisma 6 my first question is: are `datasource db { ... }` and `generator client { ... }` still in there? Do I still set `provider` and `url`? Not one word. -2. **Enums** (line 170). "A field can also hold one of a fixed set of values, which you declare as an [enum]." No syntax, not even a two-line block, in a page that shows a code block for everything else. -3. **`Numeric(10, 2)`** (line 157). Is that a type name I write literally? It's not in the table. -4. **MongoDB `id`.** Do I write `@default(auto())`, or truly nothing? See above. -5. **`uuid(7)`** (line 118). Is `uuid(4)` also valid? Is the bare `uuid()` the same as `uuid(4)`? Implied but not stated. -6. **Line 257.** "run `npx prisma contract emit` after every change to the contract, then plan and apply a migration." What are the plan and apply commands? You gave me one command and then described two more steps in prose without their commands. This is the last line of the page and the most actionable moment in it. -7. **`npx prisma skills sync`** (line 246) — fine, that one's concrete. -8. **Composite key example** (lines 126-133). `UserTag` has `userId` and `tagId` but no relation fields and no `@relation`. If I copy it, I have a model that links nothing. It's introduced as "a model that links two others" and then doesn't link them. -9. **One-to-one** (line 235). Listed as a kind of relation, no syntax shown, and the `@unique` that makes it one-to-one is never mentioned. -10. **What happens if I skip a primary key?** Line 42 says Prisma "needs" one. Error at emit time? Silent read-only model? Not said. -11. **`@@map("users")`** appears only in the MongoDB tab (line 55) with no explanation of why it's there and not in the PostgreSQL tab. Do I need it on Mongo? It's presented as part of the required MongoDB shape. - -## Places explaining internals when I wanted to know what to do - -- **Line 120.** "which MongoDB generates without asking any other server and which carries its own creation time." I don't get a choice here — you just told me the choice is made for me. The mechanism is trivia at that point. -- **Line 214 and again line 231.** "It stores nothing in the database." / "like `author` it stores nothing in the database." Said twice. The first time is useful. The second is padding. -- **Line 36.** "Every record of a model has its own identity and its own history." Philosophy, not a thing I can act on. -- **Line 108.** "It is also sequential, so it leaks row counts and invites guessing if you expose it in URLs." This one earns its place — it changes what I'd pick. - -## Could I do what the page is for, after one reading? - -Partly. I could write a model with a surrogate integer id, scalar fields, and a one-to-many relation. That's the core and it lands. - -What I would still not know: - -- Whether my MySQL database is still supported. -- What else goes in `contract.prisma` besides models — I've never seen a whole file. -- How to declare an enum, despite being told to use them. -- Whether `Numeric` is a type I can write. -- The commands to get my models into the database. "Plan and apply a migration" is not a command. -- How to write a one-to-one or many-to-many relation, or a working link model. The one link model shown is incomplete. -- When to use `Json` instead of `Jsonb`, given you told me `Json` can't be filtered at all. -- What to actually write for a MongoDB `id` field. - -Two more things worth fixing. **"Every contract is built from four things"** (line 15) is then contradicted by the page itself — enums are a fifth, and datasource/generator presumably a sixth and seventh. Say "four building blocks" and don't claim completeness. And the title is **"Overview"** while `metaTitle` is "Data modeling in Prisma ORM"; as a reader landing on a page called "Overview" I have no idea what it overviews until line 9. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/index.round3.md b/docs/orm-docs-audit/c21-data-modeling/index.round3.md deleted file mode 100644 index fe6eb724ca4..00000000000 --- a/docs/orm-docs-audit/c21-data-modeling/index.round3.md +++ /dev/null @@ -1,86 +0,0 @@ -Read once, top to bottom, as a Prisma 7 user. - -## Sentences I could not restate after one reading - -**Line 13** — "In Prisma ORM, you define this structure in your contract, the `contract.prisma` file that replaced `schema.prisma`." -Stopped by: "your contract." The page names the new word and immediately uses it as if I now know it. I know what a schema is. I do not know why this is called a contract, or whether "contract" means something more than "the file formerly called schema.prisma." Plainest version I'd have understood: "Prisma ORM 8 renames `schema.prisma` to `contract.prisma`. It holds the same model definitions; the file is called your contract." - -**Line 22** — "All four apply to both databases Prisma ORM supports today, PostgreSQL and MongoDB." -Stopped by the implication, not the words. I ship on MySQL. This sentence says my database is gone and then moves on to the next heading. Either say it plainly ("Prisma ORM 8 supports PostgreSQL and MongoDB only") or don't raise it here. - -**Line 36** — "Two posts with the same title are still two different records, and a user who changes their email address is still the same user." -Stopped by: I cannot tell what this sentence is for. It sits under a model code block, makes no claim about models, and nothing before or after picks it up. I assume it is a hint at primary keys, but the primary key section never refers back to it. - -**Line 42** — "`npx prisma contract emit` accepts a model without a primary key. The ORM client does not." -Two problems. First, `npx prisma contract emit` is used here with no explanation of what it does; the page does not say until line 257. Second, "The ORM client does not" reads as a parallel to "accepts," but a client doesn't accept a model — it fails at runtime. Plainer: "Prisma will generate a client for a model with no primary key, but `update` and `delete` on that model fail at runtime." - -**Line 59** — "The `create` input type makes `id` optional, so leaving it out type-checks and MongoDB assigns the `ObjectId` for you." -Stopped by "the `create` input type." That is the generated-types' name for something, and I only wanted to know: omit `id` when creating. Plainer: "Omit `id` when you create a document; MongoDB assigns it." - -**Line 155** — "Where a row above names a PostgreSQL type such as `numeric` or `jsonb`, that is the column type Prisma ORM uses on PostgreSQL. MongoDB has no such mapping, and `ObjectId` is the only type in the table that is specific to it." -Stopped by: this is a sentence about how to read the table I just read. Two ideas crammed together (what the PostgreSQL column names mean; which row is MongoDB-only). I had to reread to work out it wasn't telling me anything I do. - -**Line 157** — "Write `price Numeric(10, 2)` when you want an explicit precision and scale. `Numeric` is `Decimal` with the number of digits written out." -This is the worst sentence on the page for me. `Numeric` is not in the type table directly above. So is it a type? An alias? In Prisma 7 I wrote `Decimal @db.Decimal(10, 2)`, so I cannot tell whether `@db.` is gone, whether `Numeric` is a new scalar, or whether every type now takes arguments. "with the number of digits written out" is a guess-invitation, not a definition. - -**Line 161** — "Filtering on a `Jsonb` field is whole-value equality only: you can ask whether the column equals a given document, not whether a key inside it matches." -Restatable on the second read, not the first. Also unexplained: `Json` and `Jsonb` are now two separate scalar types. In Prisma 7 there was one `Json` type. The page never flags the change. - -**Line 170** — "You declare those values in an `enum` block that lists each member and says how the members are stored." -Stopped hard by "says how the members are stored." In Prisma 7 an enum is just a list of names — storage is not part of it. This sentence tells me the syntax changed and then refuses to show me the syntax, linking away instead. An enum block is four lines. Show it. - -**Line 211** — "Two kinds of field describe a relation. The model that holds the link needs both of them. The model on the other end needs only a relation field." -Three abstractions stacked before either kind of field has been named. I had to read the bullets, then come back up to this paragraph for it to parse. - -**Line 213** — "The field that stores the other record's primary key, like `authorId`." -The second bullet is named ("A relation field"). This one is not. I never learn what to call it, which matters because the next paragraph says `fields` "names the local field" — a third name for the same thing. - -**Line 235** — "One-to-one: a user has at most one profile. Write it as a one-to-many and add `@unique` to the field that stores the link." -Not restatable. "Write it as a one-to-many" — but a one-to-many has `Profile[]` on the user side. Do I write `Profile[]` and then it isn't a list? Or `Profile?`? The page tells me to do something whose result contradicts its own description, and shows no code. - -## Words and phrases I had to guess - -- **"contract"** — guessed: the new name for schema.prisma, nothing more. -- **"emit"** in `prisma contract emit` — guessed: the new name for `prisma generate`. -- **"the ORM client"** (line 42) — guessed: what used to be called Prisma Client. The page never says they are the same thing. -- **"`ORM.ROW_IDENTITY_MISSING`"** — guessed: an error code string I could match on in a catch block. Not told where I see it. -- **"`Numeric(10, 2)`"** — guessed: a scalar type that takes precision arguments, replacing `@db.Decimal`. -- **"`Jsonb`"** as a distinct type — guessed: what `Json @db.JsonB` used to be. -- **"a model for the join table"** (line 237) — guessed: the thing that used to be called an explicit many-to-many relation table. -- **"skills"** / **"the `prisma-8` skill"** (line 246) — guessed: instruction files for Claude/Cursor. The page half-defines it ("instruction files for coding agents") but I still don't know what `npx prisma skills sync` writes or where. -- **"polymorphic collections"** (line 242, 256) — guessed: storing different shapes in one collection. Never explained, appears twice. - -## Where I asked "so what do I actually type?" - -1. **Line 13, `prisma.config.ts`.** I'm told the connection string moved there and the `datasource` block is gone. That is the single change most likely to break my first five minutes, and the page shows zero lines of it. Not even a three-line snippet. -2. **Line 13, where does `contract.prisma` go?** Same directory as `schema.prisma` did? Is there a rename command, or do I move the file myself? -3. **Line 170, enums.** Told the block "says how the members are stored" and sent to another page. Show the block. -4. **Line 235, one-to-one.** Described in prose only. I cannot write it from this. -5. **Line 237, many-to-many.** "This needs a model for the join table." In Prisma 7 I could write `tags Tag[]` / `posts Post[]` and get an implicit join table. Is implicit many-to-many removed? The page implies yes by omission and never says so. No code shown. -6. **Line 159, `Temporal`.** "install the `temporal-polyfill` package" — do I then need to import or configure anything, or does Prisma pick it up? -7. **Line 203, `BigInt`.** "Convert it to a string before you send it anywhere as JSON." Where — in my serializer, per field, globally? No snippet. -8. **Line 135, the composite key example.** "The two relation fields ... are left out here." So if I copy this block, does it compile? I can't tell. -9. **Line 257, the three new commands.** `npx prisma contract emit`, `npx prisma migration plan --name `, `npx prisma db migrate`. All three replace `prisma migrate dev` and all three appear for the first time in one bullet, with no statement of what each does or when. The naming is also inconsistent — `migration plan` versus `db migrate` — which made me read the bullet three times to be sure it wasn't a typo. - -## Where it explains internals when I wanted instructions - -- **Line 42**, `ORM.ROW_IDENTITY_MISSING`. The instruction is "give every model a primary key." The error code is machinery. It sits in the middle of the instruction and interrupts it. -- **Line 59**, "The `create` input type makes `id` optional, so leaving it out type-checks." I wanted "omit `id`." I got the mechanism by which omitting it works. -- **Line 155**, the paragraph explaining how to read the type table's PostgreSQL column. -- **Line 214**, "It stores nothing in the database. It tells Prisma ORM how to follow the connection in a query." This one is borderline — it does correct a real misconception — but it's framed as how Prisma works rather than as "you don't get a column for this." -- **Line 135**, "are left out here to keep the key in view." Commentary about the docs' own editing choices. - -## After one reading, could I do what the page is for? - -Partly. The middle of the page is genuinely usable: I could write models, pick a primary key strategy, and choose scalar types from the "How to pick a data type" section without rereading. That section is the best writing on the page — concrete rule, concrete reason, code. - -What I still could not do: - -- **Set up a Prisma 8 project at all.** I know `datasource` and `generator` are gone and that a `prisma.config.ts` exists. I have never seen one. I cannot connect to a database from this page. -- **Write an enum.** The page describes syntax it declines to show. -- **Write a one-to-one or a many-to-many relation.** Prose only, and the one-to-one instruction contradicts itself as written. -- **Write a `Decimal` with fixed precision.** `Numeric(10, 2)` appears from nowhere and is not in the type table. -- **Run a migration.** Three unfamiliar commands in one bullet, none explained, in a "Next steps" list rather than anywhere I'd look for them. -- **Know whether my MySQL app has a path forward.** "both databases Prisma ORM supports today" is the only signal, and it's dropped in a transitional sentence. - -The page is billed as the four building blocks. It covers three of them well and the fourth — relations — mostly by pointing elsewhere. That's defensible for a hub page. The unforced errors are the undefined new vocabulary ("contract", "emit", "ORM client"), the `Numeric` type that isn't in its own type table, and the enum paragraph that describes syntax instead of printing it. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/index.round4.md b/docs/orm-docs-audit/c21-data-modeling/index.round4.md deleted file mode 100644 index 8f18443af88..00000000000 --- a/docs/orm-docs-audit/c21-data-modeling/index.round4.md +++ /dev/null @@ -1,70 +0,0 @@ -Read once, top to bottom, as a Prisma 6 user who has never seen v8. - -## Sentences I could not restate after one reading - -**Line 13** — "In Prisma ORM, you define this structure in your contract, the `contract.prisma` file that replaced `schema.prisma`." -Stopped by "your contract." The page uses the word as if I should already know why a schema file is now called a contract. Plainest version: "Prisma 8 renames `schema.prisma` to `contract.prisma`. It still holds your models." Also: four separate facts land in one paragraph (rename, types/migrations derived from it, no `datasource`/`generator`, connection string moved). I had to reread to separate them. - -**Line 151** — "Where a row names a PostgreSQL type such as `numeric`, that is the column type on PostgreSQL." -Abstract and self-referential; I read it three times. Plainer: "The PostgreSQL names in the table are the column types Prisma creates." - -**Line 157** — "Filtering on a `Jsonb` field is whole-value equality only: you can ask whether the column equals a given document, not whether a key inside it matches. A `Json` field offers no comparison at all in the ORM filter." -"the ORM filter" — I do not know what that names. The `where` clause? A specific API? Two types, two different restrictions, and a workaround, all in one paragraph. - -**Line 171** — "`@@type(\"pg/text@1\")` says how the values are stored, here as PostgreSQL `text`." -This explains the half I could already guess and skips the half I could not: what `pg/` and `@1` mean, whether it is required on every enum, and what else I am allowed to put there. - -**Line 204** — "Widening a type later is a migration, so picking the wider type now saves you one." -Compressed to the point of riddle. Plainer: "Changing `Int` to `BigInt` later requires a migration. Pick `BigInt` now and you skip it." - -**Line 206** — "Mark a field optional (`?`) only when \"absent\" means something different from a sensible default. A required field with a default is often the clearer model." -I understand the words and cannot picture the case. No example. Everything else in this section has one. - -**Line 212** — "Two kinds of field describe a relation, the link field and the relation field. The model that holds the link needs both of them." -Comma splice, and "the model that holds the link" arrives before I know what "the link" is. Plainer: "A relation uses two fields. One stores the other record's id. The other is typed as the other model and exists only so Prisma can follow the relation." - -**Line 236** — "List fields on both sides with no such model are rejected, so write the model out as many-to-many shows." -Rejected by what, and when — at `contract emit`? I also had to work out on my own that this means implicit many-to-many (`posts Post[]` / `tags Tag[]` with no join model) is gone, which is a significant break from what I have been writing for two years. The page never says so. - -## Words and phrases I had to guess - -- **"contract"** — guessed: just the new name for the schema file, with no behavioral meaning. Still not sure that guess is right, because the word is doing a lot of work (`prisma contract emit`). -- **"link field"** (line 214) — guessed: what I have always called the relation scalar field, e.g. `authorId`. The page never says these are the same thing. -- **"the ORM filter"** (line 157) — guessed: the `where` argument in queries. -- **"reference tables"** (line 63) — guessed correctly from the gloss that follows, but it is stated before the gloss. -- **"polymorphic collections"** / **"polymorphic relations"** (lines 241, 254) — guessed: some way to point at more than one model. Appears only in link text, never explained. -- **"`pg/text@1`"** (line 165) — guessed: PostgreSQL's `text` type, version 1 of some mapping registry. No idea what the `@1` is for. -- **"Prisma ORM skills"** (line 245) — guessed: markdown instruction files a coding agent reads. - -## Where I asked "so what do I actually type?" - -1. **Line 13, the connection string.** "the connection string is in `prisma.config.ts` instead." I have no idea what that file looks like. This is the single thing that stops me writing a working project, and the page moves on. The "complete contract" link may have it, but the sentence does not say so. -2. **Line 51, the MongoDB id.** `id ObjectId @id @map("_id")` — no `@default`. In v6 I wrote `@default(auto())`. Do I add a default or not? "Omit `id` when you create a document. MongoDB assigns it." implies not, but does not say it. -3. **Line 155, the Temporal polyfill.** "add `import \"temporal-polyfill/full/global\"`" — add it where? Top of every file? One entry file? A Next.js `instrumentation.ts`? -4. **Line 153, `Decimal` arithmetic.** "A `Decimal` value reaches your code as a string" — so how do I add two of them? The money advice below implies I should not, but never says what to do if I must. -5. **Line 234, one-to-one.** Prose only. Every other relation shape gets a code block or a link. I wanted the four lines of `contract.prisma`. -6. **Line 165, enums.** Is `@@type` mandatory? What do I write for MongoDB? The block shown would be my template and I do not trust it. -7. **Line 256, the commands.** Three commands in one bullet with no ordering or "run this when": - `npx prisma contract emit`, `npx prisma migration plan --name `, `npx prisma db migrate`. I also cannot tell whether `contract emit` is what replaced `prisma generate` — `generate` is never mentioned, and I have typed it for two years. -8. **Line 143, `Decimal` vs `Numeric(10, 2)`.** One table row, two spellings. Is `Numeric` a distinct type or `Decimal` with arguments? Line 153 implies the latter; the table implies the former. - -## Where it explains the insides when I wanted instructions - -- **Line 40** — "Prisma ORM accepts a model without one, but `update` and `delete` on that model fail at runtime, because they need a primary key or a unique field." Two clauses about validator behavior for one instruction: give every model a primary key. The runtime detail is fine as a trailing note, but it takes over the paragraph. -- **Line 106** — "it leaks row counts and invites guessing if you expose it in URLs." Reasonable, but combined with line 116's "the value tells nobody how many records you have," the same point is made twice in ten lines. -- **Line 116** — "`@default(uuid())` and `@default(uuid(4))` both give you a random UUID." I did not ask which spellings are equivalent. I asked which one to write. Say "write `@default(uuid(7))`" and drop the equivalence. - -## Could I do what the page is for, after one reading? - -Partly. The page is an overview, and I came away able to write a model, choose `@id` versus `@@id`, pick between a natural and a surrogate key, pick scalar types, and write a one-to-many relation. That part works. - -What I still would not know: - -- How to get from this page to a project that runs. The connection string moved to a file the page names once and never shows. Without it nothing I wrote here connects to anything. -- What `contract` means beyond a filename, and therefore what `npx prisma contract emit` does. -- Whether `prisma generate` still exists. -- How to write an enum I can trust, because `@@type("pg/text@1")` is unexplained. -- That implicit many-to-many is gone — I inferred it from "are rejected," and I would have written `Post[]` / `Tag[]` on both sides out of habit and been surprised. -- That MySQL is no longer supported until line 22, which is one sentence deep in a section about building blocks. For a v6 user on MySQL this is the most consequential sentence on the page and it is not treated as such. - -One more, as an existing user: line 13 points to "what changed" for people "coming from Prisma ORM 7." I am coming from 6. The page does not say whether that link covers me. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/mongodb.fix1.md b/docs/orm-docs-audit/c21-data-modeling/mongodb.fix1.md deleted file mode 100644 index d53c7d74d2f..00000000000 --- a/docs/orm-docs-audit/c21-data-modeling/mongodb.fix1.md +++ /dev/null @@ -1,54 +0,0 @@ -Done. 173 lines (was 167), budget 175. `check-plain.sh` clean, no em-dashes, no page-conventions banned words. - -## What I did with each mark - -**Sentences the reader could not restate** - -- **Line 87, pipeline builder / embedded filters.** Split into two plain facts and named the thing on first use: "`.where()` filters on top-level model fields only. To match on a field inside an embedded document, use the pipeline builder, the API for MongoDB aggregation pipelines, with a path such as `address.city`." -- **Line 118, `$lookup` denormalization paragraph.** Cut `$lookup` entirely and reduced the paragraph to two sentences with no sync pattern: "Resolving a reference costs a second read. If a read runs constantly, you can copy the few fields it needs into the parent document, such as a comment storing its author's name." -- **Line 150, unexplained `db`.** `db` now first appears in the `_id` section with the conventions wording: "`db` is the client you create once in `src/prisma/db.ts`, and `prisma orm init` writes that file. `db.orm` holds your models, and on MongoDB the accessor is the collection name, so `db.orm.users`." Plus the clause "The values go straight in, without the `data:` wrapper Prisma ORM 7 used." -- **Line 152, colon construction.** Now: "Use one collection when you almost always query the variants together, for example email, SMS, and push notifications that you read as a single feed." - -**Words guessed at** - -- **`ObjectId`**: "`ObjectId` is the type of the field. There is no `@default(auto())` and no `@db.ObjectId`." -- **pipeline builder**: defined on first use (above). -- **`@@discriminator(kind)`**: "takes the name of the field that records each document's variant, so write `kind` and not `"kind"`." -- **`@@base(...)`**: explanation now follows directly after the block, together with the new inheritance sentence. -- **`.include(...)`**: explained where it first appears, with a one-line example: "chain `.include("author")`, which adds the related record to every row you get back". -- **`prisma-8` skill**: now "the Prisma ORM skills, instruction files for coding agents". -- **discriminator field**: defined inline, "a field whose value says which kind each document is". -- **"no independent life" / "own life" metaphors**: both cut for the page's literal wording ("you cannot read embedded data without its parent"; "each record stands on its own"). -- **`@@map` optional but always used**: added the consequence, "and you reach it in code as `db.orm.user`". - -**"So what do I type"** - -1. `_id` generation: one-line `create` with `id` omitted, plus the sentence that MongoDB assigns it. -2. Create with an embedded value: two-line example passing a plain nested object. -3. Update one field inside an embedded value: `u("address.city").set("Hamburg")`. -4. Filter on an embedded field: stated plainly plus the pipeline-builder link. -5. Create a polymorphic document: `db.orm.posts.variant("Article").create({...})`, plus "Creating through a variant fills in `kind` for you" and "A variant has every field of its base plus its own, so an `Article` has `id`, `title`, `kind`, and `summary`". Also stated `.variant()` takes the model name, not the discriminator value. -6. Applying the schema: one sentence naming `npx prisma contract emit`, then `npx prisma migration plan` / `npx prisma db migrate`, with a link. -7. Indexes: "Indexes are declared on the model with `@@index([authorId])`", linked to PSL syntax. -8. Keeping the copy in sync: the paragraph no longer raises maintenance; it is two sentences and stops. - -**Mechanism cut** - -- `$lookup` gone. -- "Prisma ORM models this with a base model plus variant models" is now the first sentence of the polymorphic section, ahead of the MongoDB-shape sentence. -- 16 MB limit kept (converted from a `:::warning` block to a plain paragraph to reclaim lines). - -**Cuts made for the line budget:** the `Cart`/`CartItem` PSL block (folded into one sentence mentioning `items CartItem[]`), the duplicate "`Cart.items` is stored inside the cart document" paragraph, the `zip` field, the warning directive, and several paragraph merges. - -## Facts looked up (all in `wip/prisma-src/`) - -- Embedded value on `create` as a plain nested object, and dot-path update: `packages/2-mongo-family/5-query-builders/orm/test/integration/orm-ergonomics.test.ts:171-177` (`homeAddress: { city: 'SF', country: 'US' }`, then `u('homeAddress.city').set('NYC')`). -- Variant create fills in the discriminator: `packages/2-mongo-family/5-query-builders/orm/src/collection.ts:358` (`#injectDiscriminator`) and `.../test/integration/polymorphism.test.ts:133-153` ("variant create injects discriminator and persists it"). -- `.variant()` takes the model name: `.../test/integration/polymorphism.test.ts:100,143` (`variant('Bug')` against discriminator value `'bug'`). -- `.where()` is keyed on model fields only: `packages/2-mongo-family/5-query-builders/orm/src/types.ts:188-201`. -- `.include(relationName)` for reference relations: `collection.ts:98-101`. -- MongoDB migrations exist and `db migrate` runs `createCollection` / `createIndex`: `skills/prisma-8/references/migrations.md:14,34,48`. - -## Q list - -None. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/mongodb.fix2.md b/docs/orm-docs-audit/c21-data-modeling/mongodb.fix2.md deleted file mode 100644 index 633fbc22b9b..00000000000 --- a/docs/orm-docs-audit/c21-data-modeling/mongodb.fix2.md +++ /dev/null @@ -1,45 +0,0 @@ -Round two done. `wip/c21/dm/r2/mongodb.mdx`, 173 lines before and after, plain-language check clean, no em-dashes. - -## Marks and what I did - -**The eight-fact paragraph under the `_id` example.** Cut to four MongoDB-client facts and moved the rest into one Next steps bullet. It now reads: "`db` is the client you create once in `src/prisma/db.ts`, and `npx prisma orm init` writes that file. `db.orm.users` is the accessor for the collection, and it follows the collection name, so `@@map` renames the accessor as well. In Prisma ORM 7 it did not. The values go straight into `create`, without the `data:` wrapper Prisma ORM 7 used." The contract sentence, `npx prisma contract emit`, and both migration commands are now the first Next steps bullet with a link to `/orm/migrations/generating-a-migration`. - -**Accessor changes with `@@map` (stated twice, never contrasted with v7).** The `@@map` paragraph no longer repeats the accessor; the sentence above carries it once, with the Prisma ORM 7 contrast. - -**"There is no `@default(auto())` and no `@db.ObjectId`."** Now: "`ObjectId` replaces `String @db.ObjectId` and `@default(auto())`. Writing either of those attributes is an error whose `code` is `PSL_UNSUPPORTED_FIELD_ATTRIBUTE`." - -**`Cart` / `CartItem` referenced but never shown.** Dropped both names; the sentence now uses the page's own type: "`addresses Address[]` stores every address inside the user document, with no second collection and no join to load them." - -**"Prisma ORM models a collection..." garden path.** Now: "One collection can hold more than one kind of document. You declare a base model for the shared fields and a variant model for each kind. A discriminator field tells the kinds apart: its value says which kind each document is." The "MongoDB collections do not enforce a single shape" sentence is cut, as instructed. - -**16 MB limit with no consequence.** Now "...pushes the document toward MongoDB's 16 MB limit, and a write that exceeds it fails." - -**Copied fields go stale.** Added: "The copy then goes stale on its own, and keeping it up to date is your job." - -**"contract", `contract emit`, `migration plan`.** All three now appear once, together, in the Next steps bullet, with the contract defined as "the `contract.prisma` file that replaced `schema.prisma`" (the section's fixed wording). - -**Pipeline builder gloss.** Now "a chained builder for MongoDB aggregation pipelines", with a two-line example. - -**`u`.** Defined at first use: "In an `update`, `u` gives you each field of the document to change." - -**`.all()`.** Defined once, where `.first()` first appears: "You `await` the whole chain, and the last call says what you want back: `.first()` for one document or `null`, `.all()` for every matching document." - -**"So what do I type?" items.** Connection: one sentence at the top of the first section, no `datasource` block, `prisma.config.ts` with `defineConfig` from `@prisma/orm-mongo/config`, link to `/orm/contract-authoring/the-data-contract`. Creating a referenced record: `db.orm.posts.create({ title: "Hello", authorId: user._id })`. Filtering on an embedded field: the `db.query.from("users").match(...)` example. Querying by id: `db.orm.users.where({ _id: user._id }).first()` with "the id comes back as a string, and you pass it back as a string. There is no `ObjectId` to construct." Adding to a list: `u.tags.push("admin")` / `u.tags.pull("new")` on a `tags String[]` field added to the embedded-section model. Variant `@@map`: "A variant declares no `@@map` of its own and uses its base's collection." Indexes: the trailing index clause in the polymorphic section is cut, per the brief. - -Cuts that paid for the additions: the collection-shape sentence, the index clause, the blank lines inside the PSL blocks, and merged paragraphs that repeated each other (embed/reference intro, embedded-type intro, the two reference query examples now in one block). - -## Facts looked up (rc.10 source) - -- `where` key is `_id`, not `id`: contract fields are keyed by the mapped name. `wip/prisma-src/packages/2-mongo-family/2-authoring/contract-psl/src/interpreter.ts:1320-1336` ("the mapped-name-keyed field record"), and the fixture `.../1-foundation/mongo-contract/test/fixtures/orm-contract.json` keys `User.fields` by `_id`. Test usage `where({ _id: created._id as string })` and `expect(typeof created._id).toBe('string')` at `.../5-query-builders/orm/test/integration/orm-ergonomics.test.ts:66-71`. -- `@default(...)` and `@db.ObjectId` are both `PSL_UNSUPPORTED_FIELD_ATTRIBUTE`: `.../contract-psl/test/interpreter.attribute-specs.test.ts:87-117`; the supported field attributes are only `@id`, `@unique`, `@map`, `@relation` (`.../src/mongo-attribute-specs.ts:256-261`). -- Setting a reference field directly on create: `orm.tasks.create({ title, type, assigneeId: user._id })`, `orm-ergonomics.test.ts:236-245`. -- `push` / `pull` on a scalar list: `orm-ergonomics.test.ts:135-155`. Embedded (value-object) paths get only `set`/`unset`/`exists`/`eq(null)` (`.../query-builder/src/field-accessor.ts:92-110`), so I showed `push` on `tags String[]`, not on an embedded list. -- Variant `@@map`: rejected only when it names a different collection, `interpreter.ts:460-465`. -- `@prisma/orm-mongo/config` export exists (`wip/prisma-src/packages/9-public/@prisma/orm-mongo/package.json`), matching the config example on `apps/docs/content/docs/orm/contract-authoring/psl-syntax.mdx:118-128`. -- Pipeline filter form `f("address.city").eq("NYC")` and `db.query.from("")`: `.../query-builder/test/builder.test.ts:77-86` and `apps/docs/content/docs/orm/fundamentals/advanced-queries.mdx:307-351`. - -## Q list - -**Q1.** Whether `npx prisma db migrate` must run before the first write on MongoDB. `wip/prisma-src/skills/prisma-8/references/migrations.md:47` says the marker is a document in `_prisma_migrations`, written after schema verification, but I found no check of that marker in the MongoDB query path (nothing matching `verifyMarker`/`marker` in `wip/prisma-src/packages/2-mongo-family/7-runtime/src/`). So I could not confirm that an unmigrated database rejects a write, and the page does not claim it either way. The reader asked for this ordering explicitly. - -**Q2.** Adding to a list of embedded values (`items CartItem[]`). The typed accessor for a value-object path has no `push`, and no test shows one, so the page shows `push`/`pull` on a scalar list only. If the runtime does support it on embedded lists, the example should move there, since that is the case the embed-vs-reference table sends readers to. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/mongodb.fix3.md b/docs/orm-docs-audit/c21-data-modeling/mongodb.fix3.md deleted file mode 100644 index d79d46cdc88..00000000000 --- a/docs/orm-docs-audit/c21-data-modeling/mongodb.fix3.md +++ /dev/null @@ -1,35 +0,0 @@ -Done. `wip/c21/dm/r3/mongodb.mdx`, 173 → 172 lines, plain-language check clean, no em-dashes, no extra banned words. - -## Marks and what I did - -**1. `PSL_UNSUPPORTED_FIELD_ATTRIBUTE`** — code cut. Now: "Write `ObjectId` for the key type. The old `String @db.ObjectId` and `@default(auto())` are no longer allowed and fail at `npx prisma contract emit`, the command that checks your contract and regenerates your types." That also defines `emit` at its first use. - -**2. "accessor" / version aside** — "`db.orm.users` is how you query the `users` collection. The name after `db.orm.` is the collection name, so with `@@map("users")` you write `db.orm.users`, not `db.orm.user`." "In Prisma ORM 7 it did not" cut. The word "accessor" no longer appears in prose. - -**3. `db.ts` self-contradiction** — "`npx prisma orm init` writes `src/prisma/db.ts`, which creates the client `db`." - -**4. "`u` gives you each field"** — "In an `update`, `u` builds the changes you want. Use `u.fieldName` to change a top-level field, and `u("path.to.field")` to change a field inside an embedded document." - -**5. `db.runtime()` double await** — the `db.runtime()` line is cut. The filter example stays and ends: "See [the pipeline builder](/orm/fundamentals/advanced-queries#mongodb-pipeline-builder) for running it and what comes back." - -**6. Buried operational sentence** — the contract definition moved to the first mention of "contract" (first sentence under the first heading): "Your models go in your contract, the `contract.prisma` file that replaced `schema.prisma`." The Next steps bullet now starts at `npx prisma contract emit`, whose meaning is given where it first appears. - -**Guessed words:** "contract" defined at first use (see above); "accessor" and "PSL" removed; "models a one-to-one" → "models a one-to-one relation" (and the list case → "one-to-many relation"); "pipeline builder" keeps its existing one-clause definition; "discriminator" and "variant" the reader said are adequate, so untouched. - -**"What do I type?" gaps:** -- `_id` vs `id`: added right after the first model, folded into the `@map` sentence — "In your TypeScript code the field is called `_id`, not `id`. `@map("_id")` maps the model's `id` field to MongoDB's mandatory `_id` field and sets the name on both sides, so you write `user._id` and `where({ _id })`." -- `prisma.config.ts`: no snippet added per the brief; the sentence now says the connection string is in that file and links to the data contract page. -- Appending to / replacing embedded data: "Replace a whole embedded value with `u("address").set({ ... })`, and remove it with `u("address").unset()`. A list of embedded values is replaced as a whole with `u.addresses.set([ ... ])`." -- Nested writes: "There are no nested writes on MongoDB: create the user first, then the post with its `authorId`." -- Variants: "A variant declares only its own fields. `id`, `title`, and `kind` come from `Post`." And "You reach a single variant through the base model: `db.orm.posts.variant("Article")` limits the query to one variant." No claim made about `db.orm.articles` either way. -- Typed results after a pipeline filter: answered by the pipeline-builder link ("for running it and what comes back"), since this page does not own that. - -## Facts looked up - -- Nested writes: no `connect` and no `create` callback anywhere in `/Users/will/Projects/prisma/web/.claude/worktrees/pr-conflicts-review-fb93f7/wip/prisma-src/packages/2-mongo-family/5-query-builders/orm/src/` (grep for `connect` and `nested` returns nothing). Supports the "no nested writes" sentence. -- Variant access: `/Users/will/Projects/prisma/web/.claude/worktrees/pr-conflicts-review-fb93f7/wip/prisma-src/packages/2-mongo-family/5-query-builders/orm/test/integration/polymorphism.test.ts` uses only `orm.tasks.variant('Bug')` style, never `orm.bugs`. No `variant` accessor generation in `mongo-orm.ts`. So the page keeps "through the base model" and claims nothing more. -- `set` on a list path: `field-accessor.ts:70` gives `set(value: MongoValue)` on `LeafExpression`, and `resolve-path.ts` (`FieldToLeaf`, around lines 98-106) maps a `many: true` contract field to a leaf with `codecId: 'mongo/array@1'`. So a list of embedded values resolves to a leaf and `u.addresses.set([...])` is valid. Whole-value `set`/`unset` on a single embedded path is `field-accessor.ts:92-112` (`ObjectExpression`). - -## Q list - -None. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/mongodb.fix4.md b/docs/orm-docs-audit/c21-data-modeling/mongodb.fix4.md deleted file mode 100644 index 57dcce7e8bc..00000000000 --- a/docs/orm-docs-audit/c21-data-modeling/mongodb.fix4.md +++ /dev/null @@ -1,49 +0,0 @@ -Done. Edited only `wip/c21/dm/r4/mongodb.mdx`. - -## Marks and what I did - -**Sentences the reader could not restate** - -1. **Line 13, three new things in three sentences.** Reordered: models in the contract first, then the connection. "There is no `datasource` block on MongoDB." is now a trailing sentence. Added "That page shows the config file." after the `/orm/contract-authoring/the-data-contract` link. No `prisma.config.ts` snippet, per the brief. -2. **Line 24, eight-sentence paragraph.** Split into three: `_id` naming plus what `@map`/`@@map` do; the key type and the two removed syntaxes; how ids get assigned. -3. **"sets the name on both sides".** Now: "In the contract you call the field `id`. In TypeScript it is always `_id`, never `id`. `@map("_id")` sets the name in the database and in your code, so you write `user._id` and `where({ _id })`." -4. **`ObjectId` type vs string value.** Now: "`ObjectId` is the contract type. In TypeScript ids are plain strings, so there is no `ObjectId` to construct." -5. **`u` unexplained, array unexplained, two forms.** Now: "`update` takes a callback that returns an array, so several changes go in one call. Use `u.fieldName` to change a top-level field. Use `u("path.to.field")` for a field inside an embedded document, written as a string because a dotted path is not a property name." -6. **Pipeline builder not actionable.** Variable renamed `berliners` → `query`, and: "`build()` returns the query, not the rows. See [the pipeline builder](...) for how to run it and what comes back." -7. **"goes stale on its own".** Now: "If the original changes, the copy does not. You update it yourself." - -**Guessed words** - -- "your contract", "pipeline builder", "variant", "discriminator field", `@@base` — defined elsewhere in the section or already defined well by example; unchanged. -- "contract emit" — now said once, in the Next steps bullet, with "which checks your contract and regenerates your types". Removed from the removed-syntax sentence. -- `db.orm` — now: "`db.orm` is the ORM client, one of the query APIs on `db`. See [the ORM reference](/orm/reference)." - -**"So what do I actually type?"** - -1. `prisma.config.ts` — link plus "That page shows the config file". No snippet, per the brief. -2. Import — `import { db } from "./prisma/db";` added once, in the first TypeScript example. -3. Running the pipeline — fixed by the rename and the `build()` clause. -4. Command order — Next steps bullet is now the ordered sequence: `npm create prisma@latest -- my-app` or `npx prisma orm init`, then `contract emit`, `migration plan --name `, `db migrate`. The scattered mentions are cut (`orm init` in the `db` paragraph, `contract emit` in the removed-syntax sentence). -5. Unique index — `email String @unique` added to the first model, plus "`@unique` on `email` becomes a unique index when you migrate." -6. Variant's own fields — nothing added; see the lookup below. -7. `references: [id]` vs `_id` — "In `references: [id]` you write the field's contract-side name, `id`, not `_id`." -8. Reading the other direction — "`.include("posts")` on `db.orm.users` works the other way and gives each user an array of their posts." - -**Internals** - -- Product history on line 13 — kept the section's mandated first-use phrasing, but it is no longer the first clause carrying the connection story. -- "fail at `npx prisma contract emit`" — cut; the sentence is now "The old `String @db.ObjectId` and `@default(auto())` are no longer allowed." - -## Facts looked up - -- `@unique` on a MongoDB field creates a unique index: `wip/prisma-src/packages/2-mongo-family/2-authoring/contract-psl/src/interpreter.ts:908-912` builds `new MongoIndex({ keys: [...], unique: true })`; `wip/prisma-src/packages/2-mongo-family/9-family/src/core/contract-to-schema.ts:17-20` carries `unique` into the schema the migration applies. Spec at `.../contract-psl/src/mongo-attribute-specs.ts:146`. -- `.include` on the one-to-many list side works and returns an array: `.../5-query-builders/orm/src/collection.ts:98` accepts any `ReferenceRelationKeys`; `.../orm/src/types.ts:20-30` includes list-side reference relations; unit test `.../orm/test/collection.test.ts:492` (`User`.include('tasks')) and integration test `.../orm/test/integration/orm-ergonomics.test.ts:231-262` ("include() on 1:N relation returns array of related documents"). -- Variant rows do **not** carry the variant's own fields typed: `.../orm/test/integration/polymorphism.test.ts` only reads base fields (`type`, `title`) off variant rows and casts to `Record` at line 149 to read the discriminator off a create result. The collection row type is keyed on the base model name (`collection.ts:78-80`, `all()` at line 109). So I said nothing about `summary` on a variant read. - -## Q list - -None. - -## Line count - -173 before, 172 after. `check-plain.sh` is clean; no em-dashes, no semicolons joining clauses, no page-conventions banned words. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/mongodb.round1.md b/docs/orm-docs-audit/c21-data-modeling/mongodb.round1.md deleted file mode 100644 index 8fea2eeedaa..00000000000 --- a/docs/orm-docs-audit/c21-data-modeling/mongodb.round1.md +++ /dev/null @@ -1,65 +0,0 @@ -Read once, top to bottom. Here's what tripped me up. - -## Sentences I could not restate after one reading - -**Line 87:** "You can still match on its fields through the parent, with a path such as `address.city` in the [pipeline builder](...)." - -Two problems. I don't know what "the pipeline builder" is — it's linked but never described, and the sentence assumes I already know. Worse, the sentence implies that matching on an embedded field is something you do *in the pipeline builder*, i.e. not in an ordinary query. In the version I use I'd write a normal filter on a composite type field. Is that gone? Plainer: "You can filter on embedded fields in a normal query using X syntax" — and then show X. If the pipeline builder really is the only way, say that outright. - -**Line 118:** "A `$lookup` is a real cost on reads that run often, so document models often keep a small copy of the fields a read needs inside the parent." - -"document models often keep a small copy" — models don't keep copies, people do. And this is three ideas in one sentence: lookups are slow, therefore denormalize, and here's the general practice. Plainer: "Resolving a reference costs an extra query. If a read runs constantly, you can copy the few fields it needs into the parent document." Also, the whole paragraph gives me advice with no code — see below. - -**Line 150:** "`db.orm.posts.variant(\"Article\")` limits the query to one variant." - -I could not restate this because I don't know what `db` is, what `.orm` is, or where either comes from. In the version I've used for two years the client is `prisma.post.findMany()`. This is the only client code on the entire page and it arrives with no introduction. Is `db` something I import? Something generated? `orm` looks like a namespace I've never seen. - -**Line 152:** "Use one polymorphic collection when the variants are handled together far more than separately: a notifications collection of email, SMS, and push messages read as one stream." - -The colon-example construction made me re-read. The example is good; the sentence in front of it is compressed. Plainer: "Use one collection when you almost always query the variants together — for example, email, SMS, and push notifications that you read as a single feed." - -## Words and phrases I had to guess at - -- **`ObjectId`** (line 19) — I guessed this is a new scalar type in the schema language. In the version I know, you write `String @id @default(auto()) @map("_id") @db.ObjectId`. The page shows a bare `ObjectId` with no `@default`, and never says the type is new or that `@db.ObjectId` is gone. I guessed, and I'm not confident. -- **"the pipeline builder"** (lines 87, 166) — guessed: some API for writing MongoDB aggregation pipelines. Never stated on the page. -- **`@@discriminator(kind)`** (line 132) — guessed from the name. The page does explain it on line 150, but the unquoted `kind` next to `@@map("users")`'s quoted string made me guess whether the argument is a field reference or a string, and whether I'd get an error for writing `"kind"`. -- **`@@base(Post, "article")`** (line 139) — guessed: "Article is a subtype of Post, stored with `kind = "article"`." The explanation on line 150 confirms it, but only after two code blocks. -- **`.include(...)`** (line 165) — guessed this is a chained method. In the version I know, `include` is an option object key. The page uses method syntax in passing without flagging the change. -- **`prisma-8` skill** (line 156) — guessed: a named bundle of instructions for an AI coding agent. -- **"discriminator field"** (line 122) — guessed: a field whose value says which kind of document this is. Fine, but it's introduced as if standard vocabulary. -- **"no independent life" / "records with their own life"** (lines 32, 87) — guessed: can't be queried or updated on its own. The literal version is right there in the page (line 34, "each record stands on its own"); the metaphor is the weaker wording. - -## "So what do I actually type?" - -This is the page's biggest problem. There is **not one line of query code on the whole page** except the unexplained `db.orm.posts.variant("Article")`. - -1. **How do I get an `_id` generated?** The schema shows `id ObjectId @id @map("_id")` with no default. When I call create, do I have to supply an ObjectId myself? Does MongoDB fill it in? Does Prisma? The version I know requires `@default(auto())` and I'd get a runtime error without it. The page never says. -2. **How do I create a document with an embedded type?** Section "Embedded documents" defines `Address` and `CartItem` and then never shows a write. Do I pass a plain nested object? Is there a `create:` wrapper like relations have? -3. **How do I update one item inside an embedded array?** Line 32 says writes update "parent and children together, atomically" — great, but what do I type to change `items[2].amount`? -4. **How do I filter on an embedded field in a normal query?** See line 87 above. I'm told a path string and pointed at another page. -5. **How do I create a polymorphic document?** `Article` has `summary` and nothing else. Do I write `db.orm.articles.create({ title, summary })`? Does `kind` get set for me from `@@base`, or do I set it? Does `Article` have `title` and `id`, inherited from `Post`? The page says they "share one collection" but never says the variant model inherits the base model's fields — I'm inferring it. -6. **How do I apply this schema to MongoDB?** There are no migrations for MongoDB in the version I know; you run a push command. The page ends with a polymorphic schema and never tells me the command that makes it real. -7. **How do I declare the indexes line 152 tells me to think about?** "need very different indexes" is the only mention of indexes and there's no syntax anywhere. -8. **How do I keep the denormalized copy in sync** (line 118)? The page raises the technique and the maintenance cost, then stops. Either show the pattern or don't raise it. - -## Explaining the machinery when I wanted instructions - -- **Line 116:** "Prisma ORM resolves it with a `$lookup` aggregation." I don't need to know the MongoDB operator name to write a query. It's only justified if it sets up line 118 — and line 118 is itself advice I can't act on. -- **Line 122:** "MongoDB collections do not enforce a single shape. The usual choice is to store documents of different kinds in one collection..." This is a MongoDB-modeling lecture before I get told what Prisma does about it. Line 124 ("Prisma ORM models this with a base model plus variant models") is what I came for and it's the third sentence. -- **Line 91:** "pushes the document toward MongoDB's 16 MB limit" — this one earns its place; it's a hard limit I'd hit. Keeping it. -- **Line 26:** "Without it the collection takes the model name with a lowercase first letter, so `user`." Useful, but then every single example on the page uses `@@map` anyway, which made me wonder whether it's actually optional or just conventionally required. - -## Could I do what the page is for? - -Partly. I could **write a schema** — models with `ObjectId` ids, `type` blocks for embedded data, `@relation` for references. The embed-vs-reference table on line 38 is the best thing on the page; it answered the actual decision I came with, in one screen, with examples I recognized. - -I could **not** do anything with that schema. After one reading I still don't know: - -- Whether I need `@default(auto())` on the id, or what happens on create if I omit it. -- How to apply the schema to my database (no command anywhere). -- What `db.orm` is, or how to get one. -- How to write or read an embedded document — no client code exists on this page. -- How to filter on an embedded field without learning "the pipeline builder" first. -- Whether `Article` inherits `Post`'s fields, and how to create one. - -The page reads as a schema-design reference that has been labeled a data-modeling guide. Every time it approaches "and here's what you run," it hands off to a link (`/orm/fundamentals/reading-data`, `/orm/fundamentals/relations-and-joins`, the pipeline builder anchor) — five links, and I'd have to open at least three of them before I could run anything I modeled here. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/mongodb.round2.md b/docs/orm-docs-audit/c21-data-modeling/mongodb.round2.md deleted file mode 100644 index 9ca62c9ea95..00000000000 --- a/docs/orm-docs-audit/c21-data-modeling/mongodb.round2.md +++ /dev/null @@ -1,90 +0,0 @@ -Read it once, straight through, as a Prisma 7 user who has never seen 8. - -## Sentences I could not restate after one reading - -**Line 32 — the whole paragraph.** This is the worst thing on the page. - -> "`db` is the client you create once in `src/prisma/db.ts`, and `prisma orm init` writes that file. `db.orm` holds your models, and on MongoDB the accessor is the collection name, so `db.orm.users`. The values go straight in, without the `data:` wrapper Prisma ORM 7 used. Models themselves go in your contract, the `contract.prisma` file that replaced `schema.prisma`. Run `npx prisma contract emit` after every change to it. MongoDB has migrations like any other database: `npx prisma migration plan` writes one and `npx prisma db migrate` applies it, creating the collections and indexes." - -Eight unrelated new facts in one paragraph, none of them about MongoDB modeling: client file location, an init command, accessor naming, a removed `data:` wrapper, a renamed schema file, an `emit` command, and two migration commands. I stopped reading and went back three times. It is a "everything that changed in v8" dump wedged under a code sample about `_id`. It belongs in its own section, or on the migration page, with the MongoDB-specific bit (`db.orm.users`) left here. - -**Line 24 vs 29/32 — the accessor name.** - -> "Without it the collection takes the model name with a lowercase first letter, so `user`, and you reach it in code as `db.orm.user`." - -I had to stop and work out that the *client accessor changes when I rename the collection*. In v7 the accessor was the model name and `@@map` was purely a database-side rename. The page states the new behavior twice but never says "unlike v7, `@@map` also renames the client accessor" — which is the only thing I actually needed to be told. I would have gotten this wrong. - -**Line 26.** - -> "There is no `@default(auto())` and no `@db.ObjectId`." - -Three readings and I still don't know what happens if I write them. Removed? Ignored? Contract-emit error? "No" is not a behavior. Plainest version: "`@default(auto())` and `@db.ObjectId` are gone — writing either is an error. The `ObjectId` type covers both." - -**Line 74.** - -> "A list of embedded values models a one-to-many: `items CartItem[]` on a `Cart` model stores every line item inside the cart document" - -Two model/type names (`Cart`, `CartItem`) that appear nowhere on the page, referenced as if I'd already seen them. I could not check my mental model against anything. Show the three-line `type CartItem` + `model Cart` instead of describing it. - -**Line 124.** - -> "Prisma ORM models a collection that holds more than one kind of document with a base model plus variant models, all sharing one collection." - -Garden path: I read "models" as a noun ("Prisma ORM models" = my models) and had to restart the sentence. Also "base model plus variant models" and "polymorphic" are dropped on me as established terms. Plainer: "One collection can hold more than one kind of document. You declare a base model for the shared fields and a variant model for each kind." - -**Line 91.** - -> "pushes the document toward MongoDB's 16 MB limit" - -Minor, but "16 MB limit" appears with no statement of what happens at the limit (the write fails). I guessed. - -**Line 120.** - -> "If a read runs constantly, you can copy the few fields it needs into the parent document, such as a comment storing its author's name." - -I know this is denormalization. The sentence does not say that copies go stale and that keeping them in sync is now my job. That is the entire cost of the advice, and it's missing. - -## Words and phrases I had to guess - -- **"contract"** (line 32) — guessed: the new name for the schema file. The page says "your contract, the `contract.prisma` file that replaced `schema.prisma`" but never says what a contract *is* or why it isn't just called a schema anymore. -- **"`prisma contract emit`"** — guessed: regenerates the client, i.e. the old `prisma generate`. The page never says what it emits or why I need it. -- **"`npx prisma migration plan` writes one"** — guessed: "one" = a migration file. Guessed it goes in a `migrations/` folder. Not stated. -- **"the pipeline builder, the API for MongoDB aggregation pipelines"** (line 89) — the gloss tells me what it wraps, not what it looks like. Guessed: a chained query builder. -- **"`u`"** in `update((u) => [u("address.city").set("Hamburg")])` — guessed: a field-accessor for the row being updated. Never named or explained anywhere on the page. -- **"`.all()`"** (line 117) — guessed: executes and returns many rows. It appears once, unannounced, and `create` on line 29 does *not* need it, so I don't know the rule. -- **"variant"** (line 152) — guessed correctly, but only because `@@base(Post, "article")` was right above it. -- **"`@@discriminator(kind)`"** — the page does define "discriminator field" inline, which worked. - -## "So what do I actually type?" — unanswered - -1. **How do I tell the contract this is MongoDB.** There is no `datasource` block, no `provider = "mongodb"`, no connection string, anywhere on a page titled "MongoDB data modeling". For MongoDB specifically this is the first thing I need. -2. **How do I create a referenced record.** The references section shows the schema and a read (`.include("author")`), and then stops. I don't know whether I write `authorId` directly, or use something like v7's `connect:`, or a nested create. This is the most common thing I'd do after reading that section. -3. **How do I filter on an embedded field.** Line 89 tells me `.where()` won't do it, tells me the tool that will, and gives me zero code. I'm left with "use the pipeline builder with a path such as `address.city`" and a link. One example would fix this. -4. **How do I query by `_id`.** Do I pass a string, or construct an `ObjectId`? Every v7 MongoDB user hits this in the first hour. Not mentioned. -5. **How do I push to or remove from an embedded array.** The page recommends embedded arrays for one-to-many, then only shows `.set()` on a scalar path. Adding a cart item — the exact example it used — is not shown. -6. **Do variant models need `@@map`?** `Article` has no `id`, no `@@map`. I inferred it inherits the collection from `@@base(Post, ...)`, but that's a guess and the page should say it in five words. -7. **Where does `@@index([authorId])` go and when do I need one?** It's mentioned in one trailing clause of the polymorphic section, which has nothing to do with indexes, and then handed off to another page. -8. **Migrations for MongoDB.** "MongoDB has migrations like any other database" — but I have no idea what a migration does on a schemaless store, what `plan` produces, or whether I must run these before my first `create`. The sentence raises the question and then leaves. - -## Internal mechanics where I only wanted to know what to do - -- Line 24, `@@map("_id")` explained as a *mapping* between model field and MongoDB field. Fine and short — this one earns its place. -- Line 32, `prisma contract emit` "after every change" is the reverse problem: pure mechanism with no explanation, presented as a ritual. -- Line 124, "MongoDB collections do not enforce a single shape, so this is the usual way..." — background on MongoDB's storage model when I was trying to find out how to declare a variant. Cut it or move it after the code. - -Nothing else on the page over-explains internals. The failure mode here is the opposite: commands and APIs introduced with no explanation at all. - -## Could I do what the page is for, after one read? - -**Partly.** I could decide embed vs reference — that section (lines 36–53, including the table) is genuinely good and is the only part I could restate cleanly without rereading. I could also write the `type Address` / `model User` embedded schema and the reference schema from memory. - -**I could not actually build anything**, because after one reading I still don't know: - -- How to point the contract at my MongoDB database at all. -- How to create a record on either side of a reference. -- How to query or filter by anything inside an embedded document — the page explicitly tells me `.where()` won't work and then doesn't show me what does. -- Whether I have to run `prisma contract emit` and `prisma db migrate` before my first write, or in what order. -- How to pass an `_id` into a query. -- What `u`, `.all()`, or `.set()` are, beyond guessing from context. - -The page is a good *decision* guide and an incomplete *task* guide. The embed-vs-reference material is the strongest thing here; the v8 API and tooling around it is asserted in fragments, mostly inside one overloaded paragraph, and never demonstrated end to end. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/mongodb.round3.md b/docs/orm-docs-audit/c21-data-modeling/mongodb.round3.md deleted file mode 100644 index dee9d701e95..00000000000 --- a/docs/orm-docs-audit/c21-data-modeling/mongodb.round3.md +++ /dev/null @@ -1,71 +0,0 @@ -## Sentences I could not restate after one reading - -**1.** "`ObjectId` replaces `String @db.ObjectId` and `@default(auto())`. Writing either of those attributes is an error whose `code` is `PSL_UNSUPPORTED_FIELD_ATTRIBUTE`." (line 24) - -What stopped me: "PSL" is never spelled out. And the second sentence is internal plumbing — I don't type error codes, I read them. Plainest version I'd have understood: "Write `ObjectId`. The old `String @db.ObjectId` and `@default(auto())` are no longer allowed and will fail validation." - -**2.** "`db.orm.users` is the accessor for the collection, and it follows the collection name, so `@@map` renames the accessor as well. In Prisma ORM 7 it did not." (line 30) - -Three ideas stacked: what the accessor is, that it tracks `@@map`, and a version difference. "Accessor" is a word I had to guess at. Plainer: "`db.orm.users` is how you query the `users` collection. The name after `db.orm.` matches the collection name, so if you set `@@map("users")` you write `db.orm.users`, not `db.orm.user`." - -**3.** "`db` is the client you create once in `src/prisma/db.ts`, and `npx prisma orm init` writes that file." (line 30) - -This contradicts itself in one sentence — do I write it or does `init` write it? I still don't know whether I need to author anything there. - -**4.** "In an `update`, `u` gives you each field of the document to change." (line 81) - -I cannot restate this. "gives you each field" means nothing concrete. Then the example shows two completely different shapes — `u("address.city").set(...)` and `u.tags.push(...)` — and the page never says why one is a function call with a string and the other is a property. Plainer: "`u` is a builder. Use `u.fieldName` for a top-level field, and `u("path.to.field")` when the field is inside an embedded document." - -**5.** "`const rows = await (await db.runtime()).query(berliners);`" plus "use the pipeline builder, a chained builder for MongoDB aggregation pipelines" (lines 88–92) - -The double `await` and `db.runtime()` appear once, are never explained, and appear nowhere else on the page. What is `db.runtime()`? Why does the normal `db.orm.*` path not work here? What type is `rows` — my `User` model, or raw MongoDB documents? I'd have to guess raw. - -**6.** "Models go in your contract, the `contract.prisma` file that replaced `schema.prisma`. Run `npx prisma contract emit` after every change to it, then `npx prisma migration plan --name ` writes a migration and `npx prisma db migrate` applies it, creating the collections and indexes." (line 170) - -One sentence, three unfamiliar commands, and "emit" is never defined — emit *what*, to where? This is the single most important operational sentence on the page and it's buried in a Next steps bullet. - -## Words and phrases I had to guess - -- **"the data contract"** / **"contract"** — guessed: the new name for the schema file. The link at line 13 is dropped before the page has told me what a contract is; I only learn it's `contract.prisma` in the final bullet, 157 lines later. -- **"accessor"** — guessed: the property on `db.orm` you call query methods on. -- **"PSL"** — guessed: Prisma Schema Language. -- **"pipeline builder"** — guessed: a fluent API that constructs a MongoDB aggregation pipeline. -- **"discriminator"** — the page does define it, adequately. -- **"models a one-to-one"** (line 74) — one-to-one *what*? Guessed: relation. -- **"variant"** — guessed from context; the page does eventually make it clear. - -## "So what do I actually type?" — unanswered - -1. **`_id` vs `id` in TypeScript.** The model declares `id ObjectId @id @map("_id")`. Then every code sample uses `user._id` (lines 33, 84, 85, 120). So which is the JavaScript property — `id` or `_id`? The page never says, and the mismatch with the model is exactly the thing I'd get wrong. This is the biggest gap on the page. - -2. **The connection.** "Your `prisma.config.ts` file holds the connection, using `defineConfig` from `@prisma/orm-mongo/config`" — there is no example. Not one line of what that file looks like, where the connection string goes, or what env var it reads. - -3. **Adding to a list of embedded documents.** Line 74 introduces `addresses Address[]`. Line 81 says `.push(...)`/`.pull(...)` work on "a list of scalars." So how do I append an `Address` to `addresses`? Never shown. - -4. **Replacing or clearing a whole embedded object.** I'm shown `u("address.city").set("Hamburg")` for one field. How do I swap the entire `address`, or set it to null? - -5. **Creating a post and its author together.** "To link a new post to a user, set the reference field yourself" tells me nested writes are gone, but doesn't say whether there's any replacement for Prisma 7's nested `connect`/`create`. I'd be left wondering if I now have to do two round trips by hand. - -6. **Variant models.** Do `Article` and `Tutorial` declare `id`/`title` themselves? The example omits them and says "A variant has every field of its base," so I guess no — but I'd want that stated as a rule, not inferred. Also: is there a `db.orm.articles`, or is `db.orm.posts.variant("Article")` the only way in? - -7. **Filtering on an embedded field, then using the result.** After the pipeline example I have `rows`, and no idea if I can then `.update()` them, or whether I'm now outside the typed API entirely. - -## Implementation detail where I wanted instructions - -- The error code `PSL_UNSUPPORTED_FIELD_ATTRIBUTE` (line 24). Tell me not to write it; don't tell me the internal code name. -- "In Prisma ORM 7 it did not" (line 30). A one-clause aside about old behavior dropped mid-explanation of new behavior. If the migration difference matters, say what to change; if not, cut it. -- `db.runtime()` (line 92) exposes a layer of the client I otherwise never touch and never needed explained. - -## Could I do what the page is for after one reading? - -Partly. I could confidently write the Prisma models — documents, `type` blocks, references, and the discriminator syntax are all clear, and the embed-vs-reference table is genuinely the best part of the page. I'd make the right modeling decision. - -I could not write working code. Specifically I would still not know: - -- whether to type `user.id` or `user._id` -- what `prisma.config.ts` needs to contain to connect at all -- how to add an item to an embedded array of objects -- what `npx prisma contract emit` does or why it must run before the migration commands -- how to get typed results back when I need to filter on an embedded field - -The page reads as if the model syntax is the hard part. For someone coming from Prisma 7, the hard part is that the entire client API changed — no `data:` wrapper, a builder callback for updates, a separate pipeline API, a `db.runtime()` escape hatch — and those changes are each mentioned once, in passing, without enough of an example to copy. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/mongodb.round4.md b/docs/orm-docs-audit/c21-data-modeling/mongodb.round4.md deleted file mode 100644 index 8b5f0e31b0b..00000000000 --- a/docs/orm-docs-audit/c21-data-modeling/mongodb.round4.md +++ /dev/null @@ -1,76 +0,0 @@ -# Reader review: `wip/c21/dm/r4/mongodb.mdx` - -## Sentences I could not restate after one reading - -**1. Line 13** — "Your models go in your contract, the `contract.prisma` file that replaced `schema.prisma`. There is no `datasource` block on MongoDB. The connection string is in your `prisma.config.ts` file instead, passed to `defineConfig` from `@prisma/orm-mongo/config`." - -What stopped me: three new things in three sentences, none shown. "your contract" is used as a noun for a thing I have never heard of. `defineConfig` and `@prisma/orm-mongo/config` are named but never demonstrated — I cannot picture the file. "There is no `datasource` block" answers a question I had not asked yet and implies other databases still have one. - -Plainer: "Your models go in `contract.prisma` (Prisma 8's replacement for `schema.prisma`). MongoDB connection settings go in `prisma.config.ts`, not in a `datasource` block — [example here]." - -**2. Line 24** — the whole paragraph. It is eight sentences carrying eight unrelated facts: `_id` naming, what `@map` does, what `@@map` does, the default collection name, the key type, two removed syntaxes, the command that validates, and how ids get assigned. I lost the thread by the fourth sentence. Split it. - -**3. Line 24** — "`@map("_id")` maps the model's `id` field to MongoDB's mandatory `_id` field and sets the name on both sides, so you write `user._id` and `where({ _id })`." - -"sets the name on both sides" — which two sides? Database and TypeScript? I had to infer it from the example that follows. And it contradicts my expectation: I declared the field as `id` in the contract, so why is it `_id` in TypeScript? The page states it but never says why, so I do not know whether I can make it come out as `id`. - -Plainer: "In the contract you call the field `id`; in TypeScript it is always `_id`." - -**4. Line 30** — "The id comes back as a string, and you pass it back as a string. There is no `ObjectId` to construct." - -I had just been told to "Write `ObjectId` for the key type." So the type is `ObjectId` but the value is a string. That is not a contradiction once you think about it, but the page sets it up as one and does not close it. Say plainly: "`ObjectId` is the contract type; in TypeScript ids are plain strings." - -**5. Line 81** — "In an `update`, `u` builds the changes you want." - -`u` arrives with no introduction. Looking at the example, `update` takes a function and the function returns an **array** — nothing says why it is an array or that multiple changes go in one call (I only worked that out from the second example). Also unexplained: why `u.tags` (property) and `u("address.city")` (call) are two different forms. I guessed: dotted paths must be strings because they are not valid property names. The page should just say that. - -**6. Line 88** — "`.where()` filters on top-level model fields only. To match on a field inside an embedded document, use the pipeline builder, a chained builder for MongoDB aggregation pipelines." - -The limitation is clear; the replacement is not. "chained builder for MongoDB aggregation pipelines" tells me nothing I can act on, and the example that follows does not run (see below). - -**7. Line 123** — "The copy then goes stale on its own, and keeping it up to date is your job." - -"goes stale on its own" reads oddly, as if the data actively rots. Plainer: "If the original changes, the copy does not. You have to update it yourself." - -## Words and phrases I had to guess - -- **"your contract"** (line 13) — guessed: the Prisma 8 name for the schema file. The page tells me the filename in the same breath but never says the word "contract" is now the general term for the whole thing, and then uses it four more times ("changes to your contract", "checks your contract"). -- **"contract emit"** (line 24, 169) — guessed: the new `prisma generate`. "emit" is doing a lot of work; the inline gloss ("checks your contract and regenerates your types") helped, but it is buried mid-paragraph in a sentence about removed syntax. -- **"pipeline builder"** (line 88) — guessed: a fluent API that produces a MongoDB aggregation pipeline. -- **"variant"** (line 127 onward) — guessed: a subtype. Fine, the page defines it well enough by example. -- **"discriminator field"** (line 127) — guessed correctly, but only because I have seen the term elsewhere. The sentence "its value says which kind each document is" is doing the defining work and is good. -- **`db.orm`** (line 30) — guessed: a namespace on the client. Never explained why there is an `orm` level at all, or what else lives on `db`. -- **`@@base(Post, "article")`** — guessed: "this model extends Post, and its discriminator value is `article`." Line 148 confirms it one sentence later, which is fine. - -## "So what do I actually type?" — places the page does not say - -1. **The `prisma.config.ts` file.** Named, with an import path and a function, and never shown. This is the one thing without which nothing on the page works. It is the biggest hole on the page. -2. **Importing the client.** Line 30 says `npx prisma orm init` writes `src/prisma/db.ts`, then every example uses a bare `db`. No import line anywhere. Do I write `import { db } from "./prisma/db"`? Guessing. -3. **Running the pipeline.** Line 91 assigns `const berliners = db.query.from("users")...build()`. No `await`. So `berliners` is... a pipeline object? An array of Berliners? The name says the latter, the code says the former. The page punts to another page for "running it and what comes back" — but then it should not name the variable as though it holds results. -4. **The order of commands for a new project.** The page names `npm create prisma@latest`, `npx prisma orm init`, `npx prisma contract emit`, `npx prisma migration plan --name `, `npx prisma db migrate`, and `npx prisma skills sync`, scattered across five sections. I finished the page not knowing which of these I run first, or whether `orm init` and `npm create` are alternatives. -5. **Indexes and uniqueness.** Line 169 says migration "creat[es] the collections and indexes," but no example declares an index. If I want `email` unique — the very first model has an `email` field — the page does not tell me how. -6. **Querying a variant's own fields.** `db.orm.posts.variant("Article")` limits the query, but nothing shows reading `summary` off a result, or what happens if I `.all()` the base and want to tell an Article from a Tutorial in TypeScript. -7. **`references: [id]` vs `_id`.** Line 111 writes `references: [id]` using the contract-side name while everything in TypeScript is `_id`. I paused on whether that was a typo. One clause would fix it. -8. **Reading references the other way.** `.include("author")` goes child-to-parent. Nothing shows loading a user's `posts`. - -## Internals where I only wanted instructions - -Very little, which is unusual. Two spots: - -- **Line 13** — "the `contract.prisma` file that replaced `schema.prisma`" is product history, not an instruction. It is useful to me as an upgrader, but it is delivered as the definition of what a contract is, which it is not. -- **Line 24** — "The old `String @db.ObjectId` and `@default(auto())` are no longer allowed and fail at `npx prisma contract emit`." Telling me *which command* rejects them is implementation detail I did not need mid-paragraph; "these no longer work, write `ObjectId`" is enough, and `contract emit` deserves its own introduction elsewhere. - -## Could I do what the page is for, after one reading? - -**Half.** The decision the page exists to help me make — embed or reference — I can now make confidently. The table on lines 44–51 and the blog-comments example on line 53 are the clearest part of the page; I could apply them to my own data immediately. Same for the polymorphic section: the discriminator explanation on line 148, including "write `kind` and not `"kind"`" and `.variant("Article")` not `"article"`, anticipated exactly the two mistakes I would have made. - -**What I still would not know:** - -- How to connect to MongoDB at all. I cannot write `prisma.config.ts` from this page. -- Whether my project is set up correctly, or in what order to run the six commands named. -- How to import `db`. -- How to actually execute a filter on an embedded field — the one example is incomplete. -- How to declare a unique index on `email`, despite the page putting `email` in its very first model. -- Why my `id` field is `_id` in TypeScript, and whether that is changeable. - -The modeling guidance is solid. The surrounding mechanics — set up, connect, run — are named rather than shown, so a reader coming from Prisma 7 cannot get from this page to working code. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/page-conventions.md b/docs/orm-docs-audit/c21-data-modeling/page-conventions.md deleted file mode 100644 index df14a4dce1c..00000000000 --- a/docs/orm-docs-audit/c21-data-modeling/page-conventions.md +++ /dev/null @@ -1,15 +0,0 @@ -# Conventions for the orm/data-modeling pages (in addition to conventions.md) - -- These are explanation pages, not reference pages: Overview (models, primary keys, scalar fields, relations), Relational data modeling, MongoDB data modeling. Each one answers "how do I describe my data and why this way". Keep the heading order. Do not renumber, rename, or reorder headings; if a heading must change because it contains a banned term, pin the old anchor with `[#old-anchor]`. -- No commentary about the docs inside the docs. Cut sentences such as "This page introduces", "This page shows", "it is what this page focuses on", "the guides cover that in depth". Write the thing itself. -- Never interleave PostgreSQL and MongoDB sentence by sentence. Where both databases matter, give PostgreSQL its own sentences or block and then MongoDB its own. Tabs already on a page stay. -- The word "contract" is the subject of the contract-authoring section, not this one. On first use on a page: "your contract, the `contract.prisma` file that replaced `schema.prisma`". After that, "contract" alone. Do not use "schema" for the contract; "schema" alone means the PostgreSQL schema (`public`). -- "Join table": the model that holds one record per connected pair is "the model for the join table" on first use and the model's own name (`PostTag`) or "the join table" after. Never "junction", "junction model", "junction table", "junction record", or "join model" (the checker fails on it). -- Relation fields: "a relation field, typed as the other model" and "the field that stores the other record's primary key" (the foreign key column on SQL, the field holding the `_id` on MongoDB). "Back-relation" and "virtual" only with the plain meaning beside them ("stores nothing in the database"). -- `prisma contract emit` is the command that checks the contract. Say "`npx prisma contract emit` rejects it with an error whose `code` is `X`". Never "emit rejects", "the schema compiler", "the emitter lowers", "lowered". Referential actions: "become the foreign key's `ON DELETE` and `ON UPDATE` clauses". -- Banned on these pages in addition to the list: gate / gated; capability in prose; codec; canonical; deterministic; pure; "source of truth"; "escape hatch"; "purpose-built"; "first-class"; "wiring" / "wired"; "hangs off"; "consult"; "authoritative"; "physical" (write "in the database"); "idiomatic" (write "the usual choice" or "what MongoDB expects"); "denormalize" only with its meaning beside it ("keep a small copy of the fields a read needs"); "hot paths" (write "reads that run often"); "opaque"; "polymorphism" as a bare noun (write "a base model with variants"); "lifecycle"; "injected" (write "filled in for you"); "narrow" as a verb for `.variant()` only with "limit the query to one variant" beside it; "at the storage level" (write "in the table"); "rides along"; "lives on" / "lives inside" (write "is on", "is stored inside"). -- Types: the scalar table names Prisma types (`String`, `Int`, ...) and says what PostgreSQL column each becomes only where it matters (`Decimal`, `Bytes`, `Json`, `Jsonb`). Type ids such as `pg/text@1` do not belong in prose on these pages. Inside a code block where the syntax requires one (an `enum` needs `@@type`), show it and explain it in one clause ("stored as PostgreSQL `text`"), nothing more. Version references to a pre-release ("before 0.17") do not belong on these pages; say what the type is now. -- Query examples on these pages are short and use the page's own models. The ORM client is `db.orm.public.User` on PostgreSQL and `db.orm.users` on MongoDB (see conventions.md). Link to `/orm/fundamentals/relations-and-joins` for querying; do not restate it. -- "Prompt your coding agent" sections: keep, but the first sentence reads "Projects created with `npm create prisma@latest` include the Prisma ORM skills for your coding agent. In an existing project, run `npx prisma skills sync`." Keep the example prompts. -- The source of truth is prisma/orm at 8.0.0-rc.10 (`cfccb09be2`), exported at `wip/prisma-src/`. rc.11 changed nothing in the contract language, so rc.10 facts hold for rc.11. -- The contract-authoring pages under `/orm/contract-authoring/` (PSL syntax, TypeScript builder, the artifacts) and the CLI pages under `/cli/` are out of scope; link to them, do not restate attribute lists or flags. diff --git a/docs/orm-docs-audit/c21-data-modeling/relational-databases.fix1.md b/docs/orm-docs-audit/c21-data-modeling/relational-databases.fix1.md deleted file mode 100644 index fe9610fae3c..00000000000 --- a/docs/orm-docs-audit/c21-data-modeling/relational-databases.fix1.md +++ /dev/null @@ -1,56 +0,0 @@ -Done. Page rewritten at `/Users/will/Projects/prisma/web/.claude/worktrees/pr-conflicts-review-fb93f7/wip/c21/dm/r1/relational-databases.mdx`. 226 lines before, 216 after (budget 232). `check-plain.sh` reports clean, no em-dashes, no semicolons in prose. - -## Marks and what I did - -**Sentences the reader could not restate** - -- **Line 81, one-to-one paragraph (six ideas, unexplained command and `PSL`).** Split into one-idea sentences with the rule before the error code: "It has to be optional, because nothing in the database guarantees a matching profile row. The foreign key it mirrors has to be `@unique`. If it is not, `npx prisma contract emit` reports an error whose `code` is `PSL_NON_UNIQUE_BACKRELATION`." -- **Line 46, "matched to the foreign key" (passive, actor hidden).** Now: "`npx prisma contract emit` matches it to the foreign key on `Post`." The two-relations case is answered in a new paragraph under "How relations are declared". -- **Line 136, how the list fields find `PostTag`.** Now names the actor and the target: "`npx prisma contract emit` matches them to the two relations on `PostTag`, so queries read `post.tags` directly. If a second join table connects the same two models, name the relation on both sides." -- **Line 198, `db.orm.public.Bug` vs `Task.variant("Bug")`.** The contradiction is gone. The page now says "Read and write variants through the base model", which is what the source shows. -- **Line 209, "keep their constraints" with an unstated contrast.** Now stated directly: shared-table columns "are nullable there"; own-table columns "can be `NOT NULL`, and reading a full variant costs a join". -- **Line 226, five bare referential-action names.** Each now has a short gloss (`Cascade` deletes or updates the matching rows, `Restrict` blocks the change, `SetNull` clears the foreign key, `SetDefault` writes its default value, `NoAction` leaves the decision to the database). - -**Words the reader had to guess** - -- `npx prisma contract emit`: introduced on first use with the conventions wording, plus the contract definition, in "How relations are declared". -- `db.orm.public`: explained at the first TypeScript example with the conventions wording, and the missing `data:` wrapper is flagged in the same sentence. -- Three names for one field: everything is "the mirror field" now, defined once as "the field on the other model, which stores nothing in the database". "Back-relation" no longer appears in prose (only inside error-code names). -- "the model for the join table": kept on first use, then `PostTag` or "the join table". -- "dependent side": the definition now arrives in the same sentence as the term. -- "discriminator": defined at first use ("One field on the base model, the discriminator, records which variant each row is"). - -**"So what do I type?"** - -1. Migration command added: `npx prisma migration plan --name add-posts` then `npx prisma db migrate`, with a link to `/orm/migrations/generating-a-migration`. -2. `.variant(...)` now says it takes the variant's model name, `"Bug"`, not the discriminator value `"bug"`. -3. Variant `@id`: "it needs no `@id` of its own, because it uses the base model's primary key". -4. Optional relations: new sentence pair with `authorId Int?` / `author User?` and the mismatch error code. -5. Mirror field optional to declare: stated in the one-to-many section. -6. Two relations between the same models: new paragraph with `@relation("Authored", ...)` and `PSL_AMBIGUOUS_BACKRELATION`. -7. "query from less often": cut. -8. The `(t) => ...` callback: "`t` is the argument of the nested write callback, and it offers `t.create([...])`, `t.connect([...])`, and `t.disconnect([...])`, each taking an array", with the link out. -9. `data:` wrapper: covered above. - -**Mechanism the reader did not need** - -- Storage layouts now end with a recommendation: "Start with the shared table. Give a variant its own table when its columns have to be `NOT NULL` in the database, or when the variant has many columns of its own." This follows from the two bullets above it; nothing in the source declares a default. -- "two one-to-many relations back to back": cut. -- The relational-theory sentence at line 111: cut to one sentence about needing a third model. -- Error codes now come after the rule everywhere. - -**Cuts that paid for the additions:** the repeated `Profile` code block under "Which side owns the foreign key", the second `PostTag` code block (the `addedAt` example is one prose sentence now), and the mechanism sentences listed above. - -## Facts looked up (rc.10 at `wip/prisma-src/`) - -- `.variant(...)` takes the variant model name: `VariantNames` resolves to the keys of the model's `variants` record (`packages/3-extensions/sql-orm-client/src/types.ts:554`), and the interpreter keys that record by model name (`packages/2-sql/2-authoring/contract-psl/src/interpreter.ts:1816`). Tests use `.variant('Bug')` / `.variant('Feature')` against `@@base(Task, "bug")` (`test/integration/test/sql-orm-client/polymorphism.test.ts:148,191`). -- Variant has base fields plus its own, and no own `@id`: `stripStorageOnlyDomainFields` with `syntheticPkFieldsByVariant` drops the link column from the variant's create/read input (`interpreter.ts:1878-1884`, comment at `interpreter.ts:1891-1905`); the test rows carry base fields plus the variant field (`polymorphism.test.ts:153-171`). -- `db.orm.public.Bug` vs `Task.variant("Bug")`: `OrmNamespace` exposes every model in the namespace by name (`packages/3-extensions/sql-orm-client/src/orm.ts:70-80`), and variant models stay in the contract, so the accessor type exists. Nothing in `collection.ts` or the integration tests reads or writes a variant through its own accessor: every case goes through the base collection with `.variant(...)`, and `collection.ts:1538` restricts some mutations on a variant-limited collection. So the page states only what the source shows. -- Ambiguous relations: `PSL_AMBIGUOUS_BACKRELATION`, fix is `@relation(name: "...")` or `@relation("...")` on both sides (`psl-relation-resolution.ts:417-441`). -- Nullability: `PSL_RELATION_NULLABILITY_MISMATCH` when the relation field and its foreign key fields disagree (`interpreter.ts:702-717`). -- Mirror field optional to declare: relations are built from the foreign-key side, and there is no missing-back-relation diagnostic anywhere in `contract-psl/src` (grep for `MISSING_BACKRELATION` / `NO_BACKRELATION` returns nothing). -- Shared-table columns are nullable in the table while the variant's own field keeps its declared nullability (`materializeStiVariantStorageColumns`, `interpreter.ts:1975-2030`); own-table variants get the base primary key as a link column with a foreign key back (`materializeMtiVariantStorageLinks`, `interpreter.ts:1890-1970`). - -## Q list - -None. Every fact the reader needed was in the source. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/relational-databases.fix2.md b/docs/orm-docs-audit/c21-data-modeling/relational-databases.fix2.md deleted file mode 100644 index 7cc4263f097..00000000000 --- a/docs/orm-docs-audit/c21-data-modeling/relational-databases.fix2.md +++ /dev/null @@ -1,54 +0,0 @@ -Done. The page is at 215 lines (was 216), plain-language check clean, no em-dashes. - -## Marks and what I did - -**Line 27, four new things at once** — rewritten in the ordered sequence: contract definition first ("Your contract is the `contract.prisma` file that replaced `schema.prisma`"), then "After every change to it, run `npx prisma contract emit`. That command checks the contract and regenerates your types. It only updates your code." Then "To change the database, run `npx prisma migration plan --name add-posts`, which writes a migration file, and then `npx prisma db migrate`, which applies that file." Cut the `contract.json` / `contract.d.ts` clause. - -**Line 15, "stores the connection"** — now "The model whose table gets the foreign key column declares two fields." - -**Line 25, garbled clause and "shapes"** — "Deciding which side that is is the main choice in each of the relation kinds below." "Shapes" replaced with "kinds" everywhere. - -**Line 54, "mirror field" arrives cold, v7 contradiction** — "`posts Post[]` on `User` is the mirror field, the field on the other side of the relation, which stores nothing in the database. Declaring it is optional. Prisma ORM 7 required this field; Prisma ORM 8 does not." The "emit matches it to the foreign key" clause is cut. - -**Line 91, "bare field" plus five unordered rules** — now numbered in application order: "Three rules, in the order you apply them. First, add the field: `profile Profile?` on `User`, with a type and nothing else, because it needs no `@relation` of its own. Second, make it optional... Third, make the foreign key it mirrors `@unique`, or `npx prisma contract emit` reports an error whose `code` is `PSL_NON_UNIQUE_BACKRELATION`." - -**Line 109, "the model for the join table" repeated four times** — kept as the first-use name (page-conventions requires it), then `PostTag` or "the join table" afterwards. Repeats at old lines 134 and 152 are gone. - -**Line 134, "name the far side"** — "Note that `Post.tags` is typed `Tag[]`, not `PostTag[]`: you name the model at the other end, and Prisma ORM works out the join through `PostTag`." The emit-matching clause is cut. - -**Line 136, "pick one per pair"** — cut, along with the second code block. The alternative is now one inline clause: "You can also write the same row yourself with `db.orm.public.PostTag.create({ postId: post.id, tagId: tag.id })`." - -**Line 148, `t.create` / `connect` / `disconnect` named but not explained** — "`t.create([...])` makes new related records and links them, `t.connect([...])` links existing ones, and `t.disconnect([...])` removes the link." Only the `connect` example remains. - -**Line 184, variant has no `@id`** — unchanged; the sentence already answers it and the reader could restate it. - -**Line 186, two nearly identical strings** — now says what goes wrong: "it takes the variant's model name, `"Bug"`. Passing the discriminator value `"bug"` is a TypeScript error, because the only names it accepts are the model names of the base model's variants." - -**Line 216, five definitions in one run, and no default** — the bullet now states the two clauses it produces, then what happens when you write neither, then a five-item sub-list with one action per line. - -**"contract" / "PSL" / "`data:` wrapper" guesses** — contract defined at first use; "`PSL` is the Prisma Schema Language, the language `contract.prisma` is written in" added at the first error code; the `data:` wrapper sentence cut. - -**"Where does `db` come from?" and undefined `user`** — "`db` is the client you create once in `src/prisma/db.ts`, the file `npx prisma orm init` writes... Here `user` is a user record you read earlier." - -**"What is `public` and how do I change it?"** — "`public` is the PostgreSQL schema your tables live in, unless your contract puts the model in another schema with a [`namespace` block](/orm/contract-authoring/psl-syntax)." - -**"How do I read a relation?"** — added a one-line `.include("author")` example under one-to-many, paid for by the cuts above. - -**"What does an unfiltered `Task` query return?"** — "A query on `Task` with no `.variant(...)` returns every variant, and each row carries the base fields plus only its own variant's fields, with the discriminator field holding that variant's value, such as `"bug"`." - -**"Is there a `Bug` accessor?"** — left as "Read and write variants through the base model", no claim either way, per the brief. - -**`PSL_ORPHANED_BACKRELATION` reads as unhelpful** — the instruction now comes first ("Declare the model for the join table yourself"), the error code follows in one short sentence. - -## Facts looked up - -- Un-filtered base read: `wip/prisma-src/test/integration/test/sql-orm-client/polymorphism.test.ts:74-128`. Rows carry base fields plus only their own variant's field, with the discriminator holding `'bug'` / `'feature'`. -- Referential actions: `wip/prisma-src/packages/2-sql/2-authoring/contract-psl/src/psl-relation-resolution.ts:24-34` maps only the five names; no default is applied. The DDL builder at `wip/prisma-src/packages/3-targets/3-targets/postgres/src/core/migrations/operations/constraints.ts:31-45` emits `ON DELETE` / `ON UPDATE` only when the value is defined, so writing neither produces a foreign key with neither clause. -- Schemas: `namespace { … }` blocks exist and Postgres accepts explicit blocks (`wip/prisma-src/packages/2-sql/2-authoring/contract-psl/src/interpreter.ts:197-231`), so the "another schema" wording stays, linked to `/orm/contract-authoring/psl-syntax`. - -## Q list - -- **Q1**: the reader asked what the default `onDelete` is. The source shows Prisma ORM writes no `ON DELETE` / `ON UPDATE` clause when you write neither, so the page says exactly that and stops at "the database decides what happens". I did not find a Prisma-level default in `REFERENTIAL_ACTION_MAP` or the DDL builder, and I did not assert PostgreSQL's own `NO ACTION` default because that is not in this source. If you want the page to name `NO ACTION`, that needs your call. -- **Q2**: `/orm/contract-authoring/psl-syntax` does not currently document `namespace` blocks (only `the-contract-artifact.mdx` mentions namespaces, in the JSON). The link I added points at the right page for contract syntax but the reader will not find the block documented there yet. - -Line count: 216 before, 215 after. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/relational-databases.fix3.md b/docs/orm-docs-audit/c21-data-modeling/relational-databases.fix3.md deleted file mode 100644 index 0e427b18a7b..00000000000 --- a/docs/orm-docs-audit/c21-data-modeling/relational-databases.fix3.md +++ /dev/null @@ -1,48 +0,0 @@ -Round three edits are done. 215 lines before, 214 after; plain-language check clean, no em-dashes, no clause-joining semicolons. - -## What I did with each mark - -**"which side that is is" (line 25).** Now: "The side that declares the `fields` argument owns the foreign key. Deciding which side declares `fields` is the main choice in each relation kind below." - -**Migration paragraph crammed into "How relations are declared" (line 27).** The section keeps only the contract sentence and the emit sentence: "...It only updates your code, not the database." The migration commands moved to a Next steps bullet using ``, not `add-posts`. The reader's "what is a contract, really" question stays unanswered by design (contract-authoring owns that term, per page-conventions). - -**`PSL_RELATION_NULLABILITY_MISMATCH` and the PSL acronym sentence.** Cut. The rule remains: "The two fields have to agree. Making one optional and the other required is rejected." - -**Mirror field vs back-relation (line 54).** Added one sentence: "Prisma ORM 7 called it the back-relation, and its error codes still use that word." - -**"read it from the foreign key side only" with no query.** Added inline: `db.orm.public.Post.where({ authorId: user.id }).all()`, followed by the one-time `.all()` definition from conventions.md. - -**`namespace` clause (line 56).** Cut. - -**"Three rules" for the one-to-one mirror field (line 91).** Collapsed to: add `profile Profile?` on `User`, it has to be optional, and "The `@unique` already on `Profile.userId` is what allows the mirror field." `PSL_NON_UNIQUE_BACKRELATION` cut. - -**One-to-one has no TypeScript.** Added `const users = await db.orm.public.User.include("profile").all();` - -**"name the relation on both sides" (line 134).** Now names the two fields: the list field `Post.tags` and `PostTag.post`. Also cut "and Prisma ORM works out the join through `PostTag`". - -**"If you leave it out" (line 148).** Now "If you skip that model". `PSL_ORPHANED_BACKRELATION` kept. - -**Three quoting conventions in the polymorphic block.** One clause added: "`@@discriminator(type)` and the first argument of `@@base` take a field or model name without quotes, while the variant value is a string." - -**Type-narrowing a polymorphic read.** Added: "In TypeScript those rows are a union with one member per variant, and each member's discriminator field is that variant's value, so switch on the discriminator field to reach a variant's own fields." - -**Self-relations.** One sentence in one-to-many: "A model can also point at itself: a category with a parent category is an ordinary one-to-many whose relation field is typed as the same model." - -**`NoAction` duplicates writing nothing.** Bullet now: "`NoAction` writes the clause out, which does the same thing as writing no clause at all." - -**Referential actions filed under Next steps.** Moved into "How relations are declared", after the optional-relation paragraph, with a new opening sentence saying what they decide. The five-item list is unchanged apart from `NoAction`. Dropped from Next steps. - -## Facts checked in the source - -- Polymorphic rows are a discriminated union: `VariantRow` in `wip/prisma-src/packages/3-extensions/sql-orm-client/src/types.ts:526-546` maps each variant to `Omit & DefaultModelRow & Record` and unions them, so each member pins the discriminator to that variant's literal value. Matches `wip/prisma-src/test/integration/test/sql-orm-client/polymorphism.test.ts:74-128`. -- Self-relations: `wip/prisma-src/test/integration/test/sql-orm-client/self-relations.test.ts:12-40` shows `invitedById` as an ordinary foreign key on the same model with an `invitedUsers` list. -- `.include("profile")` from the mirror-field side: `wip/prisma-src/test/integration/test/sql-orm-client/include.test.ts:105-147` includes `profile` from `users` and returns the profile row or `null`. - -## Cuts that paid for the additions - -Merged the overview link into the opening paragraph; inlined the `.include("author")` call instead of a fenced block; dropped the closing "Placing the key on the dependent side..." paragraph, which restated the three bullets above it. - -## Q list - -- **Q1.** The reader asked whether polymorphic models need anything extra at migration time (two tables, one discriminator column, nullable extra columns). The CLI is not in the `wip/prisma-src/` export, and I found no test or doc stating what `npx prisma migration plan` does for a base model with variants. Nothing added. -- **Q2.** The reader asked whether `--name` is required on `npx prisma migration plan` and whether the `migration ` vs `db ` split follows a rule. No CLI argument definitions in the export. The page shows `--name ` as the brief specifies and says nothing about the split. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/relational-databases.fix4.md b/docs/orm-docs-audit/c21-data-modeling/relational-databases.fix4.md deleted file mode 100644 index 0e0ad3d59ac..00000000000 --- a/docs/orm-docs-audit/c21-data-modeling/relational-databases.fix4.md +++ /dev/null @@ -1,48 +0,0 @@ -Done. Page is at exactly 214 lines (unchanged budget), plain-language check clean, no em-dashes. - -## What I changed, mark by mark - -**Sentences the reader could not restate** - -- **Line 25, "contract" arriving mid-section.** Moved into the intro, before the first heading: "You write your models in your contract, the `contract.prisma` file that replaced `schema.prisma`. After every change to it, run `npx prisma contract emit`, which checks the contract and regenerates your TypeScript types. It does not touch the database." -- **Line 29, two levels of indirection.** Now: "When a `User` row is deleted or its `id` changes, referential actions decide what happens to the `Post` rows that point at it." -- **Line 35, `NoAction`.** Cut to "`NoAction` is the same as writing nothing." -- **Line 93, "allows the mirror field".** Now: "Because `userId` is unique, the mirror field can be a single `Profile?` instead of a list." -- **Line 138, relation names contradicting line 37.** Reconciled: "If a second join table connects the same two models, the relation name goes on both sides again, which here means the list field, `Post.tags`, and `PostTag.post`, the field that points back at `Post`." Verified against `psl-relation-resolution.ts:417-418`. -- **Line 192, union type in prose.** Replaced with a `switch (task.type)` example returning `task.severity` / `task.targetRelease`, introduced by "Switch on `task.type` to reach a variant's own fields:". -- **Lines 196-197, `@@map` surprise.** Now one paragraph, one sentence each: no `@@map` means the variant is stored in the base model's table with nullable extra columns; its own `@@map` splits it into its own table named by the string you pass; "In Prisma ORM 7, `@@map` only renamed a table." - -**Words guessed at** - -- "contract" and "emit" defined in the intro (above). "mirror field", "discriminator", "variant"/"base" stay as defined; the Prisma ORM 7 "back-relation" history is cut. -- **"is rejected"** now names who rejects and when: "If one is optional and the other is required, `npx prisma contract emit` reports an error." -- **"the model for the join table"** appears once, on first use; every later mention is `PostTag` ("Declare `PostTag` yourself. If you skip it, ..."). -- **`public`** explained in the intro before any use. - -**"So what do I type?"** - -1. **`db` import.** Added once, in the first TypeScript example: `import { db } from "./prisma/db";`. -2. **Command order.** Next steps bullet is now the ordered sequence: edit `contract.prisma`, `npx prisma contract emit`, `npx prisma migration plan --name `, `npx prisma db migrate`. The duplicate emit sentence is gone from "How relations are declared". -3. **`public` off PostgreSQL.** `description` and `metaDescription` now say PostgreSQL only; MongoDB keeps its own Next steps link. -4. **Self-relations.** Added "One relation between a model and itself needs no relation name." -5. **Mirror fields in many-to-many.** Added: "`Post.tags` and `Tag.posts` are the mirror fields that `t.connect` goes through. Leave them out and you write the `PostTag` rows yourself, with `db.orm.public.PostTag.create({...})`." -6. **`SetDefault` with no column default.** Nothing added, see Q1. -7. **Naming a variant's table.** Covered by "split into its own table, named by the string you pass". - -**Internals** - -Cut: the Prisma ORM 7 back-relation history, the `NoAction` SQL trivia, and the "why `"bug"` is a TypeScript error" reasoning (now just "Pass the model name, `"Bug"`, with a capital letter. `"bug"` is a type error."). Kept the `ON DELETE` / `ON UPDATE` sentence because page-conventions.md mandates that wording. - -**Ordering.** The `db` / `db.orm` / `public` explanation is now in the intro, ahead of every use. - -## Facts looked up - -- Self-relation needs no name when there is only one: `/Users/will/Projects/prisma/web/.claude/worktrees/pr-conflicts-review-fb93f7/wip/prisma-src/test/integration/test/sql-orm-client/fixtures/contract.ts:141-142` (`invitedUsers` / `invitedBy`, no relation name). The PSL fixture at `.../fixtures/self-relations/contract.prisma` does use names (`"Management"`, `"Partnership"`), but only because that model has several self-relations, which matches the page's existing rule. -- Join model with no list fields on the far models: `psl-relation-resolution.ts:225-275` and `:397-430` only run junction matching for backrelation list fields, and `PSL_ORPHANED_BACKRELATION` / `PSL_AMBIGUOUS_BACKRELATION` are raised from list-field candidates. With no list fields there are no candidates, so nothing rejects it. -- Many-to-many relation name placement: `psl-relation-resolution.ts:417-418` (the diagnostic tells you to add the name to the list field and to the join model's field pointing back). - -## Q list - -- **Q1** — Reader asked what happens with `SetDefault` when the column has no `@default`: error at emit or at runtime? Grepped `SetDefault` and `SET DEFAULT` under `wip/prisma-src/packages/3-targets/3-targets/postgres/src/core/migrations/` and `wip/prisma-src/packages/2-sql/`. The only hits are the attribute spec (`sql-attribute-specs.ts:424`), the two name mappings (`psl-relation-resolution.ts:29`, `relation-inference.ts:17`), and unrelated column-default DDL. No validation of a missing column default exists in the source, so the page says nothing about it. - -Line count: 214 before, 214 after. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/relational-databases.round1.md b/docs/orm-docs-audit/c21-data-modeling/relational-databases.round1.md deleted file mode 100644 index 73a0474f7bd..00000000000 --- a/docs/orm-docs-audit/c21-data-modeling/relational-databases.round1.md +++ /dev/null @@ -1,69 +0,0 @@ -Read once, top to bottom, as a Prisma 6/7 user seeing version 8 for the first time. - -## Sentences I could not restate after one reading - -**Line 81** — "The foreign key it mirrors must be unique, or `npx prisma contract emit` rejects the field with an error whose `code` is `PSL_NON_UNIQUE_BACKRELATION`." -Stopped by: `npx prisma contract emit` (never introduced anywhere on the page — I know `prisma generate` and `prisma migrate`, not this), and `PSL`. Also the whole paragraph is six ideas in one block: declare on the FK side, you may add a mirror field, it needs no `@relation`, it must be optional, why it must be optional, and the error you get if the FK isn't unique. -Plainest wording I'd have understood: "You can also add `profile Profile?` to `User`. It must be optional, and the `userId` field it points at must be `@unique`." - -**Line 46** — "it stores nothing in the database and is matched to the foreign key on `Post`." -Stopped by: matched *by whom*, and matched *how*? Passive voice hides the actor. If I have two relations between `User` and `Post` (author and editor), how does it know which is which? The page never says, and in the version I know that needed a named `@relation("...")`. - -**Line 136** — "The list fields on `Post` and `Tag` name the far side (`Tag[]`, `Post[]`), and `npx prisma contract emit` connects them through `PostTag`, so queries read `post.tags` directly." -Stopped by: same "how does it know" problem, now harder. `Post.tags` is typed `Tag[]`, but there is no `@relation` on it and `PostTag` is never named. If two join models existed I'd have no idea what happens. Also the unexplained command again. - -**Line 198** — "Variants appear as their own models in queries, `db.orm.public.Bug` alongside `db.orm.public.Task`. A query on the base model returns every variant. To write a variant, limit the query to one variant with `.variant(...)` and create through it." -Stopped by: it tells me `db.orm.public.Bug` exists, then the example never uses it and instead writes through `Task.variant("Bug")`. Can I call `db.orm.public.Bug.create(...)`? The page implies yes and then does the opposite. I could not restate what the rule is. - -**Line 209** — "That table holds its extra columns plus the base model's primary key, which links it back to the base table. Its columns keep their constraints, at the cost of a join to read a full variant." -Stopped by: "keep their constraints" — constraints compared to what? I have to reconstruct it from the previous bullet's "have to be nullable there." Two bullets carrying an implicit contrast that's never stated as a contrast. - -**Line 226** — "Referential actions: add `onDelete` and `onUpdate` to `@relation` with `Cascade`, `Restrict`, `SetNull`, `SetDefault`, or `NoAction`, as in `@relation(fields: [userId], references: [id], onDelete: Cascade)`. They become the foreign key's `ON DELETE` and `ON UPDATE` clauses." -Stopped by: five bare enum values with no hint of what any of them does, crammed into a bullet under "Next steps." I can't restate what `SetDefault` does. Either explain them or link out; a naked list teaches nothing. - -## Words and phrases I had to guess - -- **`npx prisma contract emit`** — guessed it's the version 8 replacement for `prisma generate`, i.e. it validates the schema and writes the typed client. The page uses it three times (lines 81, 136, 164) as the thing that reports schema errors and never once says what it is or when I run it. -- **`PSL`** in `PSL_NON_UNIQUE_BACKRELATION` / `PSL_ORPHANED_BACKRELATION` — guessed "Prisma Schema Language." -- **`db.orm.public`** — guessed `public` is the Postgres schema name and `orm` separates this from some other namespace. Pure guessing; it appears in every TypeScript example and is never explained. -- **"back-relation"** (line 46) vs **"the mirror field"** (line 81) vs **"list fields"** (line 136) — I guessed these are all the same concept: the field that has no column behind it. Three names for one thing in one page. -- **"the model for the join table"** (line 111) — guessed this is what used to be called an explicit many-to-many join model. The phrase is used five times and reads like a mouthful each time. -- **"dependent side"** (line 85) — guessed correctly from the bullets that follow, but the term arrives before the explanation. -- **"discriminator"** (line 171) — guessed: the column storing which variant a row is. The page does define it at line 196, one paragraph after first use. - -## "So what do I actually type?" - -1. **After I edit the schema, what command do I run to get the tables?** The page never mentions a migration step. `npx prisma contract emit` is presented only as the thing that *rejects* bad schemas. I've written a schema and have no idea what to run. -2. **`.variant("Bug")` — is that the model name or the discriminator value?** `@@base(Task, "bug")` declares the string `"bug"`, and then line 201 types `.variant("Bug")`. Two different strings, one capitalized, one not, and no statement of which one `.variant()` wants. This is the single worst spot on the page: I would type the wrong thing. -3. **Does `Bug` need its own `id`?** The `Bug` and `Feature` models have no `@id`. I assume it's inherited from `Task`, but nothing says so, and every other model on the page declares one. -4. **How do I make a relation optional?** A post with no author, `authorId Int?` — the single most common thing after the basic case, and it does not appear anywhere. -5. **Is the back-relation required?** `User` has `posts Post[]` in the one-to-many example. `User` has no `profile` field in the one-to-one example, and the prose says you "can" add it. So is the list field on the one-to-many side optional too, or required? Not stated. -6. **Two relations between the same two models.** Nothing on naming relations. In the version I know this was mandatory syntax; if version 8 changed it, I need to be told. -7. **Line 105: "put the key on the side you query from less often."** No reason given, so I can't judge whether the advice applies to me, and I don't know what "query from" means when both sides are readable. -8. **`tags: (t) => t.connect([{ id: tag.id }])`** — the arrow-function form is brand new to me and appears with no explanation. What else can `t` do? Is there a `disconnect`? Is the array required for one item? -9. **`Post.create({ title, authorId })`** — I expected `data: { ... }`. The wrapper is gone. The page shows it but never flags it as a change, so I'd assume I'd misread. - -## Mechanism where I wanted instructions - -- **Lines 206–209, the two storage layouts.** I asked "how do I add a variant" and got a lecture on table-per-hierarchy vs table-per-type. Worse, the switch between them is `@@map` — a directive that in the version I know just renames a table. Making a table name also decide the physical layout is surprising, and the page presents it as a fact to absorb rather than a choice with a recommendation. Tell me which one to use by default. -- **Line 136, "it is two one-to-many relations back to back."** That's how the tool thinks about it internally. What I needed was: here's the model, here's what you type to connect a pair. -- **Line 111, "A single foreign key cannot express this, because each side needs to point at many records."** Relational-database theory I already have. One sentence about needing a third model would do. -- **The error codes** (`PSL_NON_UNIQUE_BACKRELATION`, `PSL_ORPHANED_BACKRELATION`). Telling me the internal error identifier before telling me the rule inverts the order. State the rule; the code is noise unless I'm already staring at it. -- **Line 164** is the exception and is the best-written passage on the page: it names the thing I'd try from the old version, says it's gone, and says what to do instead. More of that. - -## Could I do what the page is for, after one reading? - -Partly. I could write a one-to-many and a one-to-one correctly, and I could write the many-to-many schema. Everything past the schema, I could not. - -What I would still not know: - -- What command turns my edited schema into actual database tables. The page never names a migration step. -- What `npx prisma contract emit` is, even though it is the page's authority for three separate rules. -- What `db.orm.public` means, so I couldn't adapt any TypeScript example to my own project with confidence. -- Which string `.variant()` takes. I'd get this wrong on the first try. -- How to make any relation optional. -- How to model two relations between the same pair of models. -- Whether to write a variant through `db.orm.public.Bug` or `Task.variant(...)`. -- What any of the five referential actions do. - -The schema half of this page works. The "and then you use it" half assumes I already know the version 8 client, and I don't — that's the whole reason I'm reading a version 8 page. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/relational-databases.round2.md b/docs/orm-docs-audit/c21-data-modeling/relational-databases.round2.md deleted file mode 100644 index d50e12fcd29..00000000000 --- a/docs/orm-docs-audit/c21-data-modeling/relational-databases.round2.md +++ /dev/null @@ -1,73 +0,0 @@ -Read once, top to bottom, as a Prisma 7 user. - -## Sentences I could not restate after one reading - -**Line 27** — "Run `npx prisma contract emit` after every change to your contract, the `contract.prisma` file that replaced `schema.prisma`. It checks the contract and writes `contract.json` and `contract.d.ts` beside it. To create the tables themselves, run `npx prisma migration plan --name add-posts` and then `npx prisma db migrate`, as described in [Generating a migration](...)." - -What stopped me: four new things at once. "Contract" is never defined — it's used as both the file and some abstract thing ("your contract"). I don't know what `contract.json` and `contract.d.ts` are for, or whether `contract emit` is the new `prisma generate`. And I don't know why creating tables takes two commands instead of one, or what `migration plan` produces that `db migrate` then consumes. Plainest version I'd have understood: "`contract.prisma` is the new name for `schema.prisma`. After editing it run `npx prisma contract emit`, which checks it and regenerates the client types. That only updates your code — to change the database, run X, which writes a migration file, then Y, which applies it." - -**Line 15** — "The model that stores the connection has two fields." I read "stores the connection" as some new storage concept before working out you just mean the table with the foreign key column in it. Say that: "The model whose table gets the foreign key column declares two fields." - -**Line 25** — "Which side that should be is the main decision in the shapes below." Two problems. "Which side that should be" is a garbled clause I had to re-read. "The shapes below" — I didn't know "shape" was going to be your word for relation kinds until line 35. Plainer: "Deciding which side that is is the main choice in each of the relation types below." - -**Line 54** — "`posts Post[]` on `User` is the mirror field: the field on the other model, which stores nothing in the database." I stalled on "mirror field" — it's a term I've never seen (in Prisma 7 this was just the other side of the relation, or the back-relation). The definition that follows is fine, but the term arrives before I know I need one. Also "Mirror fields are optional to declare" contradicted my whole mental model from v7, where leaving it off was an error, and the page doesn't flag that this changed. - -**Line 91** — "A bare field typed as the other model is enough, and it needs no `@relation` of its own." "Bare field" is doing work I had to guess at. And this paragraph stacks five separate rules (add it, no `@relation`, must be optional, the FK must be `@unique`, error code if not) into five short sentences with no ordering. I could restate each one but not the paragraph. - -**Line 109** — "you add a third model, the model for the join table, which holds one record per connected pair." "The model for the join table" is a clumsy phrase you then repeat three more times (lines 134, 152, 209). In v7 I called this the relation table or join model. Pick one short name and use it. - -**Line 134** — "The mirror fields on `Post` and `Tag` name the far side (`Tag[]`, `Post[]`), and `npx prisma contract emit` matches them to the two relations on `PostTag`." "Name the far side" — I guessed this means the field's type is the model at the other end of the relation, not `PostTag`. That's the surprising part and it's buried. Say it outright: "Note that `Post.tags` is typed `Tag[]`, not `PostTag[]` — you name the model at the other end, and Prisma works out the join through `PostTag`." - -**Line 136** — "Both write the same row, so pick one per pair: running both inserts the same composite key twice, and the second fails." Fine mechanically, but I had no idea why I'd ever be tempted to run both, so the warning read as a puzzle. - -**Line 148** — "`t` is the argument of the nested write callback, and it offers `t.create([...])`, `t.connect([...])`, and `t.disconnect([...])`, each taking an array." This explains the syntax of an API I've never seen by naming its parts. I wanted to know why it's a callback at all and what the difference between `create` and `connect` is. The v7 equivalent was a plain object; this is a real change and gets one sentence. - -**Line 184** — "A variant has every field of its base plus the ones it declares, and it needs no `@id` of its own, because it uses the base model's primary key." Restatable, but note `Bug` and `Feature` in the code block above have no `@id` and I'd already flagged that as a typo before reaching this line. - -**Line 186** — "`.variant(...)` limits the query to one variant, and it takes the variant's model name, `"Bug"`, not the discriminator value `"bug"`." I understand the words. I do not understand why two nearly identical strings exist, or what happens if I pass the wrong one. This looks like a trap and the page just points at it. - -**Line 216** — "Referential actions: add `onDelete` and `onUpdate` to `@relation`, as in `...`. `Cascade` deletes or updates the matching rows, `Restrict` blocks the change, `SetNull` clears the foreign key, `SetDefault` writes its default value, and `NoAction` leaves the decision to the database. They become the foreign key's `ON DELETE` and `ON UPDATE` clauses." Five options defined in one sentence, inside a bullet in "Next steps". I can't hold five definitions from one comma-separated run. Also it doesn't say what the default is when I write neither — that's the only thing I actually needed. - -## Words and phrases I had to guess - -- **"contract"** — guessed it's the new word for schema, and `contract emit` is the new `prisma generate`. Never confirmed. -- **"PSL"** in `PSL_RELATION_NULLABILITY_MISMATCH` etc. — guessed "Prisma Schema Language". Never expanded anywhere on the page. -- **"mirror field"** — guessed: what v7 called the back-relation / the other side. -- **"shapes"** (lines 25, 35) — guessed: kinds of relation. -- **"far side"** (134) — guessed: the model at the other end of the relation. -- **"bare field"** (91) — guessed: a field with a type and nothing else, no attributes. -- **"discriminator"** (158) — the page does define it, but only after using it in the heading sentence. I guessed correctly from the SQL sense. -- **"variant models"** (158) — guessed: subtypes. The page never says whether this is inheritance in the TypeScript sense. -- **"the `data:` wrapper from Prisma ORM 7 is gone"** (58) — I knew what this meant only because I'd used v7. Fine for me, meaningless to anyone else. -- **"ORPHANED_BACKRELATION"** (152) — the error name has nothing to do with "you used an implicit many-to-many". I'd have no idea why I got this error if I hit it in the wild. - -## Where I asked "so what do I actually type?" - -1. **Where does `db` come from?** Line 61 is `await db.orm.public.Post.create(...)` with no import, no construction, no file. This is the first line of runtime code on the page and I cannot run it. `user` in `authorId: user.id` is also undefined. -2. **What is `public` and how do I change it?** Line 58 says it "is `public` unless you set another one" — set it where? In `contract.prisma`? In the connection URL? Circular and unactionable. -3. **What's the actual command sequence for a schema change?** Line 27 lists three commands but never as an ordered "do this every time" list, and never again for one-to-one, many-to-many, or polymorphic. By the polymorphic section I no longer knew whether adding `@@base` needs a migration. -4. **How do I read a relation?** Every read is deferred to `/orm/fundamentals/relations-and-joins`. That's four separate punts. At minimum I wanted one `include`-style example on a page about modeling relations, because how I read it is half of how I decide who owns the key. -5. **How do I query a variant's own model?** "Read and write variants through the base model" implies `db.orm.public.Bug` doesn't exist, but the page never says so. I'd try it and find out. -6. **What do `t.create` and `t.disconnect` look like?** Named at line 148, never shown. Only `connect` has an example. -7. **What does a query on `Task` with no `.variant()` return in TypeScript?** Line 192 says "returns every variant" — as what type? A union? Base fields only? This decides whether the feature is usable. -8. **`npx prisma skills sync`** — run where, and does it need anything else? -9. **What's the default `onDelete`?** See above. - -## Places explaining internals when I wanted instructions - -- **Line 27** — "It checks the contract and writes `contract.json` and `contract.d.ts` beside it." I don't care what files it writes. Tell me to run it after every schema edit. -- **Line 54** — "`npx prisma contract emit` matches it to the foreign key on `Post`." Also line 134, same move. The matching step is the tool's business. What I need to know is: declare it or don't, and here's what changes for me if you do. -- **Line 152** — "`npx prisma contract emit` rejects it with an error whose `code` is `PSL_ORPHANED_BACKRELATION`, and the message tells you to add an explicit model for the join table." Three clauses about the error to deliver one instruction: implicit many-to-many is gone, write the join model yourself. -- **Line 197** — "That table holds its extra columns plus the base model's primary key, which links it back to the base table." This one earns its place, because it explains the join cost in the next clause. The rest of the error-code and file-writing detail does not. - -Four separate error codes (lines 29, 31, 91, 152) are presented as page content. They read as reference material bolted into a tutorial. None of them is something I'd look up here. - -## Could I do what the page is for, after one reading? - -I could write the `.prisma` models. The four schema examples are clear, the foreign-key ownership rule at line 25 is stated once and correctly, and the one-to-one "which side" section (93–103) is the best-written part of the page — three concrete signals, all pointing the same way. - -I could not run any of it. I don't know how to get a `db` object, I don't know the full command sequence to get from an edited `contract.prisma` to tables in Postgres, and I don't know what `contract emit` is actually for beyond "run it". I'd also be stuck the first time I wanted to read a relation back, since every read is a link to another page. - -The polymorphic section I'd have to reread. I understand `@@discriminator` and `@@base` in isolation, but not what the TypeScript side looks like — what comes back from an un-filtered `Task` query, whether variant models exist as their own entry points, or why `.variant("Bug")` and the `"bug"` discriminator value are spelled differently. - -Path: `/Users/will/Projects/prisma/web/.claude/worktrees/pr-conflicts-review-fb93f7/wip/c21/dm/r2/relational-databases.mdx` \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/relational-databases.round3.md b/docs/orm-docs-audit/c21-data-modeling/relational-databases.round3.md deleted file mode 100644 index 961098ec891..00000000000 --- a/docs/orm-docs-audit/c21-data-modeling/relational-databases.round3.md +++ /dev/null @@ -1,72 +0,0 @@ -Read once, top to bottom, as a Prisma 7 user who has never seen v8. - -## Sentences I could not restate after one reading - -**Line 25** — "The side that declares the `fields` argument owns the foreign key. Deciding which side that is is the main choice in each of the relation kinds below." -"is is" made me re-read to check it wasn't a typo for something else. Plainer: "Deciding which side declares `fields` is the main choice in each relation kind below." - -**Line 27** — the whole paragraph. "Your contract is the `contract.prisma` file that replaced `schema.prisma`. After every change to it, run `npx prisma contract emit`. That command checks the contract and regenerates your types. It only updates your code. To change the database, run `npx prisma migration plan --name add-posts`, which writes a migration file, and then `npx prisma db migrate`, which applies that file." -Four things at once: a renamed file, a new noun ("contract"), a codegen command, and a two-step migration flow — dropped mid-way through a section about how to declare a relation. I do not know what "contract" *means* beyond "the new name of schema.prisma," and the page never says. If that's all it means, say so; if it means more, say what. I would split this into its own short section or a callout, not bury it between two relation explanations. - -**Line 29** — "Making one optional and the other required is rejected with an error whose `code` is `PSL_RELATION_NULLABILITY_MISMATCH`. `PSL` is the Prisma Schema Language, the language `contract.prisma` is written in." -The error code is internals. I wanted to know the rule: both fields optional or both required. The code tells me nothing I can act on, and then the page has to stop and define an acronym it only introduced in order to print the code. Cut the code, keep the rule. - -**Line 54** — "`posts Post[]` on `User` is the mirror field, the field on the other side of the relation, which stores nothing in the database. Declaring it is optional. Prisma ORM 7 required this field; Prisma ORM 8 does not. Leave it out and the relation still works, and you read it from the foreign key side only." -The v7/v8 contrast is the most useful sentence on the page. But "you read it from the foreign key side only" is the part I couldn't turn into an action — see the "what do I type" list below. - -**Line 56** — "`db` is the client you create once in `src/prisma/db.ts`, the file `npx prisma orm init` writes. `db.orm` holds your models by model name, and `public` is the PostgreSQL schema your tables live in, unless your contract puts the model in another schema with a `namespace` block." -Four new things (`db.ts`, `orm init`, `db.orm`, `public`, `namespace`) in two sentences, as an aside before an example about connecting a post to a user. The `namespace` exception is a detail I cannot use because no `namespace` block is ever shown. I'd cut the exception clause entirely here. - -**Line 91** — "Three rules, in the order you apply them. First, add the field... Second, make it optional... Third, make the foreign key it mirrors `@unique`, or `npx prisma contract emit` reports an error..." -The third rule is the same `@unique` the code block sixteen lines above already has, and which line 75 already told me is what makes the relation one-to-one. Reading this, I genuinely could not tell whether the mirror field imposes a *new* `@unique` requirement or restates the existing one. Also "Three rules" is over-structure for what is really: add `profile Profile?`, that's it (the `@unique` is already there). - -**Line 134** — "If a second join table connects the same two models, name the relation on both sides." -"Both sides" of *what*? `Post.tags` and `Tag.posts`, or `PostTag.post` and `PostTag.tag`? Four fields are in play and the sentence picks none. Earlier at line 31 "both sides" meant the two `@relation` attributes; here it can't mean the same thing, because `Post.tags` has no `@relation` at all. - -**Line 148** — "An implicit many-to-many, with list fields on both sides and no join table, as in Prisma ORM 7, is not supported. Declare the model for the join table yourself. If you leave it out, `npx prisma contract emit` reports an error whose `code` is `PSL_ORPHANED_BACKRELATION`." -"If you leave it out" — leave out the join model, I assume, but "it" is three sentences from its antecedent and the nearest noun is "the model for the join table yourself." - -**Lines 210 and 215** — "Write neither and the foreign key gets neither clause, so the database decides what happens." then "`NoAction` leaves the decision to the database." -Those are the same outcome in different words. After one reading I don't know why I'd ever type `NoAction` instead of nothing. Either say what differs or say they're equivalent. - -## Words and phrases I had to guess - -- **"contract"** (line 27, and "unless your contract puts the model…" line 56) — guessed: the new name for the schema file, nothing more. Never actually defined. -- **"mirror field"** (line 54) — guessed: what v7 called the back-relation field. The page's own error codes say `BACKRELATION` (`PSL_AMBIGUOUS_BACKRELATION`, `PSL_NON_UNIQUE_BACKRELATION`, `PSL_ORPHANED_BACKRELATION`) while the prose says "mirror field." Two names for one thing in the same document, never connected. If I search the docs for the error I hit, I won't find "mirror field." -- **"the model for the join table"** (lines 109, 148) — guessed: what v7 docs called an explicit many-to-many / join model. The phrasing is clumsy enough that I wasn't sure whether the model *is* the join table or merely describes one. -- **"variant"** / **"base model"** / **"discriminator"** (lines 154–180) — guessed from context, and the guesses held up. The discriminator explanation is the clearest part of the page. -- **"dependent side"** (line 95) — immediately defined in the same sentence. Fine. -- **`.all()`** (line 68) — guessed: v8's `findMany`. Never stated. -- **`add-posts`** (line 27) — guessed: an arbitrary migration name I choose, not a literal. - -## Where I asked "so what do I actually type?" and got no answer - -1. **Reading a user's posts when I skipped the mirror field.** Line 54 says "you read it from the foreign key side only" and then never shows that query. I'd want `db.orm.public.Post.where({ authorId: user.id }).all()` or whatever it actually is. -2. **Anything at all for one-to-one.** The one-to-one section has zero TypeScript. How do I create a profile for an existing user? How do I read a user with their profile — `.include("profile")` from `User`, or `.include("user")` from `Profile`? The section says "the relation reads from either side" and shows neither. -3. **Naming a relation when two join tables connect the same models** (line 134) — which of the four fields gets the name, and what does the syntax look like when `Post.tags` has no `@relation` to hang it on. -4. **Getting at variant fields after a base query.** Line 188: "each row carries the base fields plus only its own variant's fields." So after `await db.orm.public.Task.all()`, what is the TypeScript type, and how do I narrow a row to a `Bug` to reach `severity`? Switch on `type`? Some helper? This is the first thing I'd hit in real code and it isn't there. -5. **Whether polymorphic models need anything extra at migration time.** Two tables, one discriminator column, nullable extra columns — does `npx prisma migration plan` just handle it? Silence. -6. **`@@discriminator(type)` takes a bare identifier, `@@base(Task, "bug")` takes a bare identifier *and* a string, `@@map("features")` takes a string.** Three quoting conventions in one code block, unexplained. I would get this wrong on the first try. -7. **Self-relations.** A category with a parent category, a user who manages users. Very common, in v7's docs, absent here. I'd have to guess whether it's just a one-to-many pointed at the same model. -8. **`npx prisma migration plan` vs `npx prisma db migrate`** — one is `migration `, the other `db `. Is there a rule to the CLI shape, or do I memorize? Also, is `--name` required? - -## Where it explains the internals when I only wanted to know what to do - -- **Every `PSL_*` error code** (lines 29, 31, 91, 148). Four of them. I only need the rule; I'll read the error when I hit it. These also import the "PSL" acronym and the "backrelation" vocabulary for no reader benefit. -- **Line 134** — "you name the model at the other end, and Prisma ORM works out the join through `PostTag`." The "Prisma ORM works out the join" half is mechanism. The instruction is: type `Tag[]`, not `PostTag[]`. -- **Line 193** — "That table holds its extra columns plus the base model's primary key, which links it back to the base table." Table layout I don't act on. The next clause, "Those columns can be `NOT NULL`, and reading a full variant costs a join," is the part that drives my decision, and the paragraph does use it that way, so only the first half is surplus. -- **Line 27** — "That command checks the contract and regenerates your types. It only updates your code." Fine, actually — it stops me from expecting a database change. Keep. - -## Could I do what the page is for after one reading? - -One-to-many, yes. One-to-one schema, yes; one-to-one *queries*, no, because there aren't any. Many-to-many, mostly — I could write the three models and the `t.connect` call, but I'd be guessing the moment a second join table appeared. Polymorphic, I could write the schema and a `.variant("Bug").create(...)`, and then get stuck the first time I read a mixed list back and needed the variant fields in TypeScript. - -What I'd still not know after one reading: -- What a "contract" is, beyond a renamed file. -- Whether "mirror field" and "backrelation" are the same thing (they clearly are, and the page never says). -- How to query a relation from the side without the mirror field. -- How to type-narrow a polymorphic read. -- How to model a self-relation. -- Why `NoAction` exists when omitting the clause does the same thing. - -One more structural complaint: referential actions — `onDelete: Cascade`, the thing I reach for in nearly every real schema — are a single bullet in "Next steps," below the AI-prompt section. That's a main topic filed under links. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-data-modeling/relational-databases.round4.md b/docs/orm-docs-audit/c21-data-modeling/relational-databases.round4.md deleted file mode 100644 index e51b1aa3d11..00000000000 --- a/docs/orm-docs-audit/c21-data-modeling/relational-databases.round4.md +++ /dev/null @@ -1,76 +0,0 @@ -Read once, top to bottom, as someone coming off two years of Prisma 6/7. - -## Sentences I could not restate after one reading - -**Line 25** — "Your contract is the `contract.prisma` file that replaced `schema.prisma`. After every change to it, run `npx prisma contract emit`. That command checks the contract and regenerates your types." - -This is the first time the word "contract" appears and it arrives mid-section, bolted onto a paragraph about `@relation`. I don't know what a "contract" *is* in this product — is it just the renamed schema file, or a new concept? "Emit" tells me nothing. I'd have understood: "In Prisma 8 the schema file is called `contract.prisma` instead of `schema.prisma`. After you edit it, run `npx prisma contract emit` to regenerate the TypeScript client. This does not touch the database." - -**Line 29** — "Referential actions decide what the database does to the rows on the foreign key side when the row they point at is deleted or its key changes." - -Two levels of indirection ("the rows on the foreign key side", "the row they point at") in one sentence. I had to re-read to work out which is which. Plainer: "When a `User` row is deleted or its `id` changes, referential actions decide what happens to the `Post` rows that reference it." - -**Line 35** — "`NoAction` writes the clause out, which does the same thing as writing no clause at all." - -Stopped me cold. If it does nothing different, why does it exist and why would I ever type it? The page gives me a menu item that it simultaneously says is pointless. Either say why it's there (I vaguely recall it differs from `Restrict` on some databases) or drop it. - -**Line 93** — "The `@unique` already on `Profile.userId` is what allows the mirror field." - -"Allows" is doing unexplained work. Allows it to exist at all? Allows it to be `Profile?` rather than `Profile[]`? I guessed the latter but the sentence doesn't say. Plainer: "Because `userId` is unique, the mirror field can be a single `Profile?` instead of a list." - -**Line 138** — "If a second join table connects the same two models, put the relation name on the list field, `Post.tags`, and on `PostTag.post`, the field that points back at `Post`." - -This contradicts what line 37 told me. Line 37 said names go on "both sides" of the relation, with `fields:` on one side. Here the name goes on a list field and on a *third* model's field. I cannot reconcile the two rules, and there's no code for this one, just prose describing where to put things. - -**Line 192** — "In TypeScript those rows are a union with one member per variant, and each member's discriminator field is that variant's value, so switch on the discriminator field to reach a variant's own fields." - -Three ideas in one sentence and zero code. I think I know what a discriminated union is, but the page is describing the shape of a result type entirely in words. Show me the five-line `switch (task.type)` block instead. - -**Line 196–197** — `@@map` deciding whether a variant shares the base table or gets its own. - -In every Prisma version I've used, `@@map` renames a table. Nothing more. Discovering it silently controls physical storage layout for variants is a genuine surprise, and the page states it as if it were obvious. I re-read it twice to check I hadn't misread. - -## Words and phrases I had to guess at - -- **"contract"** — guessed: the new name for the schema file. Still don't know if it means more than that. -- **"emit"** — guessed: generate the TypeScript client. -- **"mirror field"** (line 60) — guessed correctly from the definition given, but it's a brand-new coinage. You then tell me Prisma 7 called it "back-relation" and error codes still say that, so I now hold two names for one thing. -- **"is rejected"** (line 27) — guessed: `contract emit` errors. The page doesn't say who rejects it or when I'd find out. -- **"the model for the join table"** — guessed this is what used to be called an explicit many-to-many. The phrase is awkward and repeated four times; your own agent prompt at line 207 says "explicit model for the join table", so the word exists. -- **"discriminator"** — guessed: the column recording which variant a row is. The page does define it, but it's one sentence carrying a new keyword, a new concept, and a code example at once. -- **"variant"** vs **"base"** — guessed subclass/superclass. Works, but I only inferred it. -- **`public`** in `db.orm.public.Post` — told it's the PostgreSQL schema. See below. - -## Where I asked "so what do I actually type?" - -1. **`db` is never imported.** Line 62 says it lives in `src/prisma/db.ts`. Every code block then uses a bare `db`. I don't know the import line. Show `import { db } from "@/prisma/db"` once. - -2. **Three setup commands, no order.** `npm create prisma@latest` (line 203), `npx prisma orm init` (line 62), `npx prisma contract emit` (line 25), `npx prisma migration plan --name ` and `npx prisma db migrate` (line 213). I cannot assemble the actual loop. The full sequence — edit `contract.prisma`, emit, plan, migrate — is never written down in one place, and it's the single thing I most need. - -3. **What is `public` if I'm not on PostgreSQL?** The description says "PostgreSQL and other SQL databases". `db.orm.public.Post` appears in every query. On MySQL or SQLite, what do I type there? The page doesn't say, and this is in literally every example. - -4. **Self-relations (line 58).** "A category with a parent category is an ordinary one-to-many whose relation field is typed as the same model." No code. And a self-relation has two fields on one model pointing at each other — by line 37's rule that looks like it needs a `@relation` name. Does it? I'd have to go try it and see if emit errors. - -5. **Are mirror fields required for many-to-many?** Line 60 tells me mirror fields are optional in Prisma 8. The many-to-many example includes `Post.tags` and `Tag.posts`, and line 143's nested write goes through `tags`. So are they optional here too, and if I omit them do I lose `t.connect`? Not answered. - -6. **`SetDefault` with no default.** "writes the foreign key's default value" — what happens if the column has no `@default`? Error at emit, or at runtime? - -7. **How do I name a variant's own table something specific?** `@@map("features")` on `Feature` both splits it into its own table *and* names it `features`. If I want its own table named `Feature`, do I write `@@map("Feature")`? And what table does `Bug` map to — is it `Task`? Not stated. - -## Where the page explains internals instead of what to do - -- **Line 60**, the history lesson: "Prisma ORM 7 called it the back-relation, and its error codes still use that word. Prisma ORM 7 required this field. Prisma ORM 8 does not." Three sentences of version archaeology in the middle of learning one-to-many. The actionable part — "you can leave it out" — is buried. -- **Line 29**, "They become the foreign key's `ON DELETE` and `ON UPDATE` clauses. Write neither and the foreign key gets neither clause, so the database decides." Useful to some, but it's describing generated SQL when I only wanted to know which action to pick. -- **Line 35**, `NoAction` — pure implementation trivia about what SQL gets written, with no guidance attached. -- **Line 186**, the paragraph explaining that passing `"bug"` instead of `"Bug"` is a TypeScript error, *and why* ("the only names it accepts are the model names of the base model's variants"). That's the type system's reasoning. Just tell me: pass the model name, capital B. -- **Line 192**, the whole union-type paragraph, discussed above. - -## Ordering problem - -Line 60 uses `db.orm.public.Post.where(...).all()` in a sentence. Line 62 — the *next* paragraph — is where `db`, `db.orm`, and `public` are finally explained. I hit the code first and had no idea what I was looking at. - -## Could I do what the page is for, after one reading? - -Partly. I could write a one-to-many and a one-to-one schema — those sections are clear and the "which side owns the foreign key" list at line 103 is the best thing on the page. I could write the many-to-many models, though I'd be unsure whether the list fields are required. - -I could not actually run any of it. I don't know the import for `db`, I don't know the command sequence from "I edited the file" to "the database has the table", and if I'm not on Postgres I don't know what replaces `public`. Polymorphic relations I could copy-paste but not adapt: I don't understand what `@@map` will do to my table layout well enough to choose, and I couldn't write the TypeScript that reads a mixed list of tasks because the page describes that result type only in prose. \ No newline at end of file From 91ef3400b174e0b9e8b83d7a236228f8a0c4d513 Mon Sep 17 00:00:00 2001 From: willbot Date: Mon, 14 Sep 2026 17:48:30 +0200 Subject: [PATCH 09/15] docs(orm): data-modeling review fixes from CodeRabbit Temporal check for Homebrew Node.js 26, no flags in the migration sequence, narrower id claim on MongoDB, SetNull and SetDefault preconditions, and include described as a lookup. Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- apps/docs/content/docs/orm/data-modeling/index.mdx | 4 ++-- apps/docs/content/docs/orm/data-modeling/mongodb.mdx | 8 ++++---- .../docs/orm/data-modeling/relational-databases.mdx | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/docs/content/docs/orm/data-modeling/index.mdx b/apps/docs/content/docs/orm/data-modeling/index.mdx index f13b97d0b95..bcd4dd63971 100644 --- a/apps/docs/content/docs/orm/data-modeling/index.mdx +++ b/apps/docs/content/docs/orm/data-modeling/index.mdx @@ -147,7 +147,7 @@ A scalar field holds a single value. The common types: The PostgreSQL names in the table are the column types Prisma ORM creates. `ObjectId` is the only MongoDB-only type. -A `Decimal` value reaches your code as a string, so nothing is rounded through a JavaScript number. A `DateTime` value reaches your code as a `Temporal.Instant`, the standard JavaScript type for a point in time. `Temporal` is built into Node.js 26.8.2 and later. On older versions, install the `temporal-polyfill` package and add `import "temporal-polyfill/full/global"` in your app's entry file, before any query runs. A `Bytes` value reaches your code as a `Uint8Array`. +A `Decimal` value reaches your code as a string, so nothing is rounded through a JavaScript number. A `DateTime` value reaches your code as a `Temporal.Instant`, the standard JavaScript type for a point in time. `Temporal` is built into Node.js 26.8.2 and later, but Homebrew's build of Node.js 26 leaves it out, so check with `node -p "typeof Temporal"`. If that prints `undefined`, install the `temporal-polyfill` package and add `import "temporal-polyfill/full/global"` in your app's entry file, before any query runs. A `Bytes` value reaches your code as a `Uint8Array`. `Json` and `Jsonb` are two separate types. On a `Jsonb` field, `.where()` compares the whole value. You can ask whether the column equals a given document, but not whether a key inside it matches. A `Json` field cannot be compared in `.where()` at all, so use `Jsonb` unless you specifically need PostgreSQL's plain `json` column. Reach into the document with [raw SQL](/orm/reference/raw-queries) when you need a path query. [Extensions](/orm/extensions) add more types, such as vectors or geometry. @@ -248,5 +248,5 @@ Projects created with `npm create prisma@latest` include the [Prisma ORM skills] - [Model relational data](/orm/data-modeling/relational-databases): one-to-one, one-to-many, many-to-many, and polymorphic relations on PostgreSQL. - [Model MongoDB data](/orm/data-modeling/mongodb): embed or reference, and polymorphic collections. -- [Generate a migration](/orm/migrations/generating-a-migration) to get the models you wrote into the database. Edit the contract, then run `npx prisma contract emit` to check it and regenerate your types, then `npx prisma migration plan --name ` to write the migration files, then `npx prisma db migrate` to apply them. `npx prisma contract emit` replaces `prisma generate`. +- [Generate a migration](/orm/migrations/generating-a-migration) to get the models you wrote into the database. Edit the contract, then run `npx prisma contract emit` to check it and regenerate your types, then `npx prisma migration plan` to write the migration files, then `npx prisma db migrate` to apply them. That page has the flags and how the next plan finds its starting point. `npx prisma contract emit` replaces `prisma generate`. - [Query your models](/orm/fundamentals/reading-data) once your contract is in place. diff --git a/apps/docs/content/docs/orm/data-modeling/mongodb.mdx b/apps/docs/content/docs/orm/data-modeling/mongodb.mdx index 6ca64b39b81..97eacfd42dd 100644 --- a/apps/docs/content/docs/orm/data-modeling/mongodb.mdx +++ b/apps/docs/content/docs/orm/data-modeling/mongodb.mdx @@ -23,7 +23,7 @@ model User { In the contract you call the field `id`. In TypeScript it is always `_id`, never `id`. `@map("_id")` sets the name in the database and in your code, so you write `user._id` and `where({ _id })`. `@@map("users")` names the collection. `@@map` is optional. Without it the collection takes the model name with a lowercase first letter, so `user`. `@unique` on `email` becomes a unique index when you migrate. -Write `ObjectId` for the key type. `ObjectId` is the contract type. In TypeScript ids are plain strings, so there is no `ObjectId` to construct. The old `String @db.ObjectId` and `@default(auto())` are no longer allowed. +Write `ObjectId` for the key type. `ObjectId` is the contract type. In TypeScript a document's own `_id` is a plain string, so there is no `ObjectId` to construct. The old `String @db.ObjectId` and `@default(auto())` are no longer allowed. Leave the id out of the values you pass to `create`, and MongoDB assigns it. `db` is the client created in `src/prisma/db.ts`. `db.orm` is the ORM client, one of the query APIs on `db`. See [the ORM reference](/orm/reference). @@ -85,7 +85,7 @@ await db.orm.users.where({ _id: user._id }).update((u) => [u("address.city").set await db.orm.users.where({ _id: user._id }).update((u) => [u.tags.push("admin"), u.tags.pull("new")]); ``` -Replace a whole embedded value with `u.address.set({ ... })`, and remove it with `u.address.unset()`. A list of embedded values is replaced as a whole with `u.addresses.set([ ... ])`. `.where()` filters on top-level model fields only. To match on a field inside an embedded document, use the pipeline builder, a chained builder for MongoDB aggregation pipelines: +Replace a whole embedded value with `u.address.set({ ... })`, and remove it with `u.address.unset()`. A list of embedded values is replaced as a whole with `u.addresses.set([ ... ])`. The object form of `.where()` filters on top-level model fields only. To match on a field inside an embedded document, use the pipeline builder, a chained builder for MongoDB aggregation pipelines: ```ts const query = db.query.from("users").match((f) => f("address.city").eq("Berlin")).build(); @@ -120,7 +120,7 @@ await db.orm.posts.create({ title: "Hello", authorId: user._id }); const posts = await db.orm.posts.include("author").all(); ``` -`.include("posts")` on `db.orm.users` works the other way and gives each user an array of their posts. See [relations and joins](/orm/fundamentals/relations-and-joins) for the rest of the query API. Resolving a reference costs a second read. If a read runs often, you can copy the few fields it needs into the parent document, such as a comment storing its author's name. If the original changes, the copy does not. You update it yourself. +`.include("posts")` on `db.orm.users` works the other way and gives each user an array of their posts. See [relations and joins](/orm/fundamentals/relations-and-joins) for the rest of the query API. Resolving a reference means a lookup into the other collection on every read. If a read is frequent, you can copy the few fields it needs into the parent document, such as a comment storing its author's name. If the original changes, the copy does not. You update it yourself. ## Polymorphic collections @@ -166,7 +166,7 @@ Projects created with `npm create prisma@latest` include the [Prisma ORM skills] ## Next steps -- Start a new project with `npm create prisma@latest -- my-app`, or run `npx prisma orm init` in an existing one. After every change to your contract, run `npx prisma contract emit`, which checks your contract and regenerates your types. Then `npx prisma migration plan --name ` writes a migration, and `npx prisma db migrate` applies it, creating the collections and indexes. See [generating a migration](/orm/migrations/generating-a-migration). +- Start a new project with `npm create prisma@latest -- my-app`, or run `npx prisma orm init` in an existing one. After every change to your contract, run `npx prisma contract emit`, which checks your contract and regenerates your types. Then `npx prisma migration plan` writes a migration, and `npx prisma db migrate` applies it, creating the collections and indexes. [Generating a migration](/orm/migrations/generating-a-migration) has the flags and how the next plan finds its starting point. - [Query documents](/orm/fundamentals/reading-data) and [resolve references](/orm/fundamentals/relations-and-joins) with `.include(...)`. - Aggregate and reshape documents with the [pipeline builder](/orm/fundamentals/advanced-queries#mongodb-pipeline-builder). - To model relational data, see the [relational data modeling guide](/orm/data-modeling/relational-databases). diff --git a/apps/docs/content/docs/orm/data-modeling/relational-databases.mdx b/apps/docs/content/docs/orm/data-modeling/relational-databases.mdx index 797cba7b2af..7528bcfdeaa 100644 --- a/apps/docs/content/docs/orm/data-modeling/relational-databases.mdx +++ b/apps/docs/content/docs/orm/data-modeling/relational-databases.mdx @@ -30,8 +30,8 @@ When a `User` row is deleted or its `id` changes, referential actions decide wha - `Cascade` deletes or updates the matching rows. - `Restrict` blocks the change. -- `SetNull` clears the foreign key. -- `SetDefault` writes the foreign key's default value. +- `SetNull` clears the foreign key, so the foreign key field has to be optional. +- `SetDefault` writes the foreign key's default value, which has to point at a row that exists. - `NoAction` is the same as writing nothing. Two relations between the same pair of models need a name on both sides, such as `@relation("Authored", fields: [authorId], references: [id])` on one side and `@relation("Authored")` on the other. Without the name, `npx prisma contract emit` reports an error. @@ -210,5 +210,5 @@ Projects created with `npm create prisma@latest` include the [Prisma ORM skills] ## Next steps - [Query relations](/orm/fundamentals/relations-and-joins) with `.include(...)`, filters on relation data, and reads across the join table. -- To change the database to match the contract: edit `contract.prisma`, run `npx prisma contract emit`, then `npx prisma migration plan --name `, which writes the migration files, then `npx prisma db migrate`, which applies them. See [Generating a migration](/orm/migrations/generating-a-migration). +- To change the database to match the contract: edit `contract.prisma`, run `npx prisma contract emit`, then `npx prisma migration plan`, which writes the migration files, then `npx prisma db migrate`, which applies them. [Generating a migration](/orm/migrations/generating-a-migration) has the flags and how the next plan finds its starting point. - To model for MongoDB, see the [MongoDB data modeling guide](/orm/data-modeling/mongodb). From dc56811b34fc7a20b82c18a145568cb9a9f17daa Mon Sep 17 00:00:00 2001 From: willbot Date: Tue, 15 Sep 2026 07:32:04 +0200 Subject: [PATCH 10/15] docs(orm): list Release status and Supported databases in the ORM sidebar Both pages live under the Getting Started tree, so a reader browsing the ORM section could not find them. The ORM section's Introduction group now links to them, the way it already links the upgrade guides. Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- apps/docs/content/docs/orm/meta.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/docs/content/docs/orm/meta.json b/apps/docs/content/docs/orm/meta.json index aea00d7971d..c9b6b822fca 100644 --- a/apps/docs/content/docs/orm/meta.json +++ b/apps/docs/content/docs/orm/meta.json @@ -7,6 +7,8 @@ "---Introduction---", "index", "core-concepts", + "[Release status](/prisma-orm/release-status)", + "[Supported databases](/prisma-orm/supported-databases)", "coming-from-prisma-orm-7", "[Prisma ORM 7 to 8 (PostgreSQL)](/guides/upgrade-prisma-orm/postgresql)", "[Prisma ORM 6 to 8 (MongoDB)](/guides/upgrade-prisma-orm/mongodb)", From 94c4eeedafdac4f38f3c4efb6110d7fe8b94613a Mon Sep 17 00:00:00 2001 From: willbot Date: Tue, 15 Sep 2026 07:37:56 +0200 Subject: [PATCH 11/15] docs: move Release status and Supported databases into the ORM section Both pages belong to the ORM section; Getting Started now links to them instead of owning them. Old URLs under /prisma-orm/ redirect. Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- apps/docs/content/docs/(index)/getting-started.mdx | 2 +- apps/docs/content/docs/(index)/index.mdx | 2 +- .../prisma-orm/add-to-existing-project/mongodb.mdx | 2 +- .../prisma-orm/add-to-existing-project/postgresql.mdx | 2 +- apps/docs/content/docs/(index)/prisma-orm/index.mdx | 2 +- apps/docs/content/docs/(index)/prisma-orm/meta.json | 4 ++-- .../docs/(index)/prisma-orm/quickstart/mongodb.mdx | 2 +- .../docs/(index)/prisma-orm/quickstart/postgresql.mdx | 2 +- apps/docs/content/docs/cli/index.mdx | 2 +- apps/docs/content/docs/guides/deployment/meta.json | 8 +------- apps/docs/content/docs/guides/postgres/vercel.mdx | 2 +- apps/docs/content/docs/guides/v7/integrations/meta.json | 7 +------ apps/docs/content/docs/orm/coming-from-prisma-orm-7.mdx | 4 ++-- apps/docs/content/docs/orm/data-modeling/index.mdx | 2 +- apps/docs/content/docs/orm/index.mdx | 2 +- apps/docs/content/docs/orm/meta.json | 4 ++-- .../docs/{(index)/prisma-orm => orm}/release-status.mdx | 4 ++-- .../{(index)/prisma-orm => orm}/supported-databases.mdx | 8 ++++---- apps/docs/next.config.mjs | 6 ++++++ 19 files changed, 31 insertions(+), 36 deletions(-) rename apps/docs/content/docs/{(index)/prisma-orm => orm}/release-status.mdx (97%) rename apps/docs/content/docs/{(index)/prisma-orm => orm}/supported-databases.mdx (85%) diff --git a/apps/docs/content/docs/(index)/getting-started.mdx b/apps/docs/content/docs/(index)/getting-started.mdx index 69361d9b1d7..66e5155dcdb 100644 --- a/apps/docs/content/docs/(index)/getting-started.mdx +++ b/apps/docs/content/docs/(index)/getting-started.mdx @@ -8,7 +8,7 @@ metaDescription: Choose a Prisma ORM quickstart for a new project or add Prisma Start with a quickstart if you want [Prisma ORM](/orm) to create the app. Use the existing-project path if you already have an app and database. -Coming from an earlier version? [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7) maps every Prisma ORM 7 API to its Prisma ORM 8 form, and [Release status](/prisma-orm/release-status) says where Prisma ORM 8 stands today. +Coming from an earlier version? [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7) maps every Prisma ORM 7 API to its Prisma ORM 8 form, and [Release status](/orm/release-status) says where Prisma ORM 8 stands today. ## Start a new project diff --git a/apps/docs/content/docs/(index)/index.mdx b/apps/docs/content/docs/(index)/index.mdx index c5b1cacfb0f..27666209ee3 100644 --- a/apps/docs/content/docs/(index)/index.mdx +++ b/apps/docs/content/docs/(index)/index.mdx @@ -9,7 +9,7 @@ full: true Prisma is a complete TypeScript stack with one workflow: **build** your app and run all of it on your machine, then **deploy** it to the Prisma platform with one command. Check out the [full-stack tutorial](/full-stack-tutorial). -Here for the ORM? Jump straight to [Prisma ORM 7](/orm/v7) or [Prisma ORM 8](/orm), or check the [release status](/prisma-orm/release-status) first. +Here for the ORM? Jump straight to [Prisma ORM 7](/orm/v7) or [Prisma ORM 8](/orm), or check the [release status](/orm/release-status) first. diff --git a/apps/docs/content/docs/(index)/prisma-orm/add-to-existing-project/mongodb.mdx b/apps/docs/content/docs/(index)/prisma-orm/add-to-existing-project/mongodb.mdx index 0974249ff1b..2536a238fa3 100644 --- a/apps/docs/content/docs/(index)/prisma-orm/add-to-existing-project/mongodb.mdx +++ b/apps/docs/content/docs/(index)/prisma-orm/add-to-existing-project/mongodb.mdx @@ -14,7 +14,7 @@ Use this path when you already have an application and database. Make sure the a Prisma ORM 8 is the current release, as a release candidate. Prisma ORM 7 remains fully supported; its docs live at [/orm/v7](/orm/v7) and its setup paths at [/v7/getting-started](/v7/getting-started). -For what release candidate means, when the final release is expected, and how to stay on version 7, see [Release status](/prisma-orm/release-status). +For what release candidate means, when the final release is expected, and how to stay on version 7, see [Release status](/orm/release-status). ::: diff --git a/apps/docs/content/docs/(index)/prisma-orm/add-to-existing-project/postgresql.mdx b/apps/docs/content/docs/(index)/prisma-orm/add-to-existing-project/postgresql.mdx index 2d3108679cf..b1e3ba92659 100644 --- a/apps/docs/content/docs/(index)/prisma-orm/add-to-existing-project/postgresql.mdx +++ b/apps/docs/content/docs/(index)/prisma-orm/add-to-existing-project/postgresql.mdx @@ -14,7 +14,7 @@ Use this path when you already have an application and database. Make sure the a Prisma ORM 8 is the current release, as a release candidate. Prisma ORM 7 remains fully supported; its docs live at [/orm/v7](/orm/v7) and its setup paths at [/v7/getting-started](/v7/getting-started). -For what release candidate means, when the final release is expected, and how to stay on version 7, see [Release status](/prisma-orm/release-status). For the Prisma ORM 8 name of every Prisma ORM 7 API, see [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7). +For what release candidate means, when the final release is expected, and how to stay on version 7, see [Release status](/orm/release-status). For the Prisma ORM 8 name of every Prisma ORM 7 API, see [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7). ::: diff --git a/apps/docs/content/docs/(index)/prisma-orm/index.mdx b/apps/docs/content/docs/(index)/prisma-orm/index.mdx index fefed162bd3..857a578fefd 100644 --- a/apps/docs/content/docs/(index)/prisma-orm/index.mdx +++ b/apps/docs/content/docs/(index)/prisma-orm/index.mdx @@ -12,7 +12,7 @@ Prisma ORM 8 is a ground-up rebuild of Prisma ORM, from the runtime and query AP Prisma ORM 8 is the current release, as a release candidate. Prisma ORM 7 remains fully supported; its docs live at [/orm/v7](/orm/v7) and its setup paths at [/v7/getting-started](/v7/getting-started). -For what release candidate means, when the final release is expected, and how to stay on version 7, see [Release status](/prisma-orm/release-status). For the Prisma ORM 8 name of every Prisma ORM 7 API, see [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7). +For what release candidate means, when the final release is expected, and how to stay on version 7, see [Release status](/orm/release-status). For the Prisma ORM 8 name of every Prisma ORM 7 API, see [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7). ::: diff --git a/apps/docs/content/docs/(index)/prisma-orm/meta.json b/apps/docs/content/docs/(index)/prisma-orm/meta.json index aadc57bc05b..c4a694feacb 100644 --- a/apps/docs/content/docs/(index)/prisma-orm/meta.json +++ b/apps/docs/content/docs/(index)/prisma-orm/meta.json @@ -3,8 +3,8 @@ "defaultOpen": true, "pages": [ "index", - "release-status", - "supported-databases", + "[Release status](/orm/release-status)", + "[Supported databases](/orm/supported-databases)", "create-prisma", "quickstart", "add-to-existing-project" diff --git a/apps/docs/content/docs/(index)/prisma-orm/quickstart/mongodb.mdx b/apps/docs/content/docs/(index)/prisma-orm/quickstart/mongodb.mdx index e33900c983b..abff817e1ee 100644 --- a/apps/docs/content/docs/(index)/prisma-orm/quickstart/mongodb.mdx +++ b/apps/docs/content/docs/(index)/prisma-orm/quickstart/mongodb.mdx @@ -12,7 +12,7 @@ Create a Prisma ORM app with MongoDB, apply the first migration, and run your fi Prisma ORM 8 is the current release, as a release candidate. Prisma ORM 7 remains fully supported; its docs live at [/orm/v7](/orm/v7) and its setup paths at [/v7/getting-started](/v7/getting-started). -For what release candidate means, when the final release is expected, and how to stay on version 7, see [Release status](/prisma-orm/release-status). +For what release candidate means, when the final release is expected, and how to stay on version 7, see [Release status](/orm/release-status). ::: diff --git a/apps/docs/content/docs/(index)/prisma-orm/quickstart/postgresql.mdx b/apps/docs/content/docs/(index)/prisma-orm/quickstart/postgresql.mdx index ca5362698c7..4e3b4a8c6e2 100644 --- a/apps/docs/content/docs/(index)/prisma-orm/quickstart/postgresql.mdx +++ b/apps/docs/content/docs/(index)/prisma-orm/quickstart/postgresql.mdx @@ -12,7 +12,7 @@ Create a Prisma ORM app with PostgreSQL and run your first query against seeded Prisma ORM 8 is the current release, as a release candidate. Prisma ORM 7 remains fully supported; its docs live at [/orm/v7](/orm/v7) and its setup paths at [/v7/getting-started](/v7/getting-started). -For what release candidate means, when the final release is expected, and how to stay on version 7, see [Release status](/prisma-orm/release-status). For the Prisma ORM 8 name of every Prisma ORM 7 API, see [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7). +For what release candidate means, when the final release is expected, and how to stay on version 7, see [Release status](/orm/release-status). For the Prisma ORM 8 name of every Prisma ORM 7 API, see [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7). ::: diff --git a/apps/docs/content/docs/cli/index.mdx b/apps/docs/content/docs/cli/index.mdx index 69f6f463fb4..866a3c89a13 100644 --- a/apps/docs/content/docs/cli/index.mdx +++ b/apps/docs/content/docs/cli/index.mdx @@ -12,7 +12,7 @@ Prisma ORM ships with a unified Prisma CLI. One binary contains the ORM commands Prisma ORM 8 is the current release, as a release candidate. Prisma ORM 7 remains fully supported; its docs live at [/orm/v7](/orm/v7) and its setup paths at [/v7/getting-started](/v7/getting-started). -For what release candidate means, when the final release is expected, and how to stay on version 7, see [Release status](/prisma-orm/release-status). For the Prisma ORM 8 name of every Prisma ORM 7 API, see [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7). +For what release candidate means, when the final release is expected, and how to stay on version 7, see [Release status](/orm/release-status). For the Prisma ORM 8 name of every Prisma ORM 7 API, see [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7). ::: diff --git a/apps/docs/content/docs/guides/deployment/meta.json b/apps/docs/content/docs/guides/deployment/meta.json index 7a5905c0a88..1c26adff3ed 100644 --- a/apps/docs/content/docs/guides/deployment/meta.json +++ b/apps/docs/content/docs/guides/deployment/meta.json @@ -1,10 +1,4 @@ { "title": "Deployment", - "pages": [ - "cloudflare-workers", - "docker", - "turborepo", - "pnpm-workspaces", - "bun-workspaces" - ] + "pages": ["cloudflare-workers", "docker", "turborepo", "pnpm-workspaces", "bun-workspaces"] } diff --git a/apps/docs/content/docs/guides/postgres/vercel.mdx b/apps/docs/content/docs/guides/postgres/vercel.mdx index 357510290ff..c696af39173 100644 --- a/apps/docs/content/docs/guides/postgres/vercel.mdx +++ b/apps/docs/content/docs/guides/postgres/vercel.mdx @@ -81,7 +81,7 @@ Prisma ORM has no build-time generate step. `npx prisma@latest contract emit` wr :::note[Using Prisma ORM 7?] -For a Prisma ORM 7 project, keep the `prisma.config.ts` that reads `DATABASE_URL` and add `"postinstall": "prisma generate"` to `package.json` so Prisma Client is generated on every deploy. See the [Prisma ORM 7 Next.js guide](/guides/v7/frameworks/nextjs), and [Release status](/prisma-orm/release-status) for where Prisma ORM 8 stands and how to pin Prisma ORM 7. +For a Prisma ORM 7 project, keep the `prisma.config.ts` that reads `DATABASE_URL` and add `"postinstall": "prisma generate"` to `package.json` so Prisma Client is generated on every deploy. See the [Prisma ORM 7 Next.js guide](/guides/v7/frameworks/nextjs), and [Release status](/orm/release-status) for where Prisma ORM 8 stands and how to pin Prisma ORM 7. ::: diff --git a/apps/docs/content/docs/guides/v7/integrations/meta.json b/apps/docs/content/docs/guides/v7/integrations/meta.json index 625f5b50667..9f95f1a5834 100644 --- a/apps/docs/content/docs/guides/v7/integrations/meta.json +++ b/apps/docs/content/docs/guides/v7/integrations/meta.json @@ -1,9 +1,4 @@ { "title": "Integrations", - "pages": [ - "ai-sdk", - "github-actions", - "neon-accelerate", - "supabase-accelerate" - ] + "pages": ["ai-sdk", "github-actions", "neon-accelerate", "supabase-accelerate"] } diff --git a/apps/docs/content/docs/orm/coming-from-prisma-orm-7.mdx b/apps/docs/content/docs/orm/coming-from-prisma-orm-7.mdx index b725d890162..a589776e292 100644 --- a/apps/docs/content/docs/orm/coming-from-prisma-orm-7.mdx +++ b/apps/docs/content/docs/orm/coming-from-prisma-orm-7.mdx @@ -8,7 +8,7 @@ metaDescription: 'A lookup table from Prisma ORM 7 to Prisma ORM 8: schema attri If you know Prisma ORM 7, this page tells you what each thing you already use is called in Prisma ORM 8. Two renamed things to know before the tables: the schema file is now called the contract (the same file, renamed), and generating the client types is now called `emit`. This page is a lookup table, not a tutorial: one table each for the schema, the command-line tool, and the query API, and a final section on what is missing. -To move a running application, follow [Migrate from Prisma ORM 7 to 8](/guides/upgrade-prisma-orm/postgresql) instead. It runs both versions side by side against the same database and moves one part of the app at a time. To decide whether to move at all, see [Release status](/prisma-orm/release-status). +To move a running application, follow [Migrate from Prisma ORM 7 to 8](/guides/upgrade-prisma-orm/postgresql) instead. It runs both versions side by side against the same database and moves one part of the app at a time. To decide whether to move at all, see [Release status](/orm/release-status). All examples use PostgreSQL. @@ -333,4 +333,4 @@ Also not available today: - [Core concepts](/orm/core-concepts), for what a contract is and what `contract emit` does. - [ORM client reference](/orm/reference/orm-client), for every query method. - [Reading data](/orm/fundamentals/reading-data) and [Writing data](/orm/fundamentals/writing-data), for the query API in full. -- [Release status](/prisma-orm/release-status), for how close Prisma ORM 8 is to its final release, and how to stay on Prisma ORM 7. +- [Release status](/orm/release-status), for how close Prisma ORM 8 is to its final release, and how to stay on Prisma ORM 7. diff --git a/apps/docs/content/docs/orm/data-modeling/index.mdx b/apps/docs/content/docs/orm/data-modeling/index.mdx index bcd4dd63971..06dc7df41bf 100644 --- a/apps/docs/content/docs/orm/data-modeling/index.mdx +++ b/apps/docs/content/docs/orm/data-modeling/index.mdx @@ -14,7 +14,7 @@ In Prisma ORM, you define this structure in your contract, the `contract.prisma` There is no `datasource` or `generator` block any more, and the connection string is in `prisma.config.ts` instead. [A complete contract](/orm/contract-authoring/psl-syntax#a-complete-contract) shows a whole file, and that page shows the config file too. If you are [coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7), that page covers the rename and the rest of the differences. -Prisma ORM 8 supports PostgreSQL and MongoDB today, and [Supported databases](/prisma-orm/supported-databases) lists the ones coming. A contract is built from four building blocks: +Prisma ORM 8 supports PostgreSQL and MongoDB today, and [Supported databases](/orm/supported-databases) lists the ones coming. A contract is built from four building blocks: - [Models](#models): the things your application works with - [Primary keys](#primary-keys): how each record is identified diff --git a/apps/docs/content/docs/orm/index.mdx b/apps/docs/content/docs/orm/index.mdx index 87a0ac0c5b7..ae850839c38 100644 --- a/apps/docs/content/docs/orm/index.mdx +++ b/apps/docs/content/docs/orm/index.mdx @@ -12,7 +12,7 @@ Prisma ORM 8 rewrites Prisma ORM in TypeScript. The schema-first workflow and mo Prisma ORM 8 is the current release, as a release candidate. Prisma ORM 7 remains fully supported; its docs live at [/orm/v7](/orm/v7) and its setup paths at [/v7/getting-started](/v7/getting-started). -For what release candidate means, when the final release is expected, and how to stay on version 7, see [Release status](/prisma-orm/release-status). For the Prisma ORM 8 name of every Prisma ORM 7 API, see [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7). +For what release candidate means, when the final release is expected, and how to stay on version 7, see [Release status](/orm/release-status). For the Prisma ORM 8 name of every Prisma ORM 7 API, see [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7). ::: diff --git a/apps/docs/content/docs/orm/meta.json b/apps/docs/content/docs/orm/meta.json index c9b6b822fca..2edb0d9ac77 100644 --- a/apps/docs/content/docs/orm/meta.json +++ b/apps/docs/content/docs/orm/meta.json @@ -7,8 +7,8 @@ "---Introduction---", "index", "core-concepts", - "[Release status](/prisma-orm/release-status)", - "[Supported databases](/prisma-orm/supported-databases)", + "release-status", + "supported-databases", "coming-from-prisma-orm-7", "[Prisma ORM 7 to 8 (PostgreSQL)](/guides/upgrade-prisma-orm/postgresql)", "[Prisma ORM 6 to 8 (MongoDB)](/guides/upgrade-prisma-orm/mongodb)", diff --git a/apps/docs/content/docs/(index)/prisma-orm/release-status.mdx b/apps/docs/content/docs/orm/release-status.mdx similarity index 97% rename from apps/docs/content/docs/(index)/prisma-orm/release-status.mdx rename to apps/docs/content/docs/orm/release-status.mdx index 3999ba99c94..2d7ad032289 100644 --- a/apps/docs/content/docs/(index)/prisma-orm/release-status.mdx +++ b/apps/docs/content/docs/orm/release-status.mdx @@ -1,7 +1,7 @@ --- title: Release status description: 'Where Prisma ORM 8 stands today, what you get when you install it, and how to stay on Prisma ORM 7.' -url: /prisma-orm/release-status +url: /orm/release-status metaTitle: Prisma ORM release status metaDescription: 'Prisma ORM 8 is a release candidate, with general availability expected in October 2026. What npm installs today, how long Prisma ORM 7 is supported and Prisma ORM 6 receives security patches, and how to stay on version 7.' --- @@ -33,7 +33,7 @@ npm install --save-dev prisma npm install @prisma/orm-postgres ``` -For MongoDB the library is `@prisma/orm-mongo`. [Supported databases](/prisma-orm/supported-databases) lists every database and its status. +For MongoDB the library is `@prisma/orm-mongo`. [Supported databases](/orm/supported-databases) lists every database and its status. The two packages have different version numbers, and that is normal: they are released separately, and the command-line tool gets more frequent releases. Install both at `latest`; any two `latest` versions work together, and you do not pair the numbers yourself. diff --git a/apps/docs/content/docs/(index)/prisma-orm/supported-databases.mdx b/apps/docs/content/docs/orm/supported-databases.mdx similarity index 85% rename from apps/docs/content/docs/(index)/prisma-orm/supported-databases.mdx rename to apps/docs/content/docs/orm/supported-databases.mdx index 0b304593f9e..c15df0053d0 100644 --- a/apps/docs/content/docs/(index)/prisma-orm/supported-databases.mdx +++ b/apps/docs/content/docs/orm/supported-databases.mdx @@ -1,7 +1,7 @@ --- title: Supported databases description: 'Which databases Prisma ORM 8 supports today, what release candidate and early access mean, and which databases are coming.' -url: /prisma-orm/supported-databases +url: /orm/supported-databases metaTitle: Databases supported by Prisma ORM 8 metaDescription: 'PostgreSQL is a release candidate and MongoDB is in early access. MySQL, MariaDB, SQLite, SQL Server, and CockroachDB are coming. What each label means and how to ask for a database.' --- @@ -20,11 +20,11 @@ Prisma ORM 8 ships one library per database. Today there are two you can build o ## What the labels mean -**Release candidate.** Install it and build with it today. Until the final release, some details of the API may still change, so code you write now may need small edits later. Each release candidate fixes bugs found in the last one. [Release status](/prisma-orm/release-status) has the dates and what `npm install` gives you. +**Release candidate.** Install it and build with it today. Until the final release, some details of the API may still change, so code you write now may need small edits later. Each release candidate fixes bugs found in the last one. [Release status](/orm/release-status) has the dates and what `npm install` gives you. **Early access.** The library works and you can build with it, but some of what the PostgreSQL library does is not there yet. The reference pages say on each method whether it works on MongoDB, and [what is not available yet](/orm/coming-from-prisma-orm-7#not-available-yet) lists the gaps. Report what you hit on [GitHub](https://github.com/prisma/orm/issues). -**Coming soon.** There is no Prisma ORM 8 library for the database yet. Prisma ORM 7 supports it, and Prisma ORM 7 keeps receiving bug fixes and security updates for 18 months after Prisma ORM 8 reaches general availability. [How to stay on Prisma ORM 7](/prisma-orm/release-status#how-to-stay-on-prisma-orm-7) shows how to lock the version. +**Coming soon.** There is no Prisma ORM 8 library for the database yet. Prisma ORM 7 supports it, and Prisma ORM 7 keeps receiving bug fixes and security updates for 18 months after Prisma ORM 8 reaches general availability. [How to stay on Prisma ORM 7](/orm/release-status#how-to-stay-on-prisma-orm-7) shows how to lock the version. ## How to ask for a database @@ -33,4 +33,4 @@ Add a 👍 to the GitHub issue for the database you need, or open one if there i ## Next steps - The quickstart sets up a project on [PostgreSQL](/prisma-orm/quickstart/postgresql) or [MongoDB](/prisma-orm/quickstart/mongodb). -- [Release status](/prisma-orm/release-status) covers versions, dates, and staying on Prisma ORM 7. +- [Release status](/orm/release-status) covers versions, dates, and staying on Prisma ORM 7. diff --git a/apps/docs/next.config.mjs b/apps/docs/next.config.mjs index c31d4d18897..47ed7d4d2e7 100644 --- a/apps/docs/next.config.mjs +++ b/apps/docs/next.config.mjs @@ -414,6 +414,12 @@ const config = { { source: "/guides/next", destination: "/guides", permanent: true }, { source: "/guides/next/:path*", destination: "/guides/:path*", permanent: true }, { source: "/llms/next.txt", destination: "/llms/orm.txt", permanent: true }, + { source: "/prisma-orm/release-status", destination: "/orm/release-status", permanent: true }, + { + source: "/prisma-orm/supported-databases", + destination: "/orm/supported-databases", + permanent: true, + }, // The CLI engine composes structured-error docsUrls as // / (path form); the pages anchor codes as #. { From 6b9376fdb57ce173977aada60e90039f2224a669 Mon Sep 17 00:00:00 2001 From: willbot Date: Tue, 15 Sep 2026 07:42:33 +0200 Subject: [PATCH 12/15] docs(orm): one Supported databases page Merges the reference page's feature table, hosted-provider table, and community-package note into /orm/supported-databases, which keeps the status labels. /orm/reference/supported-databases redirects there. Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- .../content/docs/orm/extensions/index.mdx | 2 +- .../docs/content/docs/orm/reference/meta.json | 1 - .../orm/reference/supported-databases.mdx | 63 ----------------- .../content/docs/orm/supported-databases.mdx | 68 +++++++++++++++---- apps/docs/next.config.mjs | 8 ++- apps/docs/scripts/audit-redirects.mjs | 6 +- apps/docs/src/lib/version-metadata.test.ts | 2 +- 7 files changed, 62 insertions(+), 88 deletions(-) delete mode 100644 apps/docs/content/docs/orm/reference/supported-databases.mdx diff --git a/apps/docs/content/docs/orm/extensions/index.mdx b/apps/docs/content/docs/orm/extensions/index.mdx index 85f8eead23f..fc7add35ecb 100644 --- a/apps/docs/content/docs/orm/extensions/index.mdx +++ b/apps/docs/content/docs/orm/extensions/index.mdx @@ -42,4 +42,4 @@ When the package is on npm, [submit it to the directory](https://www.prisma.io/e ## See also - [Using extensions](/orm/extensions/using-extensions): install an extension and run your first query with it -- [Supported databases](/orm/reference/supported-databases): which databases Prisma ORM ships and what is planned +- [Supported databases](/orm/supported-databases): which databases Prisma ORM ships and what is planned diff --git a/apps/docs/content/docs/orm/reference/meta.json b/apps/docs/content/docs/orm/reference/meta.json index 7ff96f8cc77..6f0774462df 100644 --- a/apps/docs/content/docs/orm/reference/meta.json +++ b/apps/docs/content/docs/orm/reference/meta.json @@ -7,7 +7,6 @@ "pipeline-builder", "raw-queries", "transactions-and-runtime", - "supported-databases", "error-reference" ] } diff --git a/apps/docs/content/docs/orm/reference/supported-databases.mdx b/apps/docs/content/docs/orm/reference/supported-databases.mdx deleted file mode 100644 index 6cbbd7f50a6..00000000000 --- a/apps/docs/content/docs/orm/reference/supported-databases.mdx +++ /dev/null @@ -1,63 +0,0 @@ ---- -title: Supported databases -description: 'Prisma ORM supports PostgreSQL and MongoDB. This page lists what each database gets, which managed providers work, and what is planned next.' -url: /orm/reference/supported-databases -metaTitle: Databases supported by Prisma ORM -metaDescription: 'The databases Prisma ORM supports today (PostgreSQL and MongoDB), which query surfaces, extensions, and middleware each one gets, the managed providers that work, and the databases planned next.' ---- - -Prisma ORM ships first-class support for **PostgreSQL** and **MongoDB**. PostgreSQL is the primary target and the one every extension targets today. **SQLite** ships as an experimental package, and **MySQL** is planned next. - -Each database is its own package, and that package is an extension like any other: it plugs a database family and target into the Prisma ORM core. Install the one that matches your database and it brings the config, runtime, contract authoring, and migration tooling with it: - -| Database | Package | Install | -| ---------- | -------------------- | ---------------------------------- | -| PostgreSQL | `@prisma/orm-postgres` | `npm install @prisma/orm-postgres` | -| MongoDB | `@prisma/orm-mongo` | `npm install @prisma/orm-mongo` | -| SQLite (experimental) | `@prisma/orm-sqlite` | `npm install @prisma/orm-sqlite` | - -`npx create-prisma@latest` asks which database you use and installs the right package for you. See the [PostgreSQL quickstart](/prisma-orm/quickstart/postgresql) or the [MongoDB quickstart](/prisma-orm/quickstart/mongodb). - -## What each database supports - -The ORM client works the same on both databases. The lower-level surfaces follow the database's own query model: - -| Capability | PostgreSQL | MongoDB | -| --------------------------------------------------------------------- | ---------- | ------- | -| [ORM client](/orm/reference/orm-client) | Yes | Yes | -| [SQL query builder](/orm/reference/sql-query-builder) | Yes | No | -| [Pipeline builder](/orm/reference/pipeline-builder) | No | Yes | -| [Raw queries](/orm/reference/raw-queries) | Raw SQL | Raw commands | -| [Transactions](/orm/fundamentals/transactions) | Yes | Not yet | -| [Migrations](/orm/migrations/how-migrations-work) | Yes | Yes | -| [Extensions](/orm/extensions) (pgvector, PostGIS, ParadeDB, and more) | Yes | Not yet | -| [Middleware](/orm/middleware/how-middleware-works) | Yes | Yes, except `beforeCompile` | - -`db.transaction()` exists on the PostgreSQL client only. Prisma ORM does not support MongoDB transactions yet; for multi-document writes, [use the MongoDB driver's session API](/orm/fundamentals/transactions#transactions-on-mongodb) alongside the client. - -Middleware hooks are the same on both databases apart from `beforeCompile`, which rewrites the SQL AST and so exists only on PostgreSQL. The built-in `lints` and `budgets` middleware inspect SQL and run on PostgreSQL; the `cache` middleware runs on both. See [which databases a middleware runs on](/orm/middleware/how-middleware-works#which-databases-a-middleware-runs-on). - -Every extension in the [directory](https://www.prisma.io/extensions) lists the databases it targets. Today every extension that adds column types, operators, or index types targets PostgreSQL; the database packages and the `cache` middleware are the entries that list MongoDB. - -## Managed and hosted databases - -Prisma ORM connects with a standard connection string, so any host that gives you one works. - -| Provider | Database | Notes | -| ------------------------------------------ | ---------- | ------------------------------------------------------------------------------------------------- | -| [Prisma Postgres](/postgres) | PostgreSQL | The default in `create-prisma@latest`. Provisioned from the CLI, with connection pooling built in. | -| Supabase | PostgreSQL | The [Supabase extension](https://www.prisma.io/extensions/supabase) adds the auth and storage tables and role-bound clients. | -| Neon, Railway, Render, AWS RDS, Cloud SQL, self-hosted | PostgreSQL | Any PostgreSQL server. Extensions such as pgvector and PostGIS need the server-side extension to be installable, which most managed providers include. | -| MongoDB Atlas, self-hosted MongoDB | MongoDB | Any MongoDB deployment reachable with a `mongodb://` or `mongodb+srv://` URL. | - -## Planned - -SQLite is published as an experimental package ([`@prisma/orm-sqlite`](https://www.prisma.io/extensions/sqlite)) without a quickstart yet; MySQL is planned next and is not part of the Prisma ORM release candidate. To stay on a supported path for those databases today, use [Prisma ORM 7](/orm/v7/reference/supported-databases), which supports PostgreSQL, MySQL, MariaDB, SQLite, SQL Server, CockroachDB, and MongoDB. - -Because a database package is an extension, a community package can add a database Prisma does not ship. If you publish one, [submit it to the directory](https://www.prisma.io/extensions/submit) and name the database it adds in the submission. - -## See also - -- [Prisma ORM 7 supported databases](/orm/v7/reference/supported-databases) for the full version matrix of the current generally available release -- [Prisma ORM overview](/orm) for what changed and how to get started -- [Extensions](/orm/extensions) for the capabilities you can add to PostgreSQL diff --git a/apps/docs/content/docs/orm/supported-databases.mdx b/apps/docs/content/docs/orm/supported-databases.mdx index c15df0053d0..003229ba0fd 100644 --- a/apps/docs/content/docs/orm/supported-databases.mdx +++ b/apps/docs/content/docs/orm/supported-databases.mdx @@ -1,36 +1,76 @@ --- title: Supported databases -description: 'Which databases Prisma ORM 8 supports today, what release candidate and early access mean, and which databases are coming.' +description: 'Which databases Prisma ORM 8 supports today, what each one gets, which hosted providers work, and which databases are coming.' url: /orm/supported-databases metaTitle: Databases supported by Prisma ORM 8 -metaDescription: 'PostgreSQL is a release candidate and MongoDB is in early access. MySQL, MariaDB, SQLite, SQL Server, and CockroachDB are coming. What each label means and how to ask for a database.' +metaDescription: 'PostgreSQL is a release candidate and MongoDB is in early access. What each database gets (query APIs, transactions, migrations, extensions, middleware), which managed providers work, and which databases are coming.' --- -Prisma ORM 8 ships one library per database. Today there are two you can build on. +Prisma ORM 8 ships one library per database. Install the one that matches your database and it brings the config, the client, contract authoring, and migrations with it. Today there are two you can build on. -| Database | Status | Library | -| --- | --- | --- | -| PostgreSQL, including Prisma Postgres | Release candidate | `@prisma/orm-postgres` | -| MongoDB | Early access | `@prisma/orm-mongo` | -| MySQL | Coming soon | | -| MariaDB | Coming soon | | -| SQLite | Coming soon | | -| Microsoft SQL Server | Coming soon | | -| CockroachDB | Coming soon | | +| Database | Status | Library | Install | +| --- | --- | --- | --- | +| PostgreSQL, including Prisma Postgres | Release candidate | `@prisma/orm-postgres` | `npm install @prisma/orm-postgres` | +| MongoDB | Early access | `@prisma/orm-mongo` | `npm install @prisma/orm-mongo` | +| MySQL | Coming soon | | | +| MariaDB | Coming soon | | | +| SQLite | Coming soon | | | +| Microsoft SQL Server | Coming soon | | | +| CockroachDB | Coming soon | | | + +`npm create prisma@latest -- my-app` asks which database you use and installs the right library for you. See the [PostgreSQL quickstart](/prisma-orm/quickstart/postgresql) or the [MongoDB quickstart](/prisma-orm/quickstart/mongodb). ## What the labels mean **Release candidate.** Install it and build with it today. Until the final release, some details of the API may still change, so code you write now may need small edits later. Each release candidate fixes bugs found in the last one. [Release status](/orm/release-status) has the dates and what `npm install` gives you. -**Early access.** The library works and you can build with it, but some of what the PostgreSQL library does is not there yet. The reference pages say on each method whether it works on MongoDB, and [what is not available yet](/orm/coming-from-prisma-orm-7#not-available-yet) lists the gaps. Report what you hit on [GitHub](https://github.com/prisma/orm/issues). +**Early access.** The library works and you can build with it, but some of what the PostgreSQL library does is not there yet. The table below and the reference pages say what works on MongoDB, and [what is not available yet](/orm/coming-from-prisma-orm-7#not-available-yet) lists the gaps. Report what you hit on [GitHub](https://github.com/prisma/orm/issues). **Coming soon.** There is no Prisma ORM 8 library for the database yet. Prisma ORM 7 supports it, and Prisma ORM 7 keeps receiving bug fixes and security updates for 18 months after Prisma ORM 8 reaches general availability. [How to stay on Prisma ORM 7](/orm/release-status#how-to-stay-on-prisma-orm-7) shows how to lock the version. -## How to ask for a database +## What each database supports + +The ORM client works the same on both databases. The lower-level query APIs follow the database's own query model. + +| Feature | PostgreSQL | MongoDB | +| --- | --- | --- | +| [ORM client](/orm/reference/orm-client) | Yes | Yes | +| [SQL query builder](/orm/reference/sql-query-builder) | Yes | No | +| [Pipeline builder](/orm/reference/pipeline-builder) | No | Yes | +| [Raw queries](/orm/reference/raw-queries) | Raw SQL | Raw commands | +| [Transactions](/orm/fundamentals/transactions) | Yes | Not yet | +| [Migrations](/orm/migrations/how-migrations-work) | Yes | Yes | +| [Extensions](/orm/extensions) (pgvector, PostGIS, ParadeDB, and more) | Yes | Not yet | +| [Middleware](/orm/middleware/how-middleware-works) | Yes | Yes, except `beforeCompile` | + +`db.transaction()` exists on the PostgreSQL client only. Prisma ORM does not support MongoDB transactions yet. For multi-document writes, [use the MongoDB driver's session API](/orm/fundamentals/transactions#transactions-on-mongodb) alongside the client. + +Middleware hooks are the same on both databases apart from `beforeCompile`, which rewrites the SQL query before it runs and so exists only on PostgreSQL. The built-in `lints` and `budgets` middleware inspect SQL and run on PostgreSQL. The `cache` middleware runs on both. See [which databases a middleware runs on](/orm/middleware/how-middleware-works#which-databases-a-middleware-runs-on). + +Every extension in the [directory](https://www.prisma.io/extensions) lists the databases it targets. Today every extension that adds column types, operators, or index types targets PostgreSQL. The database libraries and the `cache` middleware are the entries that list MongoDB. + +## Managed and hosted databases + +Prisma ORM connects with a standard connection string, so any host that gives you one works. + +| Provider | Database | Notes | +| --- | --- | --- | +| [Prisma Postgres](/postgres) | PostgreSQL | The default in `npm create prisma@latest`. Provisioned from the CLI, with connection pooling built in. | +| Supabase | PostgreSQL | The [Supabase extension](https://www.prisma.io/extensions/supabase) adds the auth and storage tables and role-bound clients. | +| Neon, Railway, Render, AWS RDS, Cloud SQL, self-hosted | PostgreSQL | Any PostgreSQL server. Extensions such as pgvector and PostGIS need the server-side extension to be installable, which most managed providers include. | +| MongoDB Atlas, self-hosted MongoDB | MongoDB | Any MongoDB deployment reachable with a `mongodb://` or `mongodb+srv://` URL. | + +## Databases that are coming + +MySQL, MariaDB, SQLite, Microsoft SQL Server, and CockroachDB have no Prisma ORM 8 library yet. To stay on a supported path for those databases today, use [Prisma ORM 7](/orm/v7/reference/supported-databases), which supports all of them. Add a 👍 to the GitHub issue for the database you need, or open one if there is none. The number of reactions is one of the inputs to what gets built next. Organizations that need a database sooner can talk to [Prisma enterprise support](https://www.prisma.io/enterprise). +Because a database library is an extension, a community package can add a database Prisma does not ship. If you publish one, [submit it to the directory](https://www.prisma.io/extensions/submit) and name the database it adds in the submission. + ## Next steps - The quickstart sets up a project on [PostgreSQL](/prisma-orm/quickstart/postgresql) or [MongoDB](/prisma-orm/quickstart/mongodb). - [Release status](/orm/release-status) covers versions, dates, and staying on Prisma ORM 7. +- [Prisma ORM 7 supported databases](/orm/v7/reference/supported-databases) has the full matrix for the current generally available release. +- [Extensions](/orm/extensions) lists what you can add to PostgreSQL. diff --git a/apps/docs/next.config.mjs b/apps/docs/next.config.mjs index 47ed7d4d2e7..eb806cc2cc2 100644 --- a/apps/docs/next.config.mjs +++ b/apps/docs/next.config.mjs @@ -643,9 +643,11 @@ const config = { destination: "/orm/v7/reference/database-features", permanent: false, }, - // /orm/reference/supported-databases is a live Prisma 8 page again - // (content/docs/orm/reference/supported-databases.mdx), so it is not - // redirected to the Prisma 7 tree like its siblings. + { + source: "/orm/reference/supported-databases", + destination: "/orm/supported-databases", + permanent: true, + }, { source: "/orm/reference/system-requirements", destination: "/orm/v7/reference/system-requirements", diff --git a/apps/docs/scripts/audit-redirects.mjs b/apps/docs/scripts/audit-redirects.mjs index 7ef5522ab03..b9be1466806 100644 --- a/apps/docs/scripts/audit-redirects.mjs +++ b/apps/docs/scripts/audit-redirects.mjs @@ -4,11 +4,7 @@ import { fileURLToPath } from "node:url"; const cwd = path.dirname(fileURLToPath(import.meta.url)); const docsRoot = path.resolve(cwd, ".."); -const broadDestinations = new Set([ - "/docs", - "/docs/orm", - "/docs/orm/reference/supported-databases", -]); +const broadDestinations = new Set(["/docs", "/docs/orm", "/docs/orm/supported-databases"]); const acceptableBroadRedirects = new Set([ "/docs/orm/more/upgrade-guides/upgrading-versions/codemods -> /docs/guides/upgrade-prisma-orm/v7", "/docs/orm/accelerate/getting-started/connection-pooler/client-extensions -> /docs/postgres/database/connection-pooling", diff --git a/apps/docs/src/lib/version-metadata.test.ts b/apps/docs/src/lib/version-metadata.test.ts index a5c19cbca13..65c61af399f 100644 --- a/apps/docs/src/lib/version-metadata.test.ts +++ b/apps/docs/src/lib/version-metadata.test.ts @@ -27,7 +27,7 @@ test("returns null for latest and unversioned pages", () => { for (const url of [ "/", "/orm", - "/orm/reference/supported-databases", + "/orm/supported-databases", "/cli/commands", "/guides/nextjs", "/getting-started", From 8e912331e679ff0b650ae7fae6bd308778acf735 Mon Sep 17 00:00:00 2001 From: willbot Date: Tue, 15 Sep 2026 07:50:39 +0200 Subject: [PATCH 13/15] docs(orm): SQLite is an experimental package and MySQL is planned next Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- apps/docs/content/docs/orm/supported-databases.mdx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/docs/content/docs/orm/supported-databases.mdx b/apps/docs/content/docs/orm/supported-databases.mdx index 003229ba0fd..19ca96c0878 100644 --- a/apps/docs/content/docs/orm/supported-databases.mdx +++ b/apps/docs/content/docs/orm/supported-databases.mdx @@ -3,18 +3,18 @@ title: Supported databases description: 'Which databases Prisma ORM 8 supports today, what each one gets, which hosted providers work, and which databases are coming.' url: /orm/supported-databases metaTitle: Databases supported by Prisma ORM 8 -metaDescription: 'PostgreSQL is a release candidate and MongoDB is in early access. What each database gets (query APIs, transactions, migrations, extensions, middleware), which managed providers work, and which databases are coming.' +metaDescription: 'PostgreSQL is a release candidate, MongoDB is in early access, and SQLite is an experimental package. What each database gets (query APIs, transactions, migrations, extensions, middleware), which managed providers work, and which databases are coming, with MySQL planned next.' --- -Prisma ORM 8 ships one library per database. Install the one that matches your database and it brings the config, the client, contract authoring, and migrations with it. Today there are two you can build on. +Prisma ORM 8 ships one library per database. Install the one that matches your database and it brings the config, the client, contract authoring, and migrations with it. Today there are two you can build on, and an experimental third. | Database | Status | Library | Install | | --- | --- | --- | --- | | PostgreSQL, including Prisma Postgres | Release candidate | `@prisma/orm-postgres` | `npm install @prisma/orm-postgres` | | MongoDB | Early access | `@prisma/orm-mongo` | `npm install @prisma/orm-mongo` | -| MySQL | Coming soon | | | +| MySQL | Coming soon, planned next | | | | MariaDB | Coming soon | | | -| SQLite | Coming soon | | | +| SQLite | Experimental | `@prisma/orm-sqlite` | `npm install @prisma/orm-sqlite` | | Microsoft SQL Server | Coming soon | | | | CockroachDB | Coming soon | | | @@ -26,6 +26,8 @@ Prisma ORM 8 ships one library per database. Install the one that matches your d **Early access.** The library works and you can build with it, but some of what the PostgreSQL library does is not there yet. The table below and the reference pages say what works on MongoDB, and [what is not available yet](/orm/coming-from-prisma-orm-7#not-available-yet) lists the gaps. Report what you hit on [GitHub](https://github.com/prisma/orm/issues). +**Experimental.** SQLite is published as an experimental package, [`@prisma/orm-sqlite`](https://www.prisma.io/extensions/sqlite), without a quickstart yet. `db.transaction(...)` works the same way as on PostgreSQL. + **Coming soon.** There is no Prisma ORM 8 library for the database yet. Prisma ORM 7 supports it, and Prisma ORM 7 keeps receiving bug fixes and security updates for 18 months after Prisma ORM 8 reaches general availability. [How to stay on Prisma ORM 7](/orm/release-status#how-to-stay-on-prisma-orm-7) shows how to lock the version. ## What each database supports @@ -62,7 +64,7 @@ Prisma ORM connects with a standard connection string, so any host that gives yo ## Databases that are coming -MySQL, MariaDB, SQLite, Microsoft SQL Server, and CockroachDB have no Prisma ORM 8 library yet. To stay on a supported path for those databases today, use [Prisma ORM 7](/orm/v7/reference/supported-databases), which supports all of them. +MySQL is planned next and is not part of the Prisma ORM release candidate. MariaDB, Microsoft SQL Server, and CockroachDB have no Prisma ORM 8 library yet either. To stay on a supported path for those databases today, use [Prisma ORM 7](/orm/v7/reference/supported-databases), which supports all of them. Add a 👍 to the GitHub issue for the database you need, or open one if there is none. The number of reactions is one of the inputs to what gets built next. Organizations that need a database sooner can talk to [Prisma enterprise support](https://www.prisma.io/enterprise). From ec5c89e0b7b555d8bc7a95e247c870d40d811cc8 Mon Sep 17 00:00:00 2001 From: Ankur Datta <64993082+ankur-arch@users.noreply.github.com> Date: Tue, 15 Sep 2026 10:39:44 +0200 Subject: [PATCH 14/15] docs(orm): rejoin clipped sentences on the data-modeling pages Earlier reader rounds split cause-and-effect and contrast pairs into one-clause sentences, which reads as staccato. Rejoin them with because, so, but, and colons where the facts belong together. No claims change. Also add a staccato rule to the docs-writer, content-write-blog, and docs-reader-review skills so the next plain-language pass does not reintroduce it. Co-Authored-By: Claude Fable 5.1 --- .claude/skills/content-write-blog/SKILL.md | 5 ++- .claude/skills/docs-reader-review/SKILL.md | 2 +- .claude/skills/docs-writer/SKILL.md | 4 +- .../content/docs/orm/data-modeling/index.mdx | 42 +++++++++---------- .../docs/orm/data-modeling/mongodb.mdx | 32 +++++++------- .../data-modeling/relational-databases.mdx | 34 +++++++-------- .../content/docs/orm/supported-databases.mdx | 8 ++-- 7 files changed, 66 insertions(+), 61 deletions(-) diff --git a/.claude/skills/content-write-blog/SKILL.md b/.claude/skills/content-write-blog/SKILL.md index c4ce1ce0b5b..0aef1ec1462 100644 --- a/.claude/skills/content-write-blog/SKILL.md +++ b/.claude/skills/content-write-blog/SKILL.md @@ -2,7 +2,7 @@ name: content-write-blog description: Use when the operator wants to write a blog post, draft a blog article, start a new post for the Prisma blog, or publish to prisma.io/blog. metadata: - version: "2026.7.9" + version: "2026.9.15" --- # Write Blog Post @@ -180,7 +180,8 @@ The lead you draft, and the prose the author later writes over your stubs, ship - **Be specific.** Replace "this changes everything" or "the implications are significant" with the concrete change. Drop "every", "always", and "never" where a real number fits better. - **Cut filler.** No "really", "simply", "just", and no marketing verbs like "leverage", "unlock", "seamless", "powerful". - **Say it straight.** "Not X, but Y" and "X isn't the problem, Y is" telegraph the reversal. State Y. -- **Vary sentence length**, and use a comma, colon, or period in place of em dashes. +- **Vary sentence length.** A run of short, clipped sentences with no connectives reads as staccato ("The fix shipped. Latency dropped. Users noticed."), and it is the most common tell of a model-drafted post. When facts are cause and effect or contrast, join them with "because", "so", or "but", and let a longer sentence carry them. Then let a short one land the point. +- **Use a comma, colon, or period in place of em dashes.** ## Anti-patterns diff --git a/.claude/skills/docs-reader-review/SKILL.md b/.claude/skills/docs-reader-review/SKILL.md index d0194537f67..cd75cd8a447 100644 --- a/.claude/skills/docs-reader-review/SKILL.md +++ b/.claude/skills/docs-reader-review/SKILL.md @@ -42,7 +42,7 @@ For each reported sentence: | The reviewer said | Do this | | --- | --- | | a word it had to guess | replace with the plain words from `references/banned-terms.md`, or define it in that sentence | -| too many ideas in one sentence | one idea per sentence; a table cell with three ideas becomes a note under the table | +| too many ideas in one sentence | one idea per sentence, keeping the connective (because, so, but) that ties related facts together; a row of clipped one-clause sentences is its own failure, not a fix. A table cell with three ideas becomes a note under the table | | "so what do I type?" | add the command or the code, or the link to the page that has it | | mechanism instead of action | delete the mechanism, keep what the reader does and what they see | | a reference it could not resolve ("the plan", "the ref", "spec") | show the thing, or name where it comes from | diff --git a/.claude/skills/docs-writer/SKILL.md b/.claude/skills/docs-writer/SKILL.md index 4736cd0c023..3d12cd331a4 100644 --- a/.claude/skills/docs-writer/SKILL.md +++ b/.claude/skills/docs-writer/SKILL.md @@ -3,7 +3,7 @@ name: docs-writer description: Use when writing, rewriting, or improving technical docs (quickstarts, how-tos, tutorials, concept pages, or API references). metadata: author: Prisma - version: "2026.9.11" + version: "2026.9.15" --- # Docs Writer @@ -163,6 +163,7 @@ Model-drafted docs share habits a reviewer can spot in one pass. None of these i - **Contrast slogans**: "X, not Y" constructions ("injected, not discovered", "built for agents, not just terminals", "refuses to guess"). One per page at most. A page of balanced contrasts reads like ad copy; state the behavior plainly instead. - **Absolute stacking**: "never", "every", "nothing", "always", "cannot drift" piling up across a page. Each absolute is a promise the product has to keep. Keep the ones you can verify and that the reader needs; describe what happens rather than asserting what can't. - **Exception-packed sentences**: one sentence carrying the normal case, an exception, its reason, the alternative, and a safety condition, held together by semicolons. One idea per sentence: state the default first ("By default, the command uses…"), then the exception as its own sentence ("In CI or other headless environments, set…"). A semicolon joining distinct ideas is usually two sentences. +- **Staccato**: the opposite failure, and the usual result of over-correcting the one above. A run of short, clipped sentences, each holding one fact, with no connective between them: "The field takes no `@default`. Omit `id` when you create a document and MongoDB assigns it." or "A profile needs a user. A user does not need a profile. So `Profile` holds `userId`." Every sentence lands with the same thud and the reader has to work out how the facts relate. One idea per sentence does not mean one clause per sentence. When two facts are cause and effect, contrast, or condition and result, join them with "because", "so", "but", "while", or a colon, and let one sentence carry both: "A profile needs a user, but a user does not need a profile, so `Profile` holds `userId`." Vary sentence length within a paragraph, and read it aloud before finishing: if three sentences in a row are under ten words, at least two of them probably belong together. - **Manager-voice openers**: "`service` manages services…", "handles", "manages everything around". Lead with the user action or outcome ("Use `service` commands to manage…") and use concrete verbs: creates, stores, uses, selects, targets, deploys, builds. - **Coined shorthand**: compressed phrases invented mid-page ("local pin", "pick a target") instead of saying what actually happens. Spell out the relationship, and keep one consistent user-facing vocabulary: service, project, configuration, deployment, credentials. - **Implementation language**: internal detail that doesn't help the reader act ("the root node is ``'s default export"). Translate it into the behavior they observe ("the application exported as the default export from ``"). @@ -192,5 +193,6 @@ Before you finish, check: - [ ] Is there a way to verify success at the end? - [ ] Did you cut every phrase from "Cut the slop"? - [ ] Did you check the page against every pattern in "Don't write like a model"? +- [ ] Did you read the prose aloud and rejoin any run of clipped sentences that belong together? - [ ] Are product names and limitations accurate? - [ ] Has the page been through `docs-reader-review`? A fact review checks truth; the reader review checks whether a reader without your context can follow it. Nothing is finished until that pass reports no sentence the reader could not restate. diff --git a/apps/docs/content/docs/orm/data-modeling/index.mdx b/apps/docs/content/docs/orm/data-modeling/index.mdx index 06dc7df41bf..0c61b72d6f0 100644 --- a/apps/docs/content/docs/orm/data-modeling/index.mdx +++ b/apps/docs/content/docs/orm/data-modeling/index.mdx @@ -10,11 +10,11 @@ Data modeling is the process of describing the data your application needs and h For example, a blog has users, posts, and comments. A user has fields like an email and a name. A post has fields like a title and content. These models also relate to each other: a user can write many posts, and a post can have many comments. -In Prisma ORM, you define this structure in your contract, the `contract.prisma` file that replaced `schema.prisma`. It holds your models, as `schema.prisma` did. Prisma ORM derives your TypeScript types and your migrations from it. +In Prisma ORM, you define this structure in your contract, the `contract.prisma` file that replaced `schema.prisma`. It holds your models the way `schema.prisma` did, and Prisma ORM derives both your TypeScript types and your migrations from it. -There is no `datasource` or `generator` block any more, and the connection string is in `prisma.config.ts` instead. [A complete contract](/orm/contract-authoring/psl-syntax#a-complete-contract) shows a whole file, and that page shows the config file too. If you are [coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7), that page covers the rename and the rest of the differences. +There is no `datasource` or `generator` block any more, because the connection string has moved to `prisma.config.ts`. [A complete contract](/orm/contract-authoring/psl-syntax#a-complete-contract) shows a whole file together with the config file, and if you are [coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7), that page covers the rename and the rest of the differences. -Prisma ORM 8 supports PostgreSQL and MongoDB today, and [Supported databases](/orm/supported-databases) lists the ones coming. A contract is built from four building blocks: +Prisma ORM 8 supports PostgreSQL and MongoDB today, and [Supported databases](/orm/supported-databases) lists the ones that are coming. Whichever you use, a contract is built from the same four building blocks: - [Models](#models): the things your application works with - [Primary keys](#primary-keys): how each record is identified @@ -56,13 +56,13 @@ model User { } ``` -On MongoDB, the primary key is the document's mandatory `_id` field, and it must be an `ObjectId`. `@map("_id")` gives the field that name in the database. The field takes no `@default`. Omit `id` when you create a document and MongoDB assigns it. `@@map("users")` names the collection and is optional: without it, the collection is the model name with a lowercase first letter. +On MongoDB, the primary key is the document's mandatory `_id` field, and it must be an `ObjectId`. `@map("_id")` gives the field that name in the database. The field takes no `@default`, because MongoDB assigns the id itself when you create a document without one. `@@map("users")` names the collection and is optional: without it, the collection is the model name with a lowercase first letter. ### Natural keys -A natural key is a value that already identifies the record in the real world. Use one when the value is stable, unique, and assigned outside your application: a standardized code you receive rather than invent. +A natural key is a value that already identifies the record in the real world. Use one when the value is stable, unique, and assigned outside your application, such as a standardized code you receive rather than invent. -Some tables hold a fixed set of values that other records point at. Natural keys fit these reference tables best. You can identify a country by its ISO code, and the code never changes: +Natural keys fit reference tables best, the tables that hold a fixed set of values other records point at. A country, for example, is identified by its ISO code, and that code never changes: ```prisma model Country { @@ -75,11 +75,9 @@ Records that point at `Country` now store a readable value (`US`, `DE`) instead ### Surrogate keys -A surrogate key is a generated value with no business meaning: an auto-incrementing integer, a UUID, an ObjectId. It is the better default for records your application creates. +A surrogate key is a generated value with no business meaning: an auto-incrementing integer, a UUID, an ObjectId. It is the better default for records your application creates, because it is stable. Values you might be tempted to use as a key, like an email address or a product SKU, change in practice, and changing a primary key is expensive because every record that points at the old value must be updated too. A surrogate key never changes. -The reason is stability. Values you might be tempted to use as a key, like an email address or a product SKU, change in practice. Changing a primary key is expensive because every record that points at the old value must be updated too. A surrogate key never changes. - -Keep the natural value as a regular field and enforce its uniqueness with `@unique`. You get a stable key and the uniqueness guarantee: +Keep the natural value as a regular field and enforce its uniqueness with `@unique`, so you get a stable key and the uniqueness guarantee at the same time: ```prisma model User { @@ -110,7 +108,7 @@ model ApiToken { } ``` -A UUID takes more space than an integer, but independent services never produce the same one. Write `@default(uuid(7))` for ids that sort by creation time. `@default(uuid())` is random. On MongoDB the choice is made for you. The key is the `ObjectId` shown above. +A UUID takes more space than an integer, but independent services never produce the same one. `@default(uuid())` generates a random UUID, and `@default(uuid(7))` generates one that sorts by creation time. On MongoDB there is no choice to make, because the key is always the `ObjectId` shown above. ### Composite keys @@ -125,7 +123,7 @@ model UserTag { } ``` -A duplicate `(userId, tagId)` pair would be meaningless, so the pair is the key. [Many-to-many](/orm/data-modeling/relational-databases#many-to-many) writes a model like this one out with its relation fields. Avoid composite keys for ordinary models: everything that points at the model then has to store all of the key's fields. +A duplicate `(userId, tagId)` pair would be meaningless, so the pair itself is the key. [Many-to-many](/orm/data-modeling/relational-databases#many-to-many) writes a model like this one out with its relation fields. Avoid composite keys for ordinary models, though, because everything that points at the model then has to store all of the key's fields. ## Scalar fields @@ -147,9 +145,9 @@ A scalar field holds a single value. The common types: The PostgreSQL names in the table are the column types Prisma ORM creates. `ObjectId` is the only MongoDB-only type. -A `Decimal` value reaches your code as a string, so nothing is rounded through a JavaScript number. A `DateTime` value reaches your code as a `Temporal.Instant`, the standard JavaScript type for a point in time. `Temporal` is built into Node.js 26.8.2 and later, but Homebrew's build of Node.js 26 leaves it out, so check with `node -p "typeof Temporal"`. If that prints `undefined`, install the `temporal-polyfill` package and add `import "temporal-polyfill/full/global"` in your app's entry file, before any query runs. A `Bytes` value reaches your code as a `Uint8Array`. +Three types arrive in your code as something other than a plain JavaScript value. A `Decimal` reaches you as a string, so nothing is rounded through a JavaScript number, and a `Bytes` value reaches you as a `Uint8Array`. A `DateTime` reaches you as a `Temporal.Instant`, the standard JavaScript type for a point in time. `Temporal` is built into Node.js 26.8.2 and later, but Homebrew's build of Node.js 26 leaves it out, so check with `node -p "typeof Temporal"`. If that prints `undefined`, install the `temporal-polyfill` package and add `import "temporal-polyfill/full/global"` to your app's entry file, before any query runs. -`Json` and `Jsonb` are two separate types. On a `Jsonb` field, `.where()` compares the whole value. You can ask whether the column equals a given document, but not whether a key inside it matches. A `Json` field cannot be compared in `.where()` at all, so use `Jsonb` unless you specifically need PostgreSQL's plain `json` column. Reach into the document with [raw SQL](/orm/reference/raw-queries) when you need a path query. [Extensions](/orm/extensions) add more types, such as vectors or geometry. +`Json` and `Jsonb` are two separate types, and they differ in how you can filter on them. On a `Jsonb` field, `.where()` compares the whole value, so you can ask whether the column equals a given document but not whether a key inside it matches. A `Json` field cannot be compared in `.where()` at all, so use `Jsonb` unless you specifically need PostgreSQL's plain `json` column. When you need a path query into the document, reach for [raw SQL](/orm/reference/raw-queries). [Extensions](/orm/extensions) add more types, such as vectors or geometry. A trailing `?` makes a field optional: `name String?`. A trailing `[]` makes it a list: `tags String[]`. @@ -163,7 +161,7 @@ enum Priority { } ``` -`@@type` says how the values are stored, here as PostgreSQL `text`. Leave it out and Prisma ORM takes the storage from the members, text for string values and an integer for whole numbers. `pg/text@1` is the PostgreSQL type plus a version, always `@1` today. On MongoDB, write `@@type("mongo/string@1")`. [Enums](/orm/contract-authoring/psl-syntax#enums) has the rest of the syntax. +`@@type` says how the values are stored, here as PostgreSQL `text`, written as the database type plus a version that is always `@1` today. On MongoDB, write `@@type("mongo/string@1")`. You can also leave `@@type` out, in which case Prisma ORM infers the storage from the members: text for string values and an integer for whole numbers. [Enums](/orm/contract-authoring/psl-syntax#enums) has the rest of the syntax. ### How to pick a data type @@ -178,7 +176,7 @@ model Address { } ``` -Money is not a `Float`. Floating-point numbers cannot represent decimal amounts exactly, so sums drift by fractions of a cent. For ordinary money, use `Int` and store the amount in the currency's smallest unit, such as cents, because you get a plain JavaScript number. Use `Decimal` only when you need more decimal places than the currency has, such as a per-unit rate: +Money is not a `Float`, because floating-point numbers cannot represent decimal amounts exactly and sums drift by fractions of a cent. For ordinary money, use `Int` and store the amount in the currency's smallest unit, such as cents, which also gives you a plain JavaScript number to work with. Reserve `Decimal` for values that need more decimal places than the currency has, such as a per-unit rate: ```prisma model Product { @@ -187,7 +185,7 @@ model Product { } ``` -A point in time is a `DateTime`, not a `String`. A real timestamp type gives you correct comparison, sorting, and range queries: +A point in time is a `DateTime`, not a `String`, because a real timestamp type gives you correct comparison, sorting, and range queries: ```prisma model Post { @@ -196,7 +194,7 @@ model Post { } ``` -When you are unsure between two sizes, lean toward the wider one. For a counter that could grow past two billion, that means `BigInt` over `Int`. Changing `Int` to `BigInt` later needs a migration. Pick `BigInt` now and you skip it. `BigInt` does cost you something in your code: the value reaches you as a JavaScript `bigint`, which `JSON.stringify` refuses to serialize. Call `.toString()` on it first. +When you are unsure between two sizes, lean toward the wider one. For a counter that could grow past two billion, that means `BigInt` over `Int`, since changing `Int` to `BigInt` later needs a migration that picking `BigInt` now lets you skip. `BigInt` does cost you something in your code, though: the value reaches you as a JavaScript `bigint`, which `JSON.stringify` refuses to serialize until you call `.toString()` on it. Mark a field optional (`?`) only when "absent" means something different from a sensible default, such as `publishedAt DateTime?` where no value means unpublished. A required field with a default is often the clearer model. @@ -204,10 +202,10 @@ Mark a field optional (`?`) only when "absent" means something different from a A relation connects two models: a user has many posts, a post belongs to one user. -A relation uses two kinds of field. The model that holds the link needs both. The model on the other end needs only a relation field: +A relation uses two kinds of field. The model that holds the link needs both of them, while the model on the other end needs only a relation field: - The link field stores the other record's primary key, like `authorId`. Prisma ORM 7 called this the relation scalar field. On a relational database it is the foreign key column, and on MongoDB it is the field holding the other document's `_id`. -- The relation field is typed as the other model, like `author User`. You do not get a column for it. It tells Prisma ORM how to follow the connection in a query. +- The relation field is typed as the other model, like `author User`. It creates no column of its own and only tells Prisma ORM how to follow the connection in a query. The `@relation` attribute ties them together: `fields` names the link field, and `references` names the field it points at on the other model. On the other model, `posts Post[]` is the other end of the same relation. @@ -228,7 +226,7 @@ Relations come in three kinds: - One-to-one: a user has at most one profile. The model that holds the link gets `@unique` on its link field, and the field on `User` is `profile Profile?`, not a list. [One-to-one](/orm/data-modeling/relational-databases#one-to-one) has both models written out. - One-to-many: a user has many posts, as in the example above. -- Many-to-many: a post has many tags, and a tag appears on many posts. This needs a model for the join table, with one record per connected pair. Prisma ORM 7 let you write a list field on both sides with no such model. That is gone, and `npx prisma contract emit` rejects it. Write the model out as [many-to-many](/orm/data-modeling/relational-databases#many-to-many) shows. +- Many-to-many: a post has many tags, and a tag appears on many posts. This needs a model for the join table, with one record per connected pair. Prisma ORM 7 let you write a list field on both sides with no such model, but that form is gone and `npx prisma contract emit` rejects it, so write the model out as [many-to-many](/orm/data-modeling/relational-databases#many-to-many) shows. How each kind is stored, and which model should hold the link field, is where relational and document databases differ. Continue with the guide for your database: @@ -248,5 +246,5 @@ Projects created with `npm create prisma@latest` include the [Prisma ORM skills] - [Model relational data](/orm/data-modeling/relational-databases): one-to-one, one-to-many, many-to-many, and polymorphic relations on PostgreSQL. - [Model MongoDB data](/orm/data-modeling/mongodb): embed or reference, and polymorphic collections. -- [Generate a migration](/orm/migrations/generating-a-migration) to get the models you wrote into the database. Edit the contract, then run `npx prisma contract emit` to check it and regenerate your types, then `npx prisma migration plan` to write the migration files, then `npx prisma db migrate` to apply them. That page has the flags and how the next plan finds its starting point. `npx prisma contract emit` replaces `prisma generate`. +- [Generate a migration](/orm/migrations/generating-a-migration) to get the models you wrote into the database. The loop is: edit the contract, run `npx prisma contract emit` to check it and regenerate your types (it replaces `prisma generate`), run `npx prisma migration plan` to write the migration files, then run `npx prisma db migrate` to apply them. That page has the flags and explains how the next plan finds its starting point. - [Query your models](/orm/fundamentals/reading-data) once your contract is in place. diff --git a/apps/docs/content/docs/orm/data-modeling/mongodb.mdx b/apps/docs/content/docs/orm/data-modeling/mongodb.mdx index 97eacfd42dd..a144e445d69 100644 --- a/apps/docs/content/docs/orm/data-modeling/mongodb.mdx +++ b/apps/docs/content/docs/orm/data-modeling/mongodb.mdx @@ -10,7 +10,7 @@ MongoDB stores data as documents grouped into collections. A document can nest o ## Documents, collections, and the _id key -Your models go in your contract, the `contract.prisma` file that replaced `schema.prisma`. A model maps to a collection, and each record is a document. Every document has an `_id` field as its primary key. Your connection string goes in `prisma.config.ts`, passed to `defineConfig` from `@prisma/orm-mongo/config`. See [the data contract](/orm/contract-authoring/the-data-contract). That page shows the config file. There is no `datasource` block on MongoDB. +Your models go in your contract, the `contract.prisma` file that replaced `schema.prisma`. A model maps to a collection, each record is a document, and every document has an `_id` field as its primary key. There is no `datasource` block on MongoDB: your connection string goes in `prisma.config.ts` instead, passed to `defineConfig` from `@prisma/orm-mongo/config`, and [the data contract](/orm/contract-authoring/the-data-contract) page shows that config file. ```prisma model User { @@ -21,11 +21,11 @@ model User { } ``` -In the contract you call the field `id`. In TypeScript it is always `_id`, never `id`. `@map("_id")` sets the name in the database and in your code, so you write `user._id` and `where({ _id })`. `@@map("users")` names the collection. `@@map` is optional. Without it the collection takes the model name with a lowercase first letter, so `user`. `@unique` on `email` becomes a unique index when you migrate. +In the contract you call the field `id`, but in TypeScript it is always `_id`, never `id`, because `@map("_id")` sets the name in the database and in your code alike. So you write `user._id` and `where({ _id })`. `@@map("users")` names the collection and is optional: without it the collection takes the model name with a lowercase first letter, so `user`. `@unique` on `email` becomes a unique index when you migrate. -Write `ObjectId` for the key type. `ObjectId` is the contract type. In TypeScript a document's own `_id` is a plain string, so there is no `ObjectId` to construct. The old `String @db.ObjectId` and `@default(auto())` are no longer allowed. +Write `ObjectId` for the key type. That is the contract type only: in TypeScript a document's own `_id` is a plain string, so there is no `ObjectId` to construct. The old `String @db.ObjectId` and `@default(auto())` are no longer allowed. -Leave the id out of the values you pass to `create`, and MongoDB assigns it. `db` is the client created in `src/prisma/db.ts`. `db.orm` is the ORM client, one of the query APIs on `db`. See [the ORM reference](/orm/reference). +Leave the id out of the values you pass to `create`, and MongoDB assigns it. In the examples below, `db` is the client created in `src/prisma/db.ts`, and `db.orm` is the ORM client, one of the query APIs on `db` that [the ORM reference](/orm/reference) covers. ```ts import { db } from "./prisma/db"; @@ -54,7 +54,7 @@ A blog post's comments make it concrete. If you always render them with the post ## Embedded documents -Describe the shape of embedded data with a `type` block, then use it as a field. An embedded value has no `_id` and no collection of its own. It is created, loaded, updated, and deleted with the document that holds it. +Describe the shape of embedded data with a `type` block, then use it as a field. An embedded value has no `_id` and no collection of its own, so it is created, loaded, updated, and deleted with the document that holds it. ```prisma type Address { @@ -78,20 +78,22 @@ const address = { street: "1 Main St", city: "Berlin", country: "DE" }; await db.orm.users.create({ name: "Ada", tags: [], address }); ``` -In an `update`, `u` builds the changes you want. `update` takes a callback that returns an array, so several changes go in one call. Use `u.fieldName` to change a top-level field. Use `u("path.to.field")` for a field inside an embedded document, written as a string because a dotted path is not a property name. Use `.push(...)` and `.pull(...)` to add to and remove from a list of scalars: +In an `update`, the callback argument `u` builds the changes you want, and because the callback returns an array, several changes go in one call. Use `u.fieldName` to change a top-level field, or `u("path.to.field")` for a field inside an embedded document, written as a string because a dotted path is not a property name. On a list of scalars, `.push(...)` adds values and `.pull(...)` removes them: ```ts await db.orm.users.where({ _id: user._id }).update((u) => [u("address.city").set("Hamburg")]); await db.orm.users.where({ _id: user._id }).update((u) => [u.tags.push("admin"), u.tags.pull("new")]); ``` -Replace a whole embedded value with `u.address.set({ ... })`, and remove it with `u.address.unset()`. A list of embedded values is replaced as a whole with `u.addresses.set([ ... ])`. The object form of `.where()` filters on top-level model fields only. To match on a field inside an embedded document, use the pipeline builder, a chained builder for MongoDB aggregation pipelines: +Replace a whole embedded value with `u.address.set({ ... })`, remove it with `u.address.unset()`, and replace a list of embedded values as a whole with `u.addresses.set([ ... ])`. Filtering is more limited: the object form of `.where()` filters on top-level model fields only, so to match on a field inside an embedded document you use the pipeline builder, a chained builder for MongoDB aggregation pipelines: ```ts const query = db.query.from("users").match((f) => f("address.city").eq("Berlin")).build(); ``` -`build()` returns the query, not the rows. See [the pipeline builder](/orm/fundamentals/advanced-queries#mongodb-pipeline-builder) for how to run it and what comes back. Embedded arrays are only a good idea while they stay bounded. An array that grows forever slows every read of the parent and pushes the document toward MongoDB's 16 MB limit, and a write that exceeds it fails. When a list has no natural cap, reference it instead. +`build()` returns the query rather than the rows, and [the pipeline builder](/orm/fundamentals/advanced-queries#mongodb-pipeline-builder) page explains how to run it and what comes back. + +Embedded arrays are only a good idea while they stay bounded. An array that grows forever slows every read of the parent and pushes the document toward MongoDB's 16 MB limit, where a write that exceeds it fails, so when a list has no natural cap, reference it instead. ## References across collections @@ -113,18 +115,20 @@ model Post { } ``` -This is a one-to-many by reference: many posts point at one user, and each post is queryable on its own. There are no nested writes on MongoDB: create the user first, then the post with its `authorId`. In `references: [id]` you write the field's contract-side name, `id`, not `_id`. To load a post together with its author, chain `.include("author")`, which adds the related record to every row you get back: +This is a one-to-many by reference: many posts point at one user, and each post is queryable on its own. Note that in `references: [id]` you write the field's contract-side name, `id`, not `_id`. There are no nested writes on MongoDB, so create the user first and then the post with its `authorId`. To load a post together with its author, chain `.include("author")`, which adds the related record to every row you get back: ```ts await db.orm.posts.create({ title: "Hello", authorId: user._id }); const posts = await db.orm.posts.include("author").all(); ``` -`.include("posts")` on `db.orm.users` works the other way and gives each user an array of their posts. See [relations and joins](/orm/fundamentals/relations-and-joins) for the rest of the query API. Resolving a reference means a lookup into the other collection on every read. If a read is frequent, you can copy the few fields it needs into the parent document, such as a comment storing its author's name. If the original changes, the copy does not. You update it yourself. +`.include("posts")` on `db.orm.users` works the other way and gives each user an array of their posts. See [relations and joins](/orm/fundamentals/relations-and-joins) for the rest of the query API. + +Resolving a reference means a lookup into the other collection on every read. If a read is frequent, you can copy the few fields it needs into the parent document, such as a comment storing its author's name, but the copy does not follow the original when it changes, so you update it yourself. ## Polymorphic collections -One collection can hold more than one kind of document. You declare a base model for the shared fields and a variant model for each kind. A discriminator field tells the kinds apart: its value says which kind each document is. +One collection can hold more than one kind of document. You declare a base model for the shared fields and a variant model for each kind, and a discriminator field on the base model records which kind each document is. ```prisma model Post { @@ -145,9 +149,9 @@ model Tutorial { } ``` -`@@discriminator(kind)` takes the name of the field that records each document's variant, so write `kind` and not `"kind"`. `@@base(Post, "article")` declares `Article` as the `"article"` variant of `Post`. A variant declares only its own fields. `id`, `title`, and `kind` come from `Post`. A variant uses its base's collection, so it needs no `@@map` of its own. +`@@discriminator(kind)` takes the name of the field that records each document's variant, so write `kind` and not `"kind"`. `@@base(Post, "article")` declares `Article` as the `"article"` variant of `Post`. A variant declares only its own fields, since `id`, `title`, and `kind` come from `Post`, and it uses its base's collection, so it needs no `@@map` of its own. -A query for posts returns articles and tutorials together. You reach a single variant through the base model: `db.orm.posts.variant("Article")` limits the query to one variant. `.variant()` takes the variant's model name, `"Article"`, and not the discriminator value `"article"`. Creating through a variant fills in `kind` for you: +A query for posts returns articles and tutorials together. To reach a single variant, go through the base model: `db.orm.posts.variant("Article")` limits the query to one variant, and it takes the variant's model name, `"Article"`, not the discriminator value `"article"`. Creating through a variant fills in `kind` for you: ```ts const article = await db.orm.posts.variant("Article").create({ title: "Hello", summary: "A short post" }); @@ -166,7 +170,7 @@ Projects created with `npm create prisma@latest` include the [Prisma ORM skills] ## Next steps -- Start a new project with `npm create prisma@latest -- my-app`, or run `npx prisma orm init` in an existing one. After every change to your contract, run `npx prisma contract emit`, which checks your contract and regenerates your types. Then `npx prisma migration plan` writes a migration, and `npx prisma db migrate` applies it, creating the collections and indexes. [Generating a migration](/orm/migrations/generating-a-migration) has the flags and how the next plan finds its starting point. +- Start a new project with `npm create prisma@latest -- my-app`, or run `npx prisma orm init` in an existing one. After every change to your contract, run `npx prisma contract emit` to check it and regenerate your types, then `npx prisma migration plan` to write a migration and `npx prisma db migrate` to apply it, which creates the collections and indexes. [Generating a migration](/orm/migrations/generating-a-migration) has the flags and explains how the next plan finds its starting point. - [Query documents](/orm/fundamentals/reading-data) and [resolve references](/orm/fundamentals/relations-and-joins) with `.include(...)`. - Aggregate and reshape documents with the [pipeline builder](/orm/fundamentals/advanced-queries#mongodb-pipeline-builder). - To model relational data, see the [relational data modeling guide](/orm/data-modeling/relational-databases). diff --git a/apps/docs/content/docs/orm/data-modeling/relational-databases.mdx b/apps/docs/content/docs/orm/data-modeling/relational-databases.mdx index 7528bcfdeaa..c508648a70f 100644 --- a/apps/docs/content/docs/orm/data-modeling/relational-databases.mdx +++ b/apps/docs/content/docs/orm/data-modeling/relational-databases.mdx @@ -8,11 +8,11 @@ metaDescription: 'How to model one-to-one, one-to-many, many-to-many, and polymo In a relational database, each model becomes a table and each scalar field becomes a column. Models connect through foreign keys: a column in one table that holds the primary key of a row in another table. If you are new to models and keys, start with the [data modeling overview](/orm/data-modeling). -You write your models in your contract, the `contract.prisma` file that replaced `schema.prisma`. After every change to it, run `npx prisma contract emit`, which checks the contract and regenerates your TypeScript types. It does not touch the database. `db` is the client you create once in `src/prisma/db.ts`, the file `npx prisma orm init` writes. `db.orm` holds your models by model name, and `public` is the PostgreSQL schema your tables are in. +You write your models in your contract, the `contract.prisma` file that replaced `schema.prisma`. After every change to it, run `npx prisma contract emit`, which checks the contract and regenerates your TypeScript types without touching the database. The query examples below use `db`, the client you create once in `src/prisma/db.ts`, the file `npx prisma orm init` writes. On it, `db.orm` holds your models by model name, and `public` is the PostgreSQL schema your tables are in. ## How relations are declared -The model whose table gets the foreign key column declares two fields. One is a scalar field for the foreign key column. The other is a relation field, typed as the other model. `@relation(fields:, references:)` ties them together: +The model whose table gets the foreign key column declares two fields: a scalar field for the foreign key column, and a relation field typed as the other model. `@relation(fields:, references:)` ties them together: ```prisma model Post { @@ -22,11 +22,11 @@ model Post { } ``` -The side that declares the `fields` argument owns the foreign key. Deciding which side declares `fields` is the main choice in each relation kind below. +The side that declares the `fields` argument owns the foreign key, and deciding which side that should be is the main choice in each relation kind below. -To let a post have no author, make the foreign key field and the relation field both optional: `authorId Int?` and `author User?`. The two fields have to agree. If one is optional and the other is required, `npx prisma contract emit` reports an error. +To let a post have no author, make the foreign key field and the relation field both optional: `authorId Int?` and `author User?`. The two fields have to agree, so if one is optional and the other is required, `npx prisma contract emit` reports an error. -When a `User` row is deleted or its `id` changes, referential actions decide what happens to the `Post` rows that point at it. Add `onDelete` and `onUpdate` to `@relation`, as in `@relation(fields: [userId], references: [id], onDelete: Cascade)`. They become the foreign key's `ON DELETE` and `ON UPDATE` clauses. Write neither and the foreign key gets neither clause, so the database decides what happens. The five actions are: +When a `User` row is deleted or its `id` changes, referential actions decide what happens to the `Post` rows that point at it. Add `onDelete` and `onUpdate` to `@relation`, as in `@relation(fields: [userId], references: [id], onDelete: Cascade)`, and they become the foreign key's `ON DELETE` and `ON UPDATE` clauses. If you write neither, the foreign key gets neither clause and the database decides what happens. The five actions are: - `Cascade` deletes or updates the matching rows. - `Restrict` blocks the change. @@ -55,9 +55,9 @@ model Post { } ``` -The foreign key is on the "many" side, `Post.authorId`, because each post points at exactly one user, while a user points at an open-ended list of posts. Use a one-to-many whenever a record belongs to one parent and a parent has many children: comments on a post, line items on an order, employees in a department. A model can also point at itself: a category with a parent category is an ordinary one-to-many whose relation field is typed as the same model. One relation between a model and itself needs no relation name. +The foreign key is on the "many" side, `Post.authorId`, because each post points at exactly one user, while a user points at an open-ended list of posts. Use a one-to-many whenever a record belongs to one parent and a parent has many children: comments on a post, line items on an order, employees in a department. A model can also point at itself: a category with a parent category is an ordinary one-to-many whose relation field is typed as the same model, and a single self-relation like this needs no relation name. -`posts Post[]` on `User` is the mirror field, the field on the other side of the relation, which stores nothing in the database. You can leave it out. The relation still works, and you read it from the foreign key side with `db.orm.public.Post.where({ authorId: user.id }).all()`. `.all()` is the call that runs the query and returns every matching row. +`posts Post[]` on `User` is the mirror field, the field on the other side of the relation, which stores nothing in the database. You can leave it out, and the relation still works: you then read it from the foreign key side with `db.orm.public.Post.where({ authorId: user.id }).all()`, where `.all()` is the call that runs the query and returns every matching row. To connect a post to a user, set the foreign key when you create the post. Here `user` is a user record you read earlier: @@ -92,11 +92,11 @@ model Profile { } ``` -Declare the relation on the side that holds the foreign key, `Profile` here. You can also add the mirror field `profile Profile?` to `User`, with a type and nothing else, because it needs no `@relation` of its own. It has to be optional, because nothing in the database guarantees a matching profile row. Because `userId` is unique, the mirror field can be a single `Profile?` instead of a list. With it in place you can [read the relation from either side](/orm/fundamentals/relations-and-joins#one-to-one), as in `db.orm.public.User.include("profile").all()`. +Declare the relation on the side that holds the foreign key, `Profile` here. You can also add the mirror field `profile Profile?` to `User`, with a type and nothing else, because it needs no `@relation` of its own. It is a single `Profile?` rather than a list because `userId` is unique, and it has to be optional because nothing in the database guarantees a matching profile row. With it in place you can [read the relation from either side](/orm/fundamentals/relations-and-joins#one-to-one), as in `db.orm.public.User.include("profile").all()`. ### Which side owns the foreign key -Put the foreign key on the dependent side, meaning the record that cannot exist on its own. A profile needs a user. A user does not need a profile. So `Profile` holds `userId`. +Put the foreign key on the dependent side, meaning the record that cannot exist on its own. A profile needs a user, but a user does not need a profile, so `Profile` holds `userId`. The other signals all point at the same side. The dependent record is: @@ -133,7 +133,7 @@ model PostTag { } ``` -The composite primary key on `PostTag`, `@@id([postId, tagId])`, allows one record per pair. Note that `Post.tags` is typed `Tag[]`, not `PostTag[]`: you name the model at the other end. The same goes for `Tag.posts`. If a second join table connects the same two models, the relation name goes on both sides again, which here means the list field, `Post.tags`, and `PostTag.post`, the field that points back at `Post`. +The composite primary key on `PostTag`, `@@id([postId, tagId])`, allows one record per pair. Note that `Post.tags` is typed `Tag[]`, not `PostTag[]`, and the same goes for `Tag.posts`: you name the model at the other end, not the join table. If a second join table connects the same two models, the relation name goes on both sides again, which here means the list field `Post.tags` and the field that points back at `Post`, `PostTag.post`. Connect a post to a tag with a nested write on the mirror field: @@ -143,9 +143,9 @@ await db.orm.public.Post.where({ id: post.id }).update({ }); ``` -`t` is the argument of the nested write callback: `t.create([...])` makes new related records and links them, `t.connect([...])` links existing ones, and `t.disconnect([...])` removes the link. `Post.tags` and `Tag.posts` are the mirror fields that `t.connect` goes through. Leave them out and you write the `PostTag` rows yourself, with `db.orm.public.PostTag.create({ postId: post.id, tagId: tag.id })`. `PostTag` is an ordinary model, so the join table can hold data about the pairing itself. To record when a tag was added, add `addedAt DateTime @default(now())` to `PostTag`. +`t` is the argument of the nested write callback: `t.create([...])` makes new related records and links them, `t.connect([...])` links existing ones, and `t.disconnect([...])` removes the link. The nested write goes through the mirror fields, `Post.tags` and `Tag.posts`, so if you leave them out you write the `PostTag` rows yourself with `db.orm.public.PostTag.create({ postId: post.id, tagId: tag.id })`. Because `PostTag` is an ordinary model, the join table can also hold data about the pairing itself: to record when a tag was added, add `addedAt DateTime @default(now())` to `PostTag`. -An implicit many-to-many, with list fields on both sides and no join table, as in Prisma ORM 7, is not supported. Declare `PostTag` yourself. If you skip it, `npx prisma contract emit` reports an error whose `code` is `PSL_ORPHANED_BACKRELATION`. [Relations and joins](/orm/fundamentals/relations-and-joins#many-to-many) shows how to read across the join table in one query. +An implicit many-to-many, with list fields on both sides and no join table as in Prisma ORM 7, is not supported, so declare `PostTag` yourself. If you skip it, `npx prisma contract emit` reports an error whose `code` is `PSL_ORPHANED_BACKRELATION`. [Relations and joins](/orm/fundamentals/relations-and-joins#many-to-many) shows how to read across the join table in one query. ## Polymorphic relations @@ -175,9 +175,9 @@ model Feature { } ``` -`@@discriminator(type)` marks the field that records the variant. `@@base(Task, "bug")` declares that `Bug` is the `"bug"` variant of `Task`. `@@discriminator(type)` and the first argument of `@@base` take a field or model name without quotes, while the variant value is a string. A variant has every field of its base plus the ones it declares, and it needs no `@id` of its own, because it uses the base model's primary key. +`@@discriminator(type)` marks the field that records the variant, and `@@base(Task, "bug")` declares that `Bug` is the `"bug"` variant of `Task`. Both take a field or model name without quotes, while the variant value is a string. A variant has every field of its base plus the ones it declares, and it needs no `@id` of its own because it uses the base model's primary key. -Read and write variants through the base model. `.variant(...)` limits the query to one variant. Pass the model name, `"Bug"`, with a capital letter. `"bug"` is a type error. On a create, the discriminator value is filled in for you: +Read and write variants through the base model, where `.variant(...)` limits the query to one variant. It takes the model name, `"Bug"`, with a capital letter, and passing the discriminator value `"bug"` instead is a type error. On a create, the discriminator value is filled in for you: ```typescript await db.orm.public.Task.variant("Bug").create({ title: "Crash on save", severity: "high" }); @@ -194,9 +194,9 @@ switch (task.type) { } ``` -`@@map` decides how a variant is stored. A variant with no `@@map`, like `Bug`, is stored in the base model's table, and its extra columns are added there and are nullable, because a `Feature` row has no severity. A variant with its own `@@map`, like `Feature`, is split into its own table, named by the string you pass, and its columns there can be `NOT NULL`, though reading a full variant costs a join. In Prisma ORM 7, `@@map` only renamed a table. +`@@map` decides how a variant is stored, which is a new job for it: in Prisma ORM 7, `@@map` only renamed a table. A variant with no `@@map`, like `Bug`, is stored in the base model's table, and its extra columns are added there as nullable columns, because a `Feature` row has no severity. A variant with its own `@@map`, like `Feature`, is split into its own table, named by the string you pass, and its columns there can be `NOT NULL`, though reading a full variant then costs a join. -Start with the shared table. Give a variant its own table when its columns have to be `NOT NULL` in the database, or when the variant has many columns of its own. Use a base model with variants when you query the variants together (one feed of tasks, one stream of events) and each variant has real structure of its own. If you never query them together, separate models are simpler. If the variants differ by one nullable field, a single model is simpler still. +Start with the shared table, and give a variant its own table only when its columns have to be `NOT NULL` in the database or when the variant has many columns of its own. The pattern as a whole fits when you query the variants together (one feed of tasks, one stream of events) and each variant has real structure of its own. If you never query them together, separate models are simpler, and if the variants differ by one nullable field, a single model is simpler still. ## Prompt your coding agent @@ -210,5 +210,5 @@ Projects created with `npm create prisma@latest` include the [Prisma ORM skills] ## Next steps - [Query relations](/orm/fundamentals/relations-and-joins) with `.include(...)`, filters on relation data, and reads across the join table. -- To change the database to match the contract: edit `contract.prisma`, run `npx prisma contract emit`, then `npx prisma migration plan`, which writes the migration files, then `npx prisma db migrate`, which applies them. [Generating a migration](/orm/migrations/generating-a-migration) has the flags and how the next plan finds its starting point. +- To change the database to match the contract, edit `contract.prisma`, run `npx prisma contract emit`, then run `npx prisma migration plan` to write the migration files and `npx prisma db migrate` to apply them. [Generating a migration](/orm/migrations/generating-a-migration) has the flags and explains how the next plan finds its starting point. - To model for MongoDB, see the [MongoDB data modeling guide](/orm/data-modeling/mongodb). diff --git a/apps/docs/content/docs/orm/supported-databases.mdx b/apps/docs/content/docs/orm/supported-databases.mdx index 19ca96c0878..f2861c3eca4 100644 --- a/apps/docs/content/docs/orm/supported-databases.mdx +++ b/apps/docs/content/docs/orm/supported-databases.mdx @@ -6,7 +6,7 @@ metaTitle: Databases supported by Prisma ORM 8 metaDescription: 'PostgreSQL is a release candidate, MongoDB is in early access, and SQLite is an experimental package. What each database gets (query APIs, transactions, migrations, extensions, middleware), which managed providers work, and which databases are coming, with MySQL planned next.' --- -Prisma ORM 8 ships one library per database. Install the one that matches your database and it brings the config, the client, contract authoring, and migrations with it. Today there are two you can build on, and an experimental third. +Prisma ORM 8 ships one library per database, and installing the one that matches yours brings the config, the client, contract authoring, and migrations with it. Today there are two you can build on, and an experimental third. | Database | Status | Library | Install | | --- | --- | --- | --- | @@ -45,11 +45,11 @@ The ORM client works the same on both databases. The lower-level query APIs foll | [Extensions](/orm/extensions) (pgvector, PostGIS, ParadeDB, and more) | Yes | Not yet | | [Middleware](/orm/middleware/how-middleware-works) | Yes | Yes, except `beforeCompile` | -`db.transaction()` exists on the PostgreSQL client only. Prisma ORM does not support MongoDB transactions yet. For multi-document writes, [use the MongoDB driver's session API](/orm/fundamentals/transactions#transactions-on-mongodb) alongside the client. +`db.transaction()` exists on the PostgreSQL client only, because Prisma ORM does not support MongoDB transactions yet. For multi-document writes on MongoDB, [use the MongoDB driver's session API](/orm/fundamentals/transactions#transactions-on-mongodb) alongside the client. Middleware hooks are the same on both databases apart from `beforeCompile`, which rewrites the SQL query before it runs and so exists only on PostgreSQL. The built-in `lints` and `budgets` middleware inspect SQL and run on PostgreSQL. The `cache` middleware runs on both. See [which databases a middleware runs on](/orm/middleware/how-middleware-works#which-databases-a-middleware-runs-on). -Every extension in the [directory](https://www.prisma.io/extensions) lists the databases it targets. Today every extension that adds column types, operators, or index types targets PostgreSQL. The database libraries and the `cache` middleware are the entries that list MongoDB. +Every extension in the [directory](https://www.prisma.io/extensions) lists the databases it targets. Today every extension that adds column types, operators, or index types targets PostgreSQL, and the only entries that list MongoDB are the database libraries and the `cache` middleware. ## Managed and hosted databases @@ -66,7 +66,7 @@ Prisma ORM connects with a standard connection string, so any host that gives yo MySQL is planned next and is not part of the Prisma ORM release candidate. MariaDB, Microsoft SQL Server, and CockroachDB have no Prisma ORM 8 library yet either. To stay on a supported path for those databases today, use [Prisma ORM 7](/orm/v7/reference/supported-databases), which supports all of them. -Add a 👍 to the GitHub issue for the database you need, or open one if there is none. The number of reactions is one of the inputs to what gets built next. Organizations that need a database sooner can talk to [Prisma enterprise support](https://www.prisma.io/enterprise). +Add a 👍 to the GitHub issue for the database you need, or open one if there is none, because the number of reactions is one of the inputs to what gets built next. Organizations that need a database sooner can talk to [Prisma enterprise support](https://www.prisma.io/enterprise). Because a database library is an extension, a community package can add a database Prisma does not ship. If you publish one, [submit it to the directory](https://www.prisma.io/extensions/submit) and name the database it adds in the submission. From 97ca48a7ae0a1423f608bda01b554938705fddb9 Mon Sep 17 00:00:00 2001 From: Ankur Datta <64993082+ankur-arch@users.noreply.github.com> Date: Tue, 15 Sep 2026 10:49:10 +0200 Subject: [PATCH 15/15] docs(orm): staccato pass over the ORM section Rejoin runs of clipped one-clause sentences across every Prisma ORM 8 page outside v6 and v7, keeping every claim, command, and flag as it was. Add scripts/check-staccato.py to the docs-reader-review skill, with a step that runs it after the banned-term check, and point the docs-writer final pass at it. error-reference.mdx is generated and was left alone. Co-Authored-By: Claude Fable 5.1 --- .claude/skills/docs-reader-review/SKILL.md | 4 + .../scripts/check-staccato.py | 97 +++++++++++++++++++ .claude/skills/docs-writer/SKILL.md | 2 +- .../docs/orm/coming-from-prisma-orm-7.mdx | 10 +- .../orm/contract-authoring/capabilities.mdx | 14 +-- .../orm/contract-authoring/psl-syntax.mdx | 40 ++++---- .../the-contract-artifact.mdx | 16 +-- .../contract-authoring/the-data-contract.mdx | 16 +-- .../typescript-schema-builder.mdx | 46 ++++----- apps/docs/content/docs/orm/core-concepts.mdx | 4 +- .../content/docs/orm/data-modeling/index.mdx | 2 +- .../docs/orm/data-modeling/mongodb.mdx | 2 +- .../data-modeling/relational-databases.mdx | 2 +- .../docs/orm/extensions/using-extensions.mdx | 6 +- .../orm/fundamentals/advanced-queries.mdx | 36 +++---- .../docs/orm/fundamentals/reading-data.mdx | 48 ++++----- .../orm/fundamentals/relations-and-joins.mdx | 44 ++++----- .../docs/orm/fundamentals/transactions.mdx | 32 +++--- .../docs/orm/fundamentals/writing-data.mdx | 46 ++++----- apps/docs/content/docs/orm/index.mdx | 2 +- .../authoring-custom-middleware.mdx | 4 +- .../docs/orm/middleware/built-in-cache.mdx | 4 +- .../docs/orm/middleware/built-in-lints.mdx | 2 +- .../orm/middleware/how-middleware-works.mdx | 2 +- .../orm/migrations/applying-a-migration.mdx | 8 +- .../orm/migrations/editing-a-migration.mdx | 6 +- .../orm/migrations/generating-a-migration.mdx | 14 +-- .../orm/migrations/how-migrations-work.mdx | 10 +- .../orm/migrations/rollbacks-and-recovery.mdx | 6 +- .../orm/migrations/the-migration-graph.mdx | 18 ++-- .../docs/content/docs/orm/reference/index.mdx | 10 +- .../content/docs/orm/reference/orm-client.mdx | 36 +++---- .../docs/orm/reference/pipeline-builder.mdx | 16 +-- .../docs/orm/reference/raw-queries.mdx | 16 +-- .../docs/orm/reference/sql-query-builder.mdx | 28 +++--- .../reference/transactions-and-runtime.mdx | 14 +-- apps/docs/content/docs/orm/release-status.mdx | 8 +- 37 files changed, 386 insertions(+), 285 deletions(-) create mode 100755 .claude/skills/docs-reader-review/scripts/check-staccato.py diff --git a/.claude/skills/docs-reader-review/SKILL.md b/.claude/skills/docs-reader-review/SKILL.md index cd75cd8a447..f188438e23e 100644 --- a/.claude/skills/docs-reader-review/SKILL.md +++ b/.claude/skills/docs-reader-review/SKILL.md @@ -23,6 +23,10 @@ Use it after `docs-writer` (or any other writing pass) and after the fact review Run `scripts/check-plain.sh` on every changed page. It fails on the words in `references/banned-terms.md`, which lists source-code vocabulary and the plain words to use instead, and it ignores code blocks and inline code. Replace every hit before going further. Do not argue that a term is fine in context. The one exception: if the sentence itself defines the term in plain words, add `{/* plain-language:defined */}` to that line; the checker skips it and the reader review will confirm the definition landed. Terms the list marks "(unexplained)" are not checked by the script; the reviewer catches them. +### 1b. Run the staccato check + +Run `scripts/check-staccato.py` on every changed page. It flags paragraphs with three or more consecutive sentences under nine words, the usual signature of a page whose long sentences were split without keeping the connectives. Read each flagged paragraph aloud. Rejoin sentences that are cause and effect, contrast, or condition and result with "because", "so", "but", "while", or a colon, and leave apart the ones that are separate ideas. The script only finds the worst runs; a paragraph of eleven-word sentences that all land the same way is still staccato, so read the whole page for rhythm, not only the hits. Reference entries that are fragments by design (a `Payload:` line, a one-line table note) are not prose and do not count. + ### 2. Dispatch the reader Hand the page to a fresh reviewer that has no memory of writing it, using `references/reader-persona.md` verbatim as its instructions. The reviewer reads the page once, top to bottom, with nothing else open, and reports: diff --git a/.claude/skills/docs-reader-review/scripts/check-staccato.py b/.claude/skills/docs-reader-review/scripts/check-staccato.py new file mode 100755 index 00000000000..eeb2d11ec88 --- /dev/null +++ b/.claude/skills/docs-reader-review/scripts/check-staccato.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +"""Flags staccato prose: runs of short, one-clause sentences with no connective between them. + +Usage: check-staccato.py [more files...] Exit 1 on any hit. + +A hit is a paragraph containing three or more consecutive sentences that are each +under MAX_WORDS words. Fenced code, tables, headings, list items, front matter, and +import lines are skipped. Colons that introduce a code span or example are not +treated as sentence ends. The heuristic is deliberately simple: it finds the +paragraphs a human should read aloud, and the reader decides whether the sentences +belong together. Rejoin them with "because", "so", "but", "while", or a colon when +they are cause and effect, contrast, or condition and result. Leave them apart when +they are separate ideas. +""" +import re +import sys + +MAX_WORDS = 9 +RUN = 3 + +CODE = re.compile(r"`[^`]*`") +LINK = re.compile(r"\]\([^)]*\)") +SENT_END = re.compile(r"(?<=[.!?])\s+(?=[A-Z`\"'(])") + + +def paragraphs(text): + """Yield (line_number, paragraph) for prose paragraphs only.""" + lines = text.split("\n") + out, start, infence, infront = [], None, False, False + for i, line in enumerate(lines, 1): + if i == 1 and line.strip() == "---": + infront = True + continue + if infront: + if line.strip() == "---": + infront = False + continue + if re.match(r"^\s*(```|~~~)", line): + infence = not infence + continue + if infence or line.startswith((" ", "\t")): + continue + stripped = line.strip() + if not stripped: + if out: + yield start, " ".join(out) + out, start = [], None + continue + if re.match(r"^(#|\||[-*+] |\d+\. |import |export |<|{/\*)", stripped): + if out: + yield start, " ".join(out) + out, start = [], None + continue + if start is None: + start = i + out.append(stripped) + if out: + yield start, " ".join(out) + + +def sentences(para): + para = CODE.sub("CODE", para) + para = LINK.sub("]", para) + return [s.strip() for s in SENT_END.split(para) if s.strip()] + + +def check(path): + hits = [] + text = open(path, encoding="utf-8").read() + for lineno, para in paragraphs(text): + sents = sentences(para) + short = [len(s.split()) < MAX_WORDS for s in sents] + run = 0 + for j, is_short in enumerate(short): + run = run + 1 if is_short else 0 + if run == RUN: + first = j - RUN + 1 + hits.append((lineno, " | ".join(sents[first : j + 1]))) + break + return hits + + +def main(argv): + status = 0 + for path in argv: + hits = check(path) + if hits: + status = 1 + print(f"== {path}") + for lineno, snippet in hits: + print(f"{lineno}: {snippet[:180]}") + print("staccato check: clean" if status == 0 else "staccato check: clipped runs found, read them aloud") + return status + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/.claude/skills/docs-writer/SKILL.md b/.claude/skills/docs-writer/SKILL.md index 3d12cd331a4..91cd69c71d7 100644 --- a/.claude/skills/docs-writer/SKILL.md +++ b/.claude/skills/docs-writer/SKILL.md @@ -193,6 +193,6 @@ Before you finish, check: - [ ] Is there a way to verify success at the end? - [ ] Did you cut every phrase from "Cut the slop"? - [ ] Did you check the page against every pattern in "Don't write like a model"? -- [ ] Did you read the prose aloud and rejoin any run of clipped sentences that belong together? +- [ ] Did you read the prose aloud and rejoin any run of clipped sentences that belong together? `.claude/skills/docs-reader-review/scripts/check-staccato.py ` finds the worst runs. - [ ] Are product names and limitations accurate? - [ ] Has the page been through `docs-reader-review`? A fact review checks truth; the reader review checks whether a reader without your context can follow it. Nothing is finished until that pass reports no sentence the reader could not restate. diff --git a/apps/docs/content/docs/orm/coming-from-prisma-orm-7.mdx b/apps/docs/content/docs/orm/coming-from-prisma-orm-7.mdx index a589776e292..63f897ba4a1 100644 --- a/apps/docs/content/docs/orm/coming-from-prisma-orm-7.mdx +++ b/apps/docs/content/docs/orm/coming-from-prisma-orm-7.mdx @@ -62,13 +62,13 @@ The biggest visible change is in field types. Where Prisma ORM 7 wrote a Prisma The `DateTime` type reads and writes its values as `Temporal.Instant` objects. If you would rather work with the timestamp as a string, write `TimestamptzString` in place of `DateTime`. -`Temporal` is built into Node.js 26 and later. It is not in Node.js 22 or 24. On those versions, install `temporal-polyfill` and add this line at the top of `src/prisma/db.ts`: +`Temporal` is built into Node.js 26 and later, but not in Node.js 22 or 24. On those versions, install `temporal-polyfill` and add this line at the top of `src/prisma/db.ts`: ```ts import "temporal-polyfill/full/global"; ``` -Homebrew's build of Node.js 26 leaves `Temporal` out. Check with `node -p "typeof Temporal"`; if it prints `undefined`, add the polyfill. +Homebrew's build of Node.js 26 leaves `Temporal` out, so check with `node -p "typeof Temporal"`; if it prints `undefined`, add the polyfill. ::: @@ -95,7 +95,7 @@ model PostTag { } ``` -That is complete as written. You do not need a `PostTag[]` field on `Post` or `Tag`. `.include("tags")` gives you `post.tags` as a `Tag[]`, and `connect` and `disconnect` work on it. `set` does not; see [Not available](#not-available-yet) below. +That is complete as written: you do not need a `PostTag[]` field on `Post` or `Tag`. `.include("tags")` gives you `post.tags` as a `Tag[]`, and `connect` and `disconnect` work on it. `set` does not; see [Not available](#not-available-yet) below. If the join table already exists from Prisma ORM 7, it is named `_PostToTag` with columns `A` and `B`. Keep the table by mapping the model to those names: `@@map("_PostToTag")` on the model, `@map("A")` on the field that points at the model whose name sorts first alphabetically (`postId`), and `@map("B")` on the other (`tagId`). @@ -159,7 +159,7 @@ The rest are in the [CLI reference](/cli). ## `src/prisma/db.ts` replaces `PrismaClient` -There is no generated `PrismaClient`. You create the client once, in `src/prisma/db.ts`, from the two files `prisma contract emit` wrote: +There is no generated `PrismaClient`: you create the client once, in `src/prisma/db.ts`, from the two files `prisma contract emit` wrote: ```ts title="src/prisma/db.ts" import "dotenv/config"; @@ -279,7 +279,7 @@ const update = db.raw.sql`UPDATE "user" SET active = false WHERE id = ${id}` await db.runtime().execute(update); ``` -The table is `user`, not `User`: Prisma ORM 8 names a table after its model with a lowercase first letter, unless the model sets `@@map`. Every raw query that returns rows needs `.returnsRow(...)` with a type per column. Take the type from the table, as above. For a computed column, write the type name instead, such as `pg/int4@1` or `pg/text@1`; the `@1` is a version number and is always `1` today. [Raw queries](/orm/reference/raw-queries) lists the names. Finish the query with `.build()` and pass it to `db.runtime().query()` for rows or `db.runtime().execute()` for a count. +The table is `user`, not `User`: Prisma ORM 8 names a table after its model with a lowercase first letter, unless the model sets `@@map`. Every raw query that returns rows needs `.returnsRow(...)` with a type per column, and you take the type from the table, as above. For a computed column, write the type name instead, such as `pg/int4@1` or `pg/text@1`; the `@1` is a version number and is always `1` today. [Raw queries](/orm/reference/raw-queries) lists the names. Finish the query with `.build()` and pass it to `db.runtime().query()` for rows or `db.runtime().execute()` for a count. ### Transaction diff --git a/apps/docs/content/docs/orm/contract-authoring/capabilities.mdx b/apps/docs/content/docs/orm/contract-authoring/capabilities.mdx index 54f924cbaaf..b62d0d117ba 100644 --- a/apps/docs/content/docs/orm/contract-authoring/capabilities.mdx +++ b/apps/docs/content/docs/orm/contract-authoring/capabilities.mdx @@ -10,7 +10,7 @@ Your [contract](/orm/contract-authoring/the-data-contract), the `contract.prisma ## Where the `capabilities` section comes from [#where-capabilities-come-from] -The keys come from your database package (`@prisma/orm-postgres`, `@prisma/orm-sqlite`, or `@prisma/orm-mongo`) and any extension packages listed in `prisma.config.ts`. `npx prisma contract emit` never connects to your database. It reads those packages and writes what they report, in groups: +The keys come from your database package (`@prisma/orm-postgres`, `@prisma/orm-sqlite`, or `@prisma/orm-mongo`) and any extension packages listed in `prisma.config.ts`. `npx prisma contract emit` never connects to your database: it reads those packages and writes what they report, in groups: ```json title="src/prisma/contract.json (excerpt)" { @@ -21,7 +21,7 @@ The keys come from your database package (`@prisma/orm-postgres`, `@prisma/orm-s } ``` -Error messages and the table below name a key group first, `postgres.pgvector.cosine`. The `sql` group holds keys that more than one SQL database may support. SQLite reports `false` for some of them. Keys under `postgres` belong to PostgreSQL only. Extension packages add keys of their own, and `pgvector.cosine` comes from the pgvector package. Install it with `npm install @prisma/orm-extension-pgvector`, list it in `prisma.config.ts`, and run `npx prisma contract emit` again: +Error messages and the table below name a key group first, `postgres.pgvector.cosine`. The `sql` group holds keys that more than one SQL database may support, and SQLite reports `false` for some of them. Keys under `postgres` belong to PostgreSQL only. Extension packages add keys of their own, and `pgvector.cosine` comes from the pgvector package. Install it with `npm install @prisma/orm-extension-pgvector`, list it in `prisma.config.ts`, and run `npx prisma contract emit` again: ```typescript title="prisma.config.ts" import "dotenv/config"; @@ -52,13 +52,13 @@ Remove the list or author it against a target that supports scalar lists. In that message, both words mean your database package, and the message's `"scalarList"` is the `sql.scalarList` key. Do one of the two: remove the list field, or move to a database that supports list fields. To move to PostgreSQL, change the `@prisma/orm-sqlite/config` import and the connection string in `prisma.config.ts`, and create the new database. -**When you call a method on the [SQL query builder](/orm/reference/sql-query-builder) or on the ORM client.** A method that needs a feature throws an error whose `code` is `ORM.CAPABILITY_MISSING`. The message names the method and the key: +**When you call a method on the [SQL query builder](/orm/reference/sql-query-builder) or on the ORM client.** A method that needs a feature throws an error whose `code` is `ORM.CAPABILITY_MISSING`, and the message names the method and the key: ```text distinctOn() requires capability postgres.distinctOn ``` -That message means the method is not available on the database package you chose. Rewrite the query without it, or move to a database that supports the feature. If the key comes from an extension package, install that package and list it in `prisma.config.ts`. You do not have to run the query to get the error, so a test that only builds the query still fails. Your own code can read the same list through `db`, the client you create once in `src/prisma/db.ts`, which `npx prisma orm init` writes. A value such as `db.contract.capabilities.sql?.lateral` is `true` or `false`, or `undefined` if the whole group is missing. For a key with a dot in it, write `db.contract.capabilities.postgres?.["pgvector.cosine"]`: +That message means the method is not available on the database package you chose, so rewrite the query without it, or move to a database that supports the feature. If the key comes from an extension package, install that package and list it in `prisma.config.ts`. You do not have to run the query to get the error, so a test that only builds the query still fails. Your own code can read the same list through `db`, the client you create once in `src/prisma/db.ts`, which `npx prisma orm init` writes. A value such as `db.contract.capabilities.sql?.lateral` is `true` or `false`, or `undefined` if the whole group is missing. For a key with a dot in it, write `db.contract.capabilities.postgres?.["pgvector.cosine"]`: ```typescript if (db.contract.capabilities.sql?.lateral) { @@ -78,15 +78,15 @@ A few of the keys your database package and extension packages supply: | `postgres.distinctOn` | `DISTINCT ON` queries (`distinctOn()`) | | `postgres.pgvector.cosine` | Cosine distance from the pgvector package | -PostgreSQL reports `true` for all three `sql` keys above. SQLite reports `false` for `sql.lateral`, `true` for `sql.returning`, and has no `sql.scalarList` key at all. This table is a sample, not the full list. Open `src/prisma/contract.json` in your project to see every key you have. The MongoDB packages report no keys, so neither check applies on MongoDB. The [ORM reference](/orm/reference) lists the methods each database has. +PostgreSQL reports `true` for all three `sql` keys above, while SQLite reports `false` for `sql.lateral`, `true` for `sql.returning`, and has no `sql.scalarList` key at all. This table is a sample, not the full list, so open `src/prisma/contract.json` in your project to see every key you have. The MongoDB packages report no keys, so neither check applies on MongoDB. The [ORM reference](/orm/reference) lists the methods each database has. ## The `capabilities` section and `prisma db verify` [#capabilities-and-verification] -[`npx prisma db verify`](/cli/db-verify) checks the live database against the contract's tables and columns, not against this section. The keys say what your packages support, recorded when you run `npx prisma contract emit`. +[`npx prisma db verify`](/cli/db-verify) checks the live database against the contract's tables and columns, not against this section: the keys say what your packages support, recorded when you run `npx prisma contract emit`. ## Prompt your coding agent -Projects created with `npm create prisma@latest` include the [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8) for your coding agent. In an existing project, run `npx prisma skills sync`. The `prisma-8` skill covers this material. Ask your agent to: +Projects created with `npm create prisma@latest` include the [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8) for your coding agent. In an existing project, run `npx prisma skills sync` to add them. The `prisma-8` skill covers this material, so ask your agent to: - "Which keys in the `capabilities` section does our code rely on, and which package provides each?" - "Add pgvector to the project and confirm its key shows up in the emitted contract." diff --git a/apps/docs/content/docs/orm/contract-authoring/psl-syntax.mdx b/apps/docs/content/docs/orm/contract-authoring/psl-syntax.mdx index 1a23dc70877..fa9de1e3c63 100644 --- a/apps/docs/content/docs/orm/contract-authoring/psl-syntax.mdx +++ b/apps/docs/content/docs/orm/contract-authoring/psl-syntax.mdx @@ -14,11 +14,11 @@ PSL, the Prisma Schema Language, is the preferred way to author [your contract]( - base models and variants: one table can hold more than one kind of record. Put the shared fields in a base model, put the differences in each variant, and Prisma ORM uses one column to tell them apart. - extension types: field types that come from an npm package, such as vectors. -The `datasource` and `generator` blocks are gone. The connection URL and the file paths are set in `prisma.config.ts` instead. [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7#schema) lists every change to the schema file, including what each old `@db.` attribute becomes. +The `datasource` and `generator` blocks are gone: the connection URL and the file paths are set in `prisma.config.ts` instead. [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7#schema) lists every change to the schema file, including what each old `@db.` attribute becomes. ## A complete contract -Every contract starts with `// use prisma-8`. Keep that line at the top. +Every contract starts with `// use prisma-8`, so keep that line at the top. ```prisma title="src/prisma/contract.prisma" tab="PostgreSQL" // use prisma-8 @@ -103,11 +103,11 @@ model Post { } ``` -`Uuid` is PostgreSQL's `uuid` type written as a field type. Run `npx prisma contract emit` after any change to refresh `contract.json` and `contract.d.ts`. Then [`npx prisma db init`](/cli/db-init) creates the tables. +`Uuid` is PostgreSQL's `uuid` type written as a field type. Run `npx prisma contract emit` after any change to refresh `contract.json` and `contract.d.ts`, and then [`npx prisma db init`](/cli/db-init) creates the tables. ## Point the config at the schema -The config's `contract` path names the one file Prisma ORM reads. It takes a single path, so there is no folder of contract files. If the path ends in `.prisma`, Prisma ORM reads it as PSL. If it ends in `.ts`, as TypeScript. The `db` key holds the connection URL: +The config's `contract` path names the one file Prisma ORM reads: it takes a single path, so there is no folder of contract files. If the path ends in `.prisma`, Prisma ORM reads it as PSL, and if it ends in `.ts`, as TypeScript. The `db` key holds the connection URL: ```typescript title="prisma.config.ts" import 'dotenv/config'; @@ -136,7 +136,7 @@ Models declare fields with a type, an optional `?` marker, and attributes. [Scal - `@default(...)` sets a default. Database function defaults such as `@default(now())` become column defaults in the database. Generated defaults such as `@default(uuid())` come from Prisma ORM, and the database will not fill them in for you, so a row written by raw SQL or another application gets no value. - `@map("column_name")` sets a field's column name in the database. `@@map("table_name")` sets the table or collection name when it differs from the model name. -`@updatedAt` is gone. Write `temporal.updatedAt()` where the field's type would go: +`@updatedAt` is gone, so write `temporal.updatedAt()` where the field's type would go: ```prisma model Post { @@ -144,7 +144,7 @@ model Post { } ``` -Prisma ORM sets the field to the current time on every create and update. `temporal` is built in. You do not import it or declare it. On PostgreSQL the column is `timestamptz`, and you can still set the field yourself on a write. `temporal.createdAt()` sets the field once, when the row is created. +Prisma ORM sets the field to the current time on every create and update. `temporal` is built in, so you do not import it or declare it. On PostgreSQL the column is `timestamptz`, and you can still set the field yourself on a write. `temporal.createdAt()` sets the field once, when the row is created. How IDs map differs by database: @@ -160,7 +160,7 @@ model User { } ``` -On PostgreSQL the primary key is an ordinary column, so pick its type and default yourself. On MongoDB the primary key is the document's `_id`. Type it `ObjectId` and map it to `_id`. +On PostgreSQL the primary key is an ordinary column, so pick its type and default yourself. On MongoDB the primary key is the document's `_id`, so type it `ObjectId` and map it to `_id`. ## Named types @@ -172,11 +172,11 @@ types { } ``` -Fields then use `ShortName` like any built-in type. The name keeps the column decision in one place: a `varchar(35)` column rather than `text`. A name is optional, and a field can use the native PostgreSQL type directly, such as `VarChar(35)`, `Uuid`, or `Timestamptz`. In Prisma ORM 8 the native type is the field's type, so `String @db.VarChar(35)` from Prisma ORM 7 becomes `VarChar(35)`. The `@db.` attributes are gone. The `types` block is PostgreSQL only. +Fields then use `ShortName` like any built-in type. The name keeps the column decision in one place: a `varchar(35)` column rather than `text`. A name is optional, and a field can use the native PostgreSQL type directly, such as `VarChar(35)`, `Uuid`, or `Timestamptz`. In Prisma ORM 8 the native type is the field's type, so `String @db.VarChar(35)` from Prisma ORM 7 becomes `VarChar(35)`, and the `@db.` attributes are gone. The `types` block is PostgreSQL only. ## Enums -An enum lists its members. It can also say how their values are stored, with `@@type`, and what each member stores: +An enum lists its members. It can also say, with `@@type`, how their values are stored, and what each member stores: ```prisma enum Priority { @@ -189,7 +189,7 @@ enum Priority { In `pg/text@1`, `pg` is PostgreSQL, `text` is the column type, and `@1` is the version of how the value is stored and read. Write `@@type("pg/int4@1")` to store the values as integers instead. When a member has no explicit value, the member name itself is stored. -`@@type` is optional. Leave it out and Prisma ORM picks the type from the member values: bare member names and string values give the database's text type, and integer values give its integer type. Give every member the same kind of value, because a mix of string and integer values throws an error whose `code` is `PSL_ENUM_CANNOT_INFER_TYPE`. A `@default` names the member, as in `priority Priority @default(Low)`, even where the member stores a different value. +`@@type` is optional, and when you leave it out Prisma ORM picks the type from the member values: bare member names and string values give the database's text type, and integer values give its integer type. Give every member the same kind of value, because a mix of string and integer values throws an error whose `code` is `PSL_ENUM_CANNOT_INFER_TYPE`. A `@default` names the member, as in `priority Priority @default(Low)`, even where the member stores a different value. An `enum` block is not a PostgreSQL `enum` type: the column is text or an integer. For a PostgreSQL `enum` type, declare it in a `native_enum` block and type the field `pg.enum(Role)`: @@ -225,11 +225,11 @@ model User { } ``` -A value object field can be optional or a list, and a `type` block can hold a field of another `type`. Watch the two spellings: `types { ... }` declares named types, and `type X { ... }` declares a value object. Storage differs by database. On PostgreSQL a value object field is stored in a single `jsonb` column. On MongoDB it is an embedded document. Either way, `contract.d.ts` types it as a structured object rather than untyped JSON. On MongoDB, whether to embed or reference is the central modeling decision. [MongoDB data modeling](/orm/data-modeling/mongodb#embed-or-reference) covers it. +A value object field can be optional or a list, and a `type` block can hold a field of another `type`. Watch the two spellings: `types { ... }` declares named types, and `type X { ... }` declares a value object. Storage differs by database: on PostgreSQL a value object field is stored in a single `jsonb` column, while on MongoDB it is an embedded document. Either way, `contract.d.ts` types it as a structured object rather than untyped JSON. On MongoDB, whether to embed or reference is the central modeling decision, and [MongoDB data modeling](/orm/data-modeling/mongodb#embed-or-reference) covers it. ## Relations -Relations use the `@relation` syntax you know from Prisma ORM. The side that holds the foreign key declares the scalar field and the mapping. The other side declares a list: +Relations use the `@relation` syntax you know from Prisma ORM. The side that holds the foreign key declares the scalar field and the mapping, and the other side declares a list: ```prisma model Post { @@ -242,7 +242,7 @@ model User { } ``` -Add `onDelete` and `onUpdate` to the same `@relation`. They belong on the side that holds the foreign key, not on the list side. For a one-to-one, make the other side singular instead of a list, so `User` declares `profile Profile?`, and put `@unique` on the foreign-key field: +Add `onDelete` and `onUpdate` to the same `@relation`: they belong on the side that holds the foreign key, not on the list side. For a one-to-one, make the other side singular instead of a list, so `User` declares `profile Profile?`, and put `@unique` on the foreign-key field: ```prisma model Profile { @@ -251,7 +251,7 @@ model Profile { } ``` -Many-to-many relations need a model for the join table. You write that model yourself: there is no implicit many-to-many. That model must follow two rules. If a side has a composite primary key, that model needs one foreign-key field for each part of it. Its `@@id([...])` must list exactly the foreign-key fields, and nothing else. +Many-to-many relations need a model for the join table, and you write that model yourself: there is no implicit many-to-many. That model must follow two rules: if a side has a composite primary key, that model needs one foreign-key field for each part of it, and its `@@id([...])` must list exactly the foreign-key fields and nothing else. ```prisma model Post { @@ -274,7 +274,7 @@ model PostTag { } ``` -You then read `post.tags` as a list of `Tag`, without mentioning `PostTag` in the query. Break either rule and `npx prisma contract emit` reports which list field it could not match to a model. If two models qualify, `npx prisma contract emit` throws an error whose `code` is `PSL_AMBIGUOUS_BACKRELATION`. Put the same `@relation("name")` on both ends of one pair, so on `PostTag.post` for `Post.tags`. +You then read `post.tags` as a list of `Tag`, without mentioning `PostTag` in the query. Break either rule and `npx prisma contract emit` reports which list field it could not match to a model. If two models qualify, `npx prisma contract emit` throws an error whose `code` is `PSL_AMBIGUOUS_BACKRELATION`: put the same `@relation("name")` on both ends of one pair, so on `PostTag.post` for `Post.tags`. For which shape to choose and which side owns the foreign key, see [relational data modeling](/orm/data-modeling/relational-databases) and [MongoDB data modeling](/orm/data-modeling/mongodb). @@ -303,9 +303,9 @@ model Bug { A field name is written bare, as `type` is in `@@discriminator(type)`, and a database name is quoted, as in `@@map("task")`. Rows whose `type` column holds `"bug"` are `Bug` records. A variant reuses its base model's fields, so a `Bug` has `id`, `title`, and `type` as well as `severity` and `stepsToRepro`. A variant does not declare an `@id` of its own: it takes the base model's primary key. -You query a variant through the base model: `db.orm.public.Task.variant('Bug')` returns the `Bug` rows only. Here `public` is the PostgreSQL schema. Writing a row of a variant starts with the same `variant('Bug')` call. See [`variant()`](/orm/reference/orm-client#variant). +You query a variant through the base model: `db.orm.public.Task.variant('Bug')` returns the `Bug` rows only, and `public` here is the PostgreSQL schema. Writing a row of a variant starts with the same `variant('Bug')` call. See [`variant()`](/orm/reference/orm-client#variant). -On a variant, `@@map` does more than rename. On PostgreSQL it chooses between two storage layouts. Give the variant its own `@@map`, as `Bug` has here, and its fields are in a table of their own that shares the base model's primary key. Leave `@@map` out and they are nullable columns in the base table. [Relational data modeling](/orm/data-modeling/relational-databases#polymorphic-relations) covers choosing between the two. +On a variant, `@@map` does more than rename: on PostgreSQL it chooses between two storage layouts. Give the variant its own `@@map`, as `Bug` has here, and its fields are in a table of their own that shares the base model's primary key. Leave `@@map` out and they are nullable columns in the base table. [Relational data modeling](/orm/data-modeling/relational-databases#polymorphic-relations) covers choosing between the two. On MongoDB, a variant adds its fields to documents in the base model's collection, so it declares `@@base` but no `@@map` of its own. @@ -335,15 +335,15 @@ model Post { } ``` -The `pgvector` part of `pgvector.Vector(1536)` is a fixed name the pack declares, not the name you gave the import. List the pack before using its types. Run `npx prisma contract emit` again after changing the extension list. [Using extensions](/orm/extensions/using-extensions) covers installing a pack and names the packs you can add. +The `pgvector` part of `pgvector.Vector(1536)` is a fixed name the pack declares, not the name you gave the import. List the pack before using its types, and run `npx prisma contract emit` again after changing the extension list. [Using extensions](/orm/extensions/using-extensions) covers installing a pack and names the packs you can add. ## Starting from an existing database -If the database already exists, don't write the contract by hand. [`contract infer`](/cli/contract-infer) reads the live schema and writes a starter `contract.prisma` for you to review and edit. +If the database already exists, don't write the contract by hand: [`contract infer`](/cli/contract-infer) reads the live schema and writes a starter `contract.prisma` for you to review and edit. ## Prompt your coding agent -Projects created with `npm create prisma@latest -- my-app` include the [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8) for your coding agent. Skills are instruction files the agent reads. In an existing project, run `npx prisma skills sync`. The `prisma-8` skill covers PSL authoring. Ask your agent to: +Projects created with `npm create prisma@latest -- my-app` include the [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8) for your coding agent: skills are instruction files the agent reads. In an existing project, run `npx prisma skills sync` to add them. The `prisma-8` skill covers PSL authoring, so ask your agent to: - "Using the prisma-8 skill, add a Status enum stored as text and use it on the Order model." - "Add a one-to-many between User and Post with the foreign key on Post." diff --git a/apps/docs/content/docs/orm/contract-authoring/the-contract-artifact.mdx b/apps/docs/content/docs/orm/contract-authoring/the-contract-artifact.mdx index 7952cb9a545..df30ca00ec8 100644 --- a/apps/docs/content/docs/orm/contract-authoring/the-contract-artifact.mdx +++ b/apps/docs/content/docs/orm/contract-authoring/the-contract-artifact.mdx @@ -6,14 +6,14 @@ metaTitle: The Prisma ORM contract files metaDescription: 'What contract.json and contract.d.ts contain, why the same source always produces the same two files, how the hashes work, and how your app and the CLI read them.' --- -Your contract is [a single file that describes your data and how it is stored](/orm/contract-authoring/the-data-contract). It replaces the `schema.prisma` of Prisma ORM 7, and it is `src/prisma/contract.prisma` in Prisma Schema Language (PSL) or `src/prisma/contract.ts` in TypeScript. [`npx prisma contract emit`](/cli/contract-emit) reads that file and writes two files beside it. In the table below, `db` is your client, the object you run queries on. [`npx prisma orm init`](/cli/orm-init) creates the contract source, `src/prisma/db.ts`, and `.env.example` for you. In Prisma ORM 7 you edited `schema.prisma` and the tooling read the same file. Now you edit the contract and the tooling reads the two files it produces. +Your contract is [a single file that describes your data and how it is stored](/orm/contract-authoring/the-data-contract). It replaces the `schema.prisma` of Prisma ORM 7 and is either `src/prisma/contract.prisma` in Prisma Schema Language (PSL) or `src/prisma/contract.ts` in TypeScript. [`npx prisma contract emit`](/cli/contract-emit) reads that file and writes two files beside it. In the table below, `db` is your client, the object you run queries on. [`npx prisma orm init`](/cli/orm-init) creates the contract source, `src/prisma/db.ts`, and `.env.example` for you. In Prisma ORM 7 you edited `schema.prisma` and the tooling read the same file, but now you edit the contract and the tooling reads the two files it produces. | File | Contents | Read by | | --- | --- | --- | | `contract.json` | The machine-readable contract: models, how they are stored, which database features they need, and the hashes | `db`, `prisma db sign`, `prisma db verify`, and `prisma migration plan` | | `contract.d.ts` | TypeScript declarations derived from the contract | The query APIs and your application code, for typed models and results | -`prisma db sign` writes the current contract's hashes into the database, so later commands can tell which contract that database was built for. Both files are generated. Do not edit them. Change the [PSL](/orm/contract-authoring/psl-syntax) or [TypeScript](/orm/contract-authoring/typescript-schema-builder) source and run `npx prisma contract emit` again. `contract.d.ts` starts with a comment saying it is generated and must not be edited, and `contract.json` carries the same notice in a `_generated` entry. To write the two files to another directory, see [`prisma contract emit`](/cli/contract-emit). +`prisma db sign` writes the current contract's hashes into the database, so later commands can tell which contract that database was built for. Both files are generated, so do not edit them: change the [PSL](/orm/contract-authoring/psl-syntax) or [TypeScript](/orm/contract-authoring/typescript-schema-builder) source and run `npx prisma contract emit` again. `contract.d.ts` starts with a comment saying it is generated and must not be edited, and `contract.json` carries the same notice in a `_generated` entry. To write the two files to another directory, see [`prisma contract emit`](/cli/contract-emit). The everyday loop on a project: @@ -33,11 +33,11 @@ Run `npx prisma contract emit` on the same source on any machine and you get the ## The content hashes -`npx prisma contract emit` computes a hash over each part of the contract. You never compute or compare them yourself. `storageHash` covers the storage layout: tables, columns, primary keys, uniques, indexes, foreign keys, and the allowed values of each enum. `profileHash` covers which database the contract targets. The hashes are how Prisma ORM connects the contract to a live database. `npx prisma db sign` checks that the database satisfies the contract, then records `storageHash` and `profileHash` in the database. `npx prisma db verify` compares the contract you hold against that record and against the live schema, and fails when they disagree. To fix a mismatch, apply the pending migration, or run `npx prisma db sign` when the database already matches the contract. +`npx prisma contract emit` computes a hash over each part of the contract, and you never compute or compare them yourself. `storageHash` covers the storage layout: tables, columns, primary keys, uniques, indexes, foreign keys, and the allowed values of each enum. `profileHash` covers which database the contract targets. The hashes are how Prisma ORM connects the contract to a live database: `npx prisma db sign` checks that the database satisfies the contract, then records `storageHash` and `profileHash` in the database. `npx prisma db verify` compares the contract you hold against that record and against the live schema, and fails when they disagree. To fix a mismatch, apply the pending migration, or run `npx prisma db sign` when the database already matches the contract. ## Inside `contract.json` -The contract keeps your application's view of the data separate from the way the database stores it. The `domain` section describes models, fields, and relations. The `storage` section describes tables, columns, keys, and indexes. On MongoDB it describes collections and indexes instead. Each model's own `storage` block maps each field to its column. Everything is grouped by namespace. On PostgreSQL the namespace is the schema. It is `public` unless you put the models in a `namespace` block in your [PSL source](/orm/contract-authoring/psl-syntax). +The contract keeps your application's view of the data separate from the way the database stores it. The `domain` section describes models, fields, and relations, while the `storage` section describes tables, columns, keys, and indexes. On MongoDB it describes collections and indexes instead. Each model's own `storage` block maps each field to its column. Everything is grouped by namespace, and on PostgreSQL the namespace is the schema, which is `public` unless you put the models in a `namespace` block in your [PSL source](/orm/contract-authoring/psl-syntax). An abridged `contract.json` for a `User` and `Post` schema: @@ -130,13 +130,13 @@ export const db = postgres({ }); ``` -The connection string comes from the `DATABASE_URL` environment variable. `prisma orm init` writes `.env.example`. Put your `DATABASE_URL` in `.env`. Keep the `.d` in the import path. Delete the `.d` and the import stops resolving. The `tsconfig.json` settings the JSON import needs are on the [transactions and runtime page](/orm/reference/transactions-and-runtime). Before its first query, `db` reads the record in the database of which contract it matches and compares it with the contract's hashes. If they do not match, it logs a warning whose `code` is `CONTRACT.MARKER_MISMATCH` and runs the query anyway. If there is no record at all, because nothing has signed the database yet, it logs a warning whose `code` is `CONTRACT.MARKER_MISSING` and again runs the query. On MongoDB the client is `mongo(...)` from `@prisma/orm-mongo/runtime`, described under [`mongo(options)`](/orm/reference/transactions-and-runtime#mongooptions). +The connection string comes from the `DATABASE_URL` environment variable: `prisma orm init` writes `.env.example`, and you put your `DATABASE_URL` in `.env`. Keep the `.d` in the import path, because if you delete it the import stops resolving. The `tsconfig.json` settings the JSON import needs are on the [transactions and runtime page](/orm/reference/transactions-and-runtime). Before its first query, `db` reads the record in the database of which contract it matches and compares it with the contract's hashes. If they do not match, it logs a warning whose `code` is `CONTRACT.MARKER_MISMATCH` and runs the query anyway. If there is no record at all, because nothing has signed the database yet, it logs a warning whose `code` is `CONTRACT.MARKER_MISSING` and again runs the query. On MongoDB the client is `mongo(...)` from `@prisma/orm-mongo/runtime`, described under [`mongo(options)`](/orm/reference/transactions-and-runtime#mongooptions). -When `CONTRACT.MARKER_MISMATCH` shows up in production, apply the pending migration, or run `npx prisma db sign` when the database already matches the contract. The client warns by default. Set [`verifyMarker`](/orm/reference/transactions-and-runtime#postgresoptions) to `false` to skip the check. It cannot be made to fail. Run `prisma db verify` when you need a check that fails. +When `CONTRACT.MARKER_MISMATCH` shows up in production, apply the pending migration, or run `npx prisma db sign` when the database already matches the contract. The client warns by default, and you can set [`verifyMarker`](/orm/reference/transactions-and-runtime#postgresoptions) to `false` to skip the check. It cannot be made to fail, so run `prisma db verify` when you need a check that fails. ## Version control -Commit `contract.json` and `contract.d.ts` alongside the source. They hold structure only, no data and no credentials. Committing them lets teammates, CI, and deploys read the contract without running `npx prisma contract emit` first. Run `npx prisma contract emit` after every source change so the two files never trail the source. A CI job can check this in two lines: +Commit `contract.json` and `contract.d.ts` alongside the source: they hold structure only, no data and no credentials. Committing them lets teammates, CI, and deploys read the contract without running `npx prisma contract emit` first. Run `npx prisma contract emit` after every source change so the two files never trail the source. A CI job can check this in two lines: ```bash npx prisma contract emit @@ -145,7 +145,7 @@ git diff --exit-code src/prisma ## Prompt your coding agent -Projects created with `npm create prisma@latest` include the [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8) for your coding agent. In an existing project, run `npx prisma skills sync`. The `prisma-8` skill covers the contract. Ask your agent to: +Projects created with `npm create prisma@latest` include the [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8) for your coding agent. In an existing project, run `npx prisma skills sync` to add them. The `prisma-8` skill covers the contract, so ask your agent to: - "Explain the difference between contract.json and contract.d.ts in this project." - "Run prisma contract emit and show me what changed in contract.json." diff --git a/apps/docs/content/docs/orm/contract-authoring/the-data-contract.mdx b/apps/docs/content/docs/orm/contract-authoring/the-data-contract.mdx index 83711263ae5..fe5aef2417a 100644 --- a/apps/docs/content/docs/orm/contract-authoring/the-data-contract.mdx +++ b/apps/docs/content/docs/orm/contract-authoring/the-data-contract.mdx @@ -25,7 +25,7 @@ model Post { } ``` -`npx prisma orm init` writes the first line, `// use prisma-8`, into every `.prisma` contract. The CLI does not need this line. The VS Code extension does, so keep it. A `contract.ts` file needs no header line. +`npx prisma orm init` writes the first line, `// use prisma-8`, into every `.prisma` contract. The CLI does not need this line, but the VS Code extension does, so keep it. A `contract.ts` file needs no header line. `npx prisma contract emit` turns this file into two files that the CLI and your application read: `contract.json` and `contract.d.ts`. Your queries are type-checked against the contract, migrations are planned as changes to it, and `npx prisma db verify` checks a live database against it. @@ -37,9 +37,9 @@ In Prisma ORM, the **contract** is what you write in your code. The **schema** i ## Why a contract -Your contract compiles to two plain files you can open and read. `contract.json` describes your models, how they are stored, and the [database features they need](/orm/contract-authoring/capabilities). `contract.d.ts` holds the TypeScript types derived from it. You can read both files in a code review and hand them to tools and coding agents. +Your contract compiles to two plain files you can open and read: `contract.json` describes your models, how they are stored, and the [database features they need](/orm/contract-authoring/capabilities), and `contract.d.ts` holds the TypeScript types derived from it. You can read both files in a code review and hand them to tools and coding agents. -`npx prisma db migrate` records in your database which contract it applied. The first time `db` runs a query it checks that record and logs a warning on a mismatch. To fail instead, run [`npx prisma db verify`](/cli/db-verify) in a deploy check. +`npx prisma db migrate` records in your database which contract it applied, and the first time `db` runs a query it checks that record and logs a warning on a mismatch. To fail instead, run [`npx prisma db verify`](/cli/db-verify) in a deploy check. ## How it works @@ -60,7 +60,7 @@ export default definePrismaConfig({ }); ``` -`definePrismaConfig` holds the whole config: which contract file to use and how to reach your database. Your connection string is in `prisma.config.ts` now, and the contract has no `datasource` block. `npx prisma orm init` writes `.env.example`, and your `DATABASE_URL` goes in `.env`. `@prisma/orm-postgres/config` exports `defineConfig`. The example renames it to `ormConfig` on import. The packages are `@prisma/orm-postgres` for PostgreSQL, `@prisma/orm-sqlite` for SQLite, and `@prisma/orm-mongo` for MongoDB. The examples here use PostgreSQL. +`definePrismaConfig` holds the whole config: which contract file to use and how to reach your database. Your connection string is in `prisma.config.ts` now, and the contract has no `datasource` block. `npx prisma orm init` writes `.env.example`, and your `DATABASE_URL` goes in `.env`. `@prisma/orm-postgres/config` exports `defineConfig`, which the example renames to `ormConfig` on import. The packages are `@prisma/orm-postgres` for PostgreSQL, `@prisma/orm-sqlite` for SQLite, and `@prisma/orm-mongo` for MongoDB. The examples here use PostgreSQL. To start a new project, run `npm create prisma@latest -- my-app`. To add Prisma ORM 8 to a project that has no Prisma ORM at all, run `npx prisma orm init`. If you already have a Prisma ORM 7 `schema.prisma`, follow the [PostgreSQL upgrade guide](/guides/upgrade-prisma-orm/postgresql) instead, and [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7) lists what changed. Both `npm create prisma@latest` and `npx prisma orm init` write `prisma.config.ts`, the contract file, and `src/prisma/db.ts`, and install the database package. @@ -80,11 +80,11 @@ npx prisma db migrate ## Two authoring modes, one `contract.json` [#two-authoring-modes-one-artifact] -**[PSL](/orm/contract-authoring/psl-syntax)** is the usual way to write the contract. It is a compact language for describing data. It is what `npm create prisma@latest` scaffolds, and what [`npx prisma contract infer`](/cli/contract-infer) writes when you start from an existing database. +**[PSL](/orm/contract-authoring/psl-syntax)**, a compact language for describing data, is the usual way to write the contract: it is what `npm create prisma@latest` scaffolds, and what [`npx prisma contract infer`](/cli/contract-infer) writes when you start from an existing database. -Define models with the **[TypeScript builder](/orm/contract-authoring/typescript-schema-builder)** instead when you want to build them with code. Reach for it when model definitions must be composed, generated, or shared as ordinary TypeScript modules. That page shows what a `contract.ts` file looks like. +Define models with the **[TypeScript builder](/orm/contract-authoring/typescript-schema-builder)** instead when you want to build them with code: reach for it when model definitions must be composed, generated, or shared as ordinary TypeScript modules. That page shows what a `contract.ts` file looks like. -Both modes produce the same contract, so migrations, database checks, and `db` behave the same no matter which mode you write. A project has exactly one contract file. Do not keep both a `contract.prisma` and a `contract.ts`: the config names one file and Prisma ORM reads only that one, so edits to the other file do nothing. To switch modes, change the path in `contract` and delete the old file. +Both modes produce the same contract, so migrations, database checks, and `db` behave the same no matter which mode you write. A project has exactly one contract file, so do not keep both a `contract.prisma` and a `contract.ts`: the config names one file and Prisma ORM reads only that one, so edits to the other file do nothing. To switch modes, change the path in `contract` and delete the old file. ## What the contract contains @@ -100,7 +100,7 @@ It contains no rows, no credentials, and no connection details, so committing th ## Prompt your coding agent -Projects created with `npm create prisma@latest` include the [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8) for your coding agent. In an existing project, run `npx prisma skills sync`. The `prisma-8` skill covers the contract. Ask your agent to: +Projects created with `npm create prisma@latest` include the [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8) for your coding agent. In an existing project, run `npx prisma skills sync` to add them. The `prisma-8` skill covers the contract, so ask your agent to: - "Using the prisma-8 skill, explain what our contract.json currently declares." - "Add an Invoice model to the contract and run prisma contract emit." diff --git a/apps/docs/content/docs/orm/contract-authoring/typescript-schema-builder.mdx b/apps/docs/content/docs/orm/contract-authoring/typescript-schema-builder.mdx index e94aa77ec36..5f4b1826ba4 100644 --- a/apps/docs/content/docs/orm/contract-authoring/typescript-schema-builder.mdx +++ b/apps/docs/content/docs/orm/contract-authoring/typescript-schema-builder.mdx @@ -6,22 +6,22 @@ metaTitle: Author the Prisma ORM contract in TypeScript metaDescription: 'How to define a Prisma ORM contract in TypeScript with defineContract, including fields, relations, enums, storage mapping, and extension types.' --- -In Prisma ORM 8, `schema.prisma` is replaced by a file called [the contract](/orm/contract-authoring/the-data-contract). It holds the model definitions you used to write in `schema.prisma`. You can write it in two forms. One is a `.prisma` file written in PSL, short for Prisma Schema Language. The other is TypeScript, in `src/prisma/contract.ts`, built with the `defineContract` builder. Both forms produce the same two files. [`npx prisma contract emit`](/cli/contract-emit) writes `contract.json` and `contract.d.ts` into the folder that holds your contract file. +In Prisma ORM 8, `schema.prisma` is replaced by a file called [the contract](/orm/contract-authoring/the-data-contract), which holds the model definitions you used to write in `schema.prisma`. You can write it in two forms: one is a `.prisma` file written in PSL, short for Prisma Schema Language, and the other is TypeScript, in `src/prisma/contract.ts`, built with the `defineContract` builder. Both forms produce the same two files: [`npx prisma contract emit`](/cli/contract-emit) writes `contract.json` and `contract.d.ts` into the folder that holds your contract file. ## When to choose TypeScript over PSL -[PSL](/orm/contract-authoring/psl-syntax) is the preferred way to write the contract. Both forms produce the same two files, so you give up nothing by staying with PSL. Use the TypeScript builder for the cases PSL does not cover. Reach for it when: +[PSL](/orm/contract-authoring/psl-syntax) is the preferred way to write the contract, and both forms produce the same two files, so you give up nothing by staying with PSL. Use the TypeScript builder for the cases PSL does not cover, and reach for it when: - model definitions must be split, composed, or reused across ordinary TypeScript modules or packages - you want to build models in a loop from data you already keep in TypeScript, such as one model per entry in a list of table names -If neither applies, write PSL. It is more compact, and it is what [`contract infer`](/cli/contract-infer) writes. +If neither applies, write PSL, which is more compact and is what [`contract infer`](/cli/contract-infer) writes. ## Point the config at the contract file -For a new project, run [`npx prisma orm init`](/cli/orm-init). It asks how you want to write your schema, and choosing TypeScript creates the contract file and the config together. +For a new project, run [`npx prisma orm init`](/cli/orm-init), which asks how you want to write your schema, and choosing TypeScript creates the contract file and the config together. -The config's `contract` path names the one file Prisma ORM reads. A `.ts` extension selects TypeScript authoring: +The config's `contract` path names the one file Prisma ORM reads, and a `.ts` extension selects TypeScript authoring: ```typescript title="prisma.config.ts" tab="PostgreSQL" import { definePrismaConfig } from "prisma/config"; @@ -49,7 +49,7 @@ export default definePrismaConfig({ The builder comes from your database's package: `@prisma/orm-postgres/contract-builder` for PostgreSQL, `@prisma/orm-mongo/contract-builder` for MongoDB. -Export the contract under the name `contract`, as below, or as the file's default export. Those are the only two names `prisma contract emit` looks for. Run `npx prisma contract emit` again after every edit to the contract, and commit the contract file together with [`contract.json` and `contract.d.ts`](/orm/contract-authoring/the-contract-artifact). +Export the contract under the name `contract`, as below, or as the file's default export, because those are the only two names `prisma contract emit` looks for. Run `npx prisma contract emit` again after every edit to the contract, and commit the contract file together with [`contract.json` and `contract.d.ts`](/orm/contract-authoring/the-contract-artifact). ```typescript title="src/prisma/contract.ts" tab="PostgreSQL" import { defineContract, enumType, member } from "@prisma/orm-postgres/contract-builder"; @@ -124,9 +124,9 @@ export const contract = defineContract({ models: { User, Post } }); ## How `defineContract` works -On PostgreSQL, `defineContract` takes an options object and then a function. You do not set a `provider` anywhere. Importing `@prisma/orm-postgres/contract-builder` is what selects PostgreSQL. The options object lists the extension packs you use, and you write `{}` when you use none. The function returns the contract's content: `models`, plus `enums` and `types` if you have them. +On PostgreSQL, `defineContract` takes an options object and then a function. You do not set a `provider` anywhere: importing `@prisma/orm-postgres/contract-builder` is what selects PostgreSQL. The options object lists the extension packs you use, and you write `{}` when you use none. The function returns the contract's content: `models`, plus `enums` and `types` if you have them. -Take `field`, `model`, `rel`, and `type` from the function's one argument. Import `defineContract`, `enumType`, and `member` from the package. The package also exports `model` and `rel` for use outside the function, plus a `field` that has only `column`, `generated`, and `namedType`. +Take `field`, `model`, `rel`, and `type` from the function's one argument, and import `defineContract`, `enumType`, and `member` from the package. The package also exports `model` and `rel` for use outside the function, plus a `field` that has only `column`, `generated`, and `namedType`. On MongoDB, `defineContract` also accepts a single object holding `models`, as above, and you import `field`, `model`, and `rel` from the package instead. The MongoDB builder differs from the PostgreSQL one in five ways: @@ -136,7 +136,7 @@ On MongoDB, `defineContract` also accepts a single object holding `models`, as a - Relations name the field on each side. `rel.hasMany` takes `{ from, to }` on MongoDB and `{ by }` on PostgreSQL. `to` accepts either the field name as a string or a typed reference such as `User.ref("_id")`, and both forms mean the same thing. PostgreSQL writes that reference as `User.refs.id`. - Indexes are an `indexes` option on the model, built with the `index` helper the same package exports, as in `indexes: [index({ email: 1 }, { unique: true })]`. The `1` is ascending, MongoDB's own index syntax. -Enums and extension packs work on MongoDB too. `enumType` and `member` come from `@prisma/orm-mongo/contract-builder`. Pass the enums in the same object as the models, as `defineContract({ models, enums })`, and extension packs go in the same `extensions` option. +Enums and extension packs work on MongoDB too: `enumType` and `member` come from `@prisma/orm-mongo/contract-builder`. Pass the enums in the same object as the models, as `defineContract({ models, enums })`, and extension packs go in the same `extensions` option. The examples below use the PostgreSQL builder. @@ -160,9 +160,9 @@ The examples below use the PostgreSQL builder. There is no helper for a date without a time, or a time without a date. For a date-only column, name the type yourself: `field.column({ codecId: "pg/date-temporal@1", nativeType: "date" } as const)`. `field.column(...)` and the chained `.column(...)` below are different calls: `field.column(descriptor)` builds a field from a type description, and `.column(name)` on an existing field sets its column name. Extension packs add more helpers of their own. -For a primary key whose value your app generates, use `field.id.uuidv4String()`, `field.id.uuidv7String()`, `field.id.ulid()`, `field.id.nanoid()`, `field.id.cuid2()`, or `field.id.ksuid()`. They all generate the ID in your app, and `uuidv4String` is the common choice. Each marks the field as the primary key for you. `field.id.uuidv4Native()` and `field.id.uuidv7Native()` do the same in a `uuid` column. There is no auto-increment helper, so use one of those `field.id.*` helpers. For a key the database generates instead, write `field.uuidNative().defaultSql("gen_random_uuid()").id()`. +For a primary key whose value your app generates, use `field.id.uuidv4String()`, `field.id.uuidv7String()`, `field.id.ulid()`, `field.id.nanoid()`, `field.id.cuid2()`, or `field.id.ksuid()`. They all generate the ID in your app, and `uuidv4String` is the common choice. Each marks the field as the primary key for you, and `field.id.uuidv4Native()` and `field.id.uuidv7Native()` do the same in a `uuid` column. There is no auto-increment helper, so use one of those `field.id.*` helpers. For a key the database generates instead, write `field.uuidNative().defaultSql("gen_random_uuid()").id()`. -`field.temporal.createdAt()` fills the column with `now()` when the row is created. `field.temporal.updatedAt()` fills it on every create and every update, and Prisma ORM sets it in the client, not with a database trigger. `field.namedType(x)` takes an enum or a type from an extension pack. +`field.temporal.createdAt()` fills the column with `now()` when the row is created, while `field.temporal.updatedAt()` fills it on every create and every update, and Prisma ORM sets it in the client, not with a database trigger. `field.namedType(x)` takes an enum or a type from an extension pack. Every field builder supports chained modifiers: @@ -192,15 +192,15 @@ export const contract = defineContract({}, ({ field, model }) => { }); ``` -`Role` does not go in the returned `enums` map. Using it on a field is enough to create the type in the database. +`Role` does not go in the returned `enums` map: using it on a field is enough to create the type in the database. ## Relations Relations are declared on the model builder with `.relations(...)` and the `rel` helpers, as the full example above shows. -`rel.hasMany(Model, { by })` names the foreign key field on the other model. `rel.hasOne(Model, { by })` is the same with at most one row on the other side. `rel.belongsTo(Model, { from, to })` maps the local foreign key field to the field it points at. +`rel.hasMany(Model, { by })` names the foreign key field on the other model, and `rel.hasOne(Model, { by })` is the same with at most one row on the other side. `rel.belongsTo(Model, { from, to })` maps the local foreign key field to the field it points at. -`rel.manyToMany(Model, { through, from, to })` goes through a join table. You declare the model for the join table yourself and pass it as `through`. Below, `PostTag` is that model, and it holds the two foreign key fields `postId` and `tagId`: +`rel.manyToMany(Model, { through, from, to })` goes through a join table: you declare the model for the join table yourself and pass it as `through`. Below, `PostTag` is that model, holding the two foreign key fields `postId` and `tagId`: ```typescript Post.relations({ @@ -208,7 +208,7 @@ Post.relations({ }); ``` -`rel.belongsTo` on its own does not create a foreign key constraint in the database. Ask for one by chaining `.sql(...)` on the relation itself, inside the `.relations({ ... })` object, as the full example above does: +`rel.belongsTo` on its own does not create a foreign key constraint in the database, so ask for one by chaining `.sql(...)` on the relation itself, inside the `.relations({ ... })` object, as the full example above does: ```typescript Post.relations({ @@ -220,13 +220,13 @@ Post.relations({ Prisma ORM does not check that the column types on the two sides of a relation match, so choose a field helper that produces the same column type as the key you point at. -`rel.hasMany`, `rel.hasOne`, `rel.belongsTo`, and `rel.manyToMany` also accept the model name as a string. Pass the model object, as above, and a typo is a compile error. Pass a string and the typo is reported when `prisma contract emit` builds the contract. +`rel.hasMany`, `rel.hasOne`, `rel.belongsTo`, and `rel.manyToMany` also accept the model name as a string. Pass the model object, as above, and a typo is a compile error, but pass a string and the typo is reported when `prisma contract emit` builds the contract. ## Storage mapping -`.sql(...)` maps a model to its table. The object form covers the common case: `User.sql({ table: "user" })`. +`.sql(...)` maps a model to its table, and the object form covers the common case: `User.sql({ table: "user" })`. -You can chain `.relations(...)`, `.attributes(...)`, and `.sql(...)` in any order, and you can skip any of them. A model with no relations calls `.sql({ table })` straight after `model(...)`. +You can chain `.relations(...)`, `.attributes(...)`, and `.sql(...)` in any order, and you can skip any of them, so a model with no relations calls `.sql({ table })` straight after `model(...)`. The callback form gives you the model's columns as `cols` and the constraint builders as `constraints`. Use it for indexes: @@ -273,7 +273,7 @@ export const contract = defineContract({ extensions: { pgvector } }, ({ field, m The key on `type.pgvector` is the pack's own name, not the name you gave the import. The name is in the pack's documentation, and pgvector's is `pgvector`. `Embedding1536` is a name you choose, and it appears under that name in `contract.d.ts`. Return the `types` map so the name reaches the two files. -Add the same pack to `prisma.config.ts`. It goes inside `ormConfig({ ... })`, beside `contract`, and the config imports the pack's `/control` export while the contract file imports its `/pack` export: +Add the same pack to `prisma.config.ts`: it goes inside `ormConfig({ ... })`, beside `contract`, and the config imports the pack's `/control` export while the contract file imports its `/pack` export: ```typescript title="prisma.config.ts" import { definePrismaConfig } from "prisma/config"; @@ -290,7 +290,7 @@ export default definePrismaConfig({ ## Keep the contract file free of changing values [#keep-the-contract-file-pure] -The contract file describes structure. These rules keep it usable: +The contract file describes structure, and these rules keep it usable: - Do not read `process.env`, the current time, or random values into the contract. A contract built from those values makes `npx prisma contract emit` produce a different `contract.json` on each run. - Keep field values plain: strings, numbers, booleans, and the builder's own objects. Functions, class instances, and `Date` objects do not serialize. @@ -301,13 +301,13 @@ Configuration that legitimately varies per environment, such as the database URL ## Parity with PSL -TypeScript and [PSL](/orm/contract-authoring/psl-syntax) authoring produce the same `contract.json` and `contract.d.ts` for an equivalent contract. You can move between the two forms without changing anything downstream. A project names exactly one contract file in its config. +TypeScript and [PSL](/orm/contract-authoring/psl-syntax) authoring produce the same `contract.json` and `contract.d.ts` for an equivalent contract, so you can move between the two forms without changing anything downstream. A project names exactly one contract file in its config. -No command converts a `.prisma` contract into TypeScript. To move an existing project, write the TypeScript file by hand, point `contract` in `prisma.config.ts` at it, and delete the `.prisma` file so the two can never disagree. +No command converts a `.prisma` contract into TypeScript, so to move an existing project, write the TypeScript file by hand, point `contract` in `prisma.config.ts` at it, and delete the `.prisma` file so the two can never disagree. ## Prompt your coding agent -Projects created with `npm create prisma@latest` include the [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8) for your coding agent. In an existing project, run `npx prisma skills sync`. The `prisma-8` skill covers TypeScript authoring. Ask your agent to: +Projects created with `npm create prisma@latest` include the [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8) for your coding agent. In an existing project, run `npx prisma skills sync` to add them. The `prisma-8` skill covers TypeScript authoring, so ask your agent to: - "Convert this contract.prisma to the TypeScript schema builder." - "Using the prisma-8 skill, add a unique constraint to the email field in our TypeScript schema." diff --git a/apps/docs/content/docs/orm/core-concepts.mdx b/apps/docs/content/docs/orm/core-concepts.mdx index 09a055a4607..e4a6290357c 100644 --- a/apps/docs/content/docs/orm/core-concepts.mdx +++ b/apps/docs/content/docs/orm/core-concepts.mdx @@ -54,9 +54,9 @@ npx prisma@latest contract emit 1. `contract.json`: a canonical JSON description of your models, storage layout, and required capabilities. 2. `contract.d.ts`: the TypeScript types derived from it, which is what makes your queries type-safe. -Every other part of the toolchain reads these artifacts, not your source file. The query APIs read `contract.d.ts` for types. The migration planner diffs two `contract.json` files. The runtime verifies `contract.json` against the database. That is why `contract emit` comes first in almost every workflow: after any contract change, emit before you plan, migrate, or run. +Every other part of the toolchain reads these artifacts, not your source file: the query APIs read `contract.d.ts` for types, the migration planner diffs two `contract.json` files, and the runtime verifies `contract.json` against the database. That is why `contract emit` comes first in almost every workflow: after any contract change, emit before you plan, migrate, or run. -Emission is deterministic. The same source always produces byte-identical artifacts, so both files are committed to version control and diff cleanly in code review. Think of the pair like `package.json` and `package-lock.json`: the source is what you ask for, the artifacts are the exact resolved result. +Emission is deterministic: the same source always produces byte-identical artifacts, so both files are committed to version control and diff cleanly in code review. Think of the pair like `package.json` and `package-lock.json`: the source is what you ask for, the artifacts are the exact resolved result. See [contract.json and contract.d.ts](/orm/contract-authoring/the-contract-artifact) for what is inside each file. diff --git a/apps/docs/content/docs/orm/data-modeling/index.mdx b/apps/docs/content/docs/orm/data-modeling/index.mdx index 0c61b72d6f0..0a8be0c8ca8 100644 --- a/apps/docs/content/docs/orm/data-modeling/index.mdx +++ b/apps/docs/content/docs/orm/data-modeling/index.mdx @@ -235,7 +235,7 @@ How each kind is stored, and which model should hold the link field, is where re ## Prompt your coding agent -Projects created with `npm create prisma@latest` include the [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8) for your coding agent, instruction files that tell it how Prisma ORM 8 works. In an existing project, run `npx prisma skills sync`. Prompts that map to each section: +Projects created with `npm create prisma@latest` include the [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8) for your coding agent, instruction files that tell it how Prisma ORM 8 works. In an existing project, run `npx prisma skills sync` to add them, then try prompts that map to each section: - "Using the prisma-8 skill, add a Product model with a surrogate id and a unique sku field." - "Add a Country reference table keyed by its ISO code." diff --git a/apps/docs/content/docs/orm/data-modeling/mongodb.mdx b/apps/docs/content/docs/orm/data-modeling/mongodb.mdx index a144e445d69..28a62020d65 100644 --- a/apps/docs/content/docs/orm/data-modeling/mongodb.mdx +++ b/apps/docs/content/docs/orm/data-modeling/mongodb.mdx @@ -161,7 +161,7 @@ Use one collection when you almost always query the variants together, for examp ## Prompt your coding agent -Projects created with `npm create prisma@latest` include the [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8), instruction files for coding agents. In an existing project, run `npx prisma skills sync`. The `prisma-8` skill covers document modeling. Prompts that map to each section: +Projects created with `npm create prisma@latest` include the [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8), instruction files for coding agents. In an existing project, run `npx prisma skills sync` to add them. The `prisma-8` skill covers document modeling, so try prompts that map to each section: - "Using the prisma-8 skill, add an embedded Address type to the User model." - "Cart items are always read with the cart. Model them as an embedded list." diff --git a/apps/docs/content/docs/orm/data-modeling/relational-databases.mdx b/apps/docs/content/docs/orm/data-modeling/relational-databases.mdx index c508648a70f..bcc70118349 100644 --- a/apps/docs/content/docs/orm/data-modeling/relational-databases.mdx +++ b/apps/docs/content/docs/orm/data-modeling/relational-databases.mdx @@ -200,7 +200,7 @@ Start with the shared table, and give a variant its own table only when its colu ## Prompt your coding agent -Projects created with `npm create prisma@latest` include the [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8) for your coding agent. In an existing project, run `npx prisma skills sync`. The `prisma-8` skill covers relation modeling. Prompts that map to each section: +Projects created with `npm create prisma@latest` include the [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8) for your coding agent. In an existing project, run `npx prisma skills sync` to add them. The `prisma-8` skill covers relation modeling, so try prompts that map to each section: - "Using the prisma-8 skill, add a one-to-many between User and Post with the foreign key on Post." - "Make the Profile relation one-to-one by adding a unique constraint to its foreign key." diff --git a/apps/docs/content/docs/orm/extensions/using-extensions.mdx b/apps/docs/content/docs/orm/extensions/using-extensions.mdx index c7281d4c359..2e7f7e56927 100644 --- a/apps/docs/content/docs/orm/extensions/using-extensions.mdx +++ b/apps/docs/content/docs/orm/extensions/using-extensions.mdx @@ -57,7 +57,7 @@ export const db = postgres({ ## 4. Use the new type in your schema -The extension's types are now available in your contract. Declare a vector column with an explicit dimension: +The extension's types are now available in your contract, so declare a vector column with an explicit dimension: ```prisma title="src/prisma/contract.prisma" types { @@ -96,7 +96,7 @@ One package, two registrations, one database: ## Capabilities -Each extension names what it adds under a key like `pgvector.cosine`. You never write these keys by hand. Registering the extension in the config records them in your contract. Registering it in `db.ts` provides them at runtime. +Each extension names what it adds under a key like `pgvector.cosine`, but you never write these keys by hand. Registering the extension in the config records them in your contract, and registering it in `db.ts` provides them at runtime. The point of this bookkeeping is to fail early: @@ -127,7 +127,7 @@ The database packages are extensions too: `@prisma/orm-postgres`, `@prisma/orm-s :::note[Supabase does not follow the recipe above] -Supabase ships no `/control` subpath. Import its pack from `@prisma/orm-extension-supabase/pack` instead and pass it to the same `ormConfig({ extensions: [supabasePack] })` call the recipe above uses. The client is where the wiring differs: build it with the `supabase()` factory from `@prisma/orm-extension-supabase/runtime` rather than `postgres()`. That factory is role-first (`asUser(jwt)`, `asAnon()`, `asServiceRole()`) and carries the JWT validation and RLS binding. See the [runnable example](https://github.com/prisma/orm/tree/main/examples/supabase) for the whole wiring. +Supabase ships no `/control` subpath, so import its pack from `@prisma/orm-extension-supabase/pack` instead and pass it to the same `ormConfig({ extensions: [supabasePack] })` call the recipe above uses. The client is where the wiring differs: build it with the `supabase()` factory from `@prisma/orm-extension-supabase/runtime` rather than `postgres()`. That factory is role-first (`asUser(jwt)`, `asAnon()`, `asServiceRole()`) and carries the JWT validation and RLS binding. See the [runnable example](https://github.com/prisma/orm/tree/main/examples/supabase) for the whole wiring. ::: diff --git a/apps/docs/content/docs/orm/fundamentals/advanced-queries.mdx b/apps/docs/content/docs/orm/fundamentals/advanced-queries.mdx index dbea0b6d249..f9feb755d06 100644 --- a/apps/docs/content/docs/orm/fundamentals/advanced-queries.mdx +++ b/apps/docs/content/docs/orm/fundamentals/advanced-queries.mdx @@ -6,13 +6,13 @@ metaTitle: Advanced queries in Prisma ORM metaDescription: 'Use the Prisma ORM SQL query builder for explicit joins, grouped aggregates, and RETURNING, and the typed aggregation pipeline builder on MongoDB.' --- -Most queries fit the ORM API. When one doesn't, there is a query builder. Prisma ORM 8 supports PostgreSQL, SQLite, and MongoDB; the SQL query builder is for PostgreSQL and SQLite, the pipeline builder for MongoDB. Neither builder asks you to write SQL as a string. When the builder cannot express a query, `db.raw.sql` lets you write one, with values still sent as parameters. Both builders come with `@prisma/orm-postgres` and `@prisma/orm-mongo`, so there is nothing extra to install. +Most queries fit the ORM API, and when one doesn't, there is a query builder. Prisma ORM 8 supports PostgreSQL, SQLite, and MongoDB; the SQL query builder is for PostgreSQL and SQLite, the pipeline builder for MongoDB. Neither builder asks you to write SQL as a string. When the builder cannot express a query, `db.raw.sql` lets you write one, with values still sent as parameters. Both builders come with `@prisma/orm-postgres` and `@prisma/orm-mongo`, so there is nothing extra to install. -The schema file is now `contract.prisma`; it is the same schema language as Prisma ORM 7 (with the type changes on [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7#schema)). The docs call it your contract. TypeScript checks both builders against it. +The schema file is now `contract.prisma`; it is the same schema language as Prisma ORM 7 (with the type changes on [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7#schema)). The docs call it your contract, and TypeScript checks both builders against it. -You choose per query, not per app. Using the ORM API everywhere and a builder in the three places that need it is normal. +You choose per query, not per app, so using the ORM API everywhere and a builder in the three places that need it is normal. -Every example here imports `db`. `db` is the client you create once in `src/prisma/db.ts`, and `prisma orm init` writes that file for you. The examples run from a file directly inside `src/`. +Every example here imports `db`, the client you create once in `src/prisma/db.ts`, and `prisma orm init` writes that file for you. The examples run from a file directly inside `src/`. Every example on this page queries these four models: @@ -55,7 +55,7 @@ In Prisma ORM 8 a many-to-many relation is the two list fields plus a model for ## PostgreSQL: SQL query builder -A SQL builder query takes two steps. First you build it, then you run it. You decide the exact SQL: the joins, the grouping, and the columns that come back. +A SQL builder query takes two steps: first you build it, then you run it. You decide the exact SQL: the joins, the grouping, and the columns that come back. **Use it when:** @@ -68,7 +68,7 @@ A SQL builder query takes two steps. First you build it, then you run it. You de ### Build a query and run it -Start from a table with `db.sql.public.`. `db.sql` is the SQL query builder, and it is keyed by table name. A table's name is the model name with a lowercase first letter, unless the model sets `@@map`. Column names are the field names, unless a field sets `@map`. `public` is the PostgreSQL schema your tables are in, unless the model sits in a `namespace billing { ... }` block in your contract, in which case the path is `db.sql.billing.
`. +Start from a table with `db.sql.public.
`. `db.sql` is the SQL query builder, keyed by table name. A table's name is the model name with a lowercase first letter unless the model sets `@@map`, and column names are the field names unless a field sets `@map`. `public` is the PostgreSQL schema your tables are in, unless the model sits in a `namespace billing { ... }` block in your contract, in which case the path is `db.sql.billing.
`. `db.runtime()` is the database connection; call it whenever you run a query. `query()` always resolves to an array of rows, even when the query matches one row. @@ -86,7 +86,7 @@ const publishedPosts = db.sql.public.post const rows = await db.runtime().query(publishedPosts); ``` -The `.where(...)` callback receives two arguments. The first, `f`, holds the column references. The second, `fns`, holds the operators: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `notIn`, `and`, `or`, and `ilike` for text columns on PostgreSQL. There are more; the [SQL query builder reference](/orm/reference/sql-query-builder#built-in-functions) has the full list. +The `.where(...)` callback receives two arguments. The first, `f`, holds the column references, and the second, `fns`, holds the operators: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `notIn`, `and`, `or`, and `ilike` for text columns on PostgreSQL. There are more; the [SQL query builder reference](/orm/reference/sql-query-builder#built-in-functions) has the full list. Pass a value from your own code straight into an operator. Here `authorId` is a variable, such as one you read from a request: @@ -110,7 +110,7 @@ function titles(posts: PublishedPost[]) { } ``` -`ResultType<...>` is one row, not the array. `await db.runtime().query(publishedPosts)` resolves to `PublishedPost[]`. See [`ResultType`](/orm/reference/sql-query-builder#resulttype) in the reference. +`ResultType<...>` is one row, not the array, so `await db.runtime().query(publishedPosts)` resolves to `PublishedPost[]`. See [`ResultType`](/orm/reference/sql-query-builder#resulttype) in the reference. ### Join tables with precise control @@ -177,7 +177,7 @@ const rows = await db.runtime().query(topAuthors); Pass the same aggregate to `.orderBy(...)` that you passed to `.select(...)`, and the database does the sorting and the limiting. -Use `fns.countBigInt()` when a count can grow past `Number.MAX_SAFE_INTEGER`. It returns a `bigint`. `fns.count()` returns a JavaScript `number`. A count larger than `Number.MAX_SAFE_INTEGER` throws an error with code `RUNTIME.DECODE_FAILED` rather than rounding. +Use `fns.countBigInt()` when a count can grow past `Number.MAX_SAFE_INTEGER`, because it returns a `bigint` while `fns.count()` returns a JavaScript `number`. A count larger than `Number.MAX_SAFE_INTEGER` throws an error with code `RUNTIME.DECODE_FAILED` rather than rounding. ### Write with RETURNING @@ -210,7 +210,7 @@ Neither of those two returns rows, so both go to `execute()`. Add `.returning(.. ### Raw SQL -Raw SQL comes in two sizes. When the operators don't cover a single expression you need, write that expression with `fns.raw` and declare what type it returns with `.returns(...)`. The rest of the query stays typed. +Raw SQL comes in two sizes. When the operators don't cover a single expression you need, write that expression with `fns.raw` and declare what type it returns with `.returns(...)`, and the rest of the query stays typed. Calling `.select` a second time adds columns rather than replacing the ones already chosen. The second call takes a name for the new column, then the expression: @@ -224,9 +224,9 @@ const rows = await db.runtime().query(users); // [{ id: 'cuid20000000000000000001', email: 'alice@prisma.io', upperEmail: 'ALICE@PRISMA.IO' }, ...] ``` -`.returns(...)` takes a type name as a string. `"pg/text@1"` is the PostgreSQL type `text`. Type names always end in `@1`. The [raw queries reference](/orm/reference/raw-queries) covers how a bare value picks its type name. +`.returns(...)` takes a type name as a string: `"pg/text@1"` is the PostgreSQL type `text`, and type names always end in `@1`. The [raw queries reference](/orm/reference/raw-queries) covers how a bare value picks its type name. -A raw fragment is as safe as an operator. You can interpolate columns and other typed expressions the same way. +A raw fragment is as safe as an operator, and you can interpolate columns and other typed expressions the same way. When the whole statement has to be raw, write it with `db.raw.sql` and end it with one of two calls. Use `.returnsRow(...)` when the statement returns rows, then run the built query with `query()`: @@ -249,7 +249,7 @@ const rows = await db.runtime().query(authorPostCounts); Quote identifiers as PostgreSQL requires; `"authorId"` keeps the camel case. Each entry in the `.returnsRow(...)` object says how to read one result column. An entry is either a column from the contract, as in `user.columns.id`, or a type name as a string for a result column the contract has no counterpart for. `"pg/int8@1"` is a 64-bit integer, so `postCount` comes back as a `bigint`. -Use `.affectedCount()` when the statement returns no rows, then run the built query with `execute()`. It resolves to `{ affectedRows }`: +Use `.affectedCount()` when the statement returns no rows, then run the built query with `execute()`, which resolves to `{ affectedRows }`: ```typescript const publishDrafts = db.raw.sql` @@ -293,7 +293,7 @@ A column the inner statement already declared can be reused in the outer row obj On MongoDB the builder is at `db.query`, and it builds a typed MongoDB aggregation pipeline: a sequence of stages such as `$match`, `$group`, `$sort`, and `$lookup`, checked against your contract. There is no `db.sql` on MongoDB. -All MongoDB aggregation happens here. The ORM API has no `.aggregate(...)` on MongoDB, so counts, sums, and grouping are pipeline builder work. +All MongoDB aggregation happens here: the ORM API has no `.aggregate(...)` on MongoDB, so counts, sums, and grouping are pipeline builder work. **Use it when:** @@ -302,11 +302,11 @@ All MongoDB aggregation happens here. The ORM API has no `.aggregate(...)` on Mo - A query needs MongoDB pipeline stages that don't map to the ORM API, such as multi-stage filtering and projection. - You want to filter with an operator such as `gt` or `ne`. On MongoDB the ORM API's `.where(...)` matches exact values only, and `.match(...)` here takes the operators. -**Prefer the ORM API when** the query is document create, read, update, or delete. Prefer it as well for reading a reference relation. A reference relation is a relation stored as an id in the document rather than as an embedded document, and `.include(...)` already covers the common `$lookup` case. +**Prefer the ORM API when** the query is document create, read, update, or delete, and for reading a reference relation. A reference relation is a relation stored as an id in the document rather than as an embedded document, and `.include(...)` already covers the common `$lookup` case. ### Build a pipeline and run it -Start from a collection with `db.query.from(...)`. It takes the collection name as a string: the model's `@@map` value, or the model name with a lowercase first letter when the model has no `@@map`. The models above set no `@@map` except `PostTag`, so the collections are `user`, `post`, `tag`, and `post_tag`. That is the same name the ORM API uses, as in `db.orm.post`. +Start from a collection with `db.query.from(...)`, which takes the collection name as a string: the model's `@@map` value, or the model name with a lowercase first letter when the model has no `@@map`. The models above set no `@@map` except `PostTag`, so the collections are `user`, `post`, `tag`, and `post_tag`. That is the same name the ORM API uses, as in `db.orm.post`. `acc` holds the aggregate functions you can use inside `.group(...)`, such as `acc.count()` and `acc.max(f.createdAt)`. In the pipeline builder, fields use their stored names, so the id is `_id` (see the note under the contract above), and `_id` is also the grouping key in `.group(...)`. On MongoDB `db.runtime()` returns a promise, so await it before you run anything. Chain stages, call `.build()`, and run the result: @@ -348,7 +348,7 @@ const draftsByAuthor = db.query const rows = await runtime.query(draftsByAuthor); ``` -In a `.match(...)` callback the operators are on the field itself, as in `f.published.eq(false)`. The SQL query builder puts them on `fns` instead. The [pipeline builder reference](/orm/reference/pipeline-builder#match) lists the operators you can use here. +In a `.match(...)` callback the operators are on the field itself, as in `f.published.eq(false)`, while the SQL query builder puts them on `fns` instead. The [pipeline builder reference](/orm/reference/pipeline-builder#match) lists the operators you can use here. ### Join collections with $lookup @@ -387,7 +387,7 @@ Inside a transaction, `tx` is the argument your callback receives, and you run t ## Prompt your coding agent -Projects created with `npm create prisma@latest` include the [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8) for your coding agent. The `prisma-8` skill covers both builders and the choice between them and the ORM API. Prompts that map to each section: +Projects created with `npm create prisma@latest` include the [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8) for your coding agent. The `prisma-8` skill covers both builders and the choice between them and the ORM API, so try prompts that map to each section: - "Using the prisma-8 skill, write a SQL builder query for the top 10 authors by post count." - "This report needs post and author columns in one flat result. Build the join with the SQL query builder." diff --git a/apps/docs/content/docs/orm/fundamentals/reading-data.mdx b/apps/docs/content/docs/orm/fundamentals/reading-data.mdx index 7475e0d015b..9f783d4c475 100644 --- a/apps/docs/content/docs/orm/fundamentals/reading-data.mdx +++ b/apps/docs/content/docs/orm/fundamentals/reading-data.mdx @@ -8,7 +8,7 @@ metaDescription: 'Query PostgreSQL and MongoDB with Prisma ORM. Filter with wher This page shows how to read data with Prisma ORM: fetching [many records or one](#fetch-many-records-or-one), [filtering](#filter-records), [selecting fields](#select-fields), [sorting and paginating](#sort-and-paginate), [counting](#count-records), and [iterating large results](#iterate-a-large-result). -Every query chains methods on a model. The last call in the chain says what you want back and runs the query. Usually that is `.all()` or `.first()`: +Every query chains methods on a model, and the last call in the chain says what you want back and runs the query, usually `.all()` or `.first()`: ```typescript tab="PostgreSQL" import { db } from "./prisma/db"; @@ -30,15 +30,15 @@ const posts = await db.orm.posts.where({ published: true }).all(); const user = await db.orm.users.where({ email: "alice@prisma.io" }).first(); ``` -`npm create prisma@latest` scaffolds a new project. `npx prisma orm init` adds Prisma ORM to a project you already have. +`npm create prisma@latest` scaffolds a new project, while `npx prisma orm init` adds Prisma ORM to a project you already have. -Your contract is the `contract.prisma` file that replaced `schema.prisma`. It is where you declare your models, and every result on this page is typed from it. See [Core concepts](/orm/core-concepts) for how the contract and the client fit together. +Your contract is the `contract.prisma` file that replaced `schema.prisma`: it is where you declare your models, and every result on this page is typed from it. See [Core concepts](/orm/core-concepts) for how the contract and the client fit together. -`db` is the client you create once in `src/prisma/db.ts`. `npx prisma orm init` writes that file for you. Commit it; unlike Prisma ORM 7's generated client, it is your code. `db.ts` reads two generated files that `npx prisma contract emit` writes next to it; you do not edit those. These examples are written from a file in `src/`, so the import is `./prisma/db`; from elsewhere, change the relative path. +`db` is the client you create once in `src/prisma/db.ts`, and `npx prisma orm init` writes that file for you. Commit it; unlike Prisma ORM 7's generated client, it is your code. `db.ts` reads two generated files that `npx prisma contract emit` writes next to it; you do not edit those. These examples are written from a file in `src/`, so the import is `./prisma/db`; from elsewhere, change the relative path. `db.orm` holds your models. On PostgreSQL you reach a model through the database schema it is in: in `db.orm.public.User`, `User` is the model name and `public` is the PostgreSQL schema. It is `public` unless the model sits in a `namespace` block in your contract; see [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7#schema). On MongoDB there is no schema segment, and the name is the collection: `db.orm.users`. -You can write `.where`, `.select`, `.orderBy`, `.limit`, and `.offset` in any order. `.cursor(...)` must come after `.orderBy(...)`. +You can write `.where`, `.select`, `.orderBy`, `.limit`, and `.offset` in any order, but `.cursor(...)` must come after `.orderBy(...)`. For Prisma ORM 7 users, `findMany` and `findFirst` / `findUnique` map directly onto `.all()` and `.first()` ([Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7) maps the rest of the API): @@ -126,9 +126,9 @@ const users = await db.orm.public.User.all(); ] ``` -`.all()` takes no filter of its own. Put the filter in `.where(...)` before it. `.all()` also applies no limit, so combine it with [`limit`](#sort-and-paginate) on tables that can grow. +`.all()` takes no filter of its own, so put the filter in `.where(...)` before it. `.all()` also applies no limit, so combine it with [`limit`](#sort-and-paginate) on tables that can grow. -Use `.first()` when you want a single record. It returns the record, or `null` when nothing matches. On both databases it asks for at most one row: +Use `.first()` when you want a single record. It returns the record, or `null` when nothing matches, and on both databases it asks for at most one row: ```typescript const user = await db.orm.public.User.where({ email: "alice@prisma.io" }).first(); @@ -138,7 +138,7 @@ const user = await db.orm.public.User.where({ email: "alice@prisma.io" }).first( { id: 'cuid20000000000000000001', email: 'alice@prisma.io', name: 'Alice', createdAt: 2026-07-06T09:03:13.808Z } ``` -On PostgreSQL you can skip `.where(...)` and pass the filter straight to `.first(...)`. It takes anything `.where(...)` takes, not just the primary key. On MongoDB, `.first()` takes no argument, so filter with `.where(...)`: +On PostgreSQL you can skip `.where(...)` and pass the filter straight to `.first(...)`, which takes anything `.where(...)` takes, not just the primary key. On MongoDB, `.first()` takes no argument, so filter with `.where(...)`: ```typescript tab="PostgreSQL" const user = await db.orm.public.User.first({ id: userId }); @@ -148,23 +148,23 @@ const user = await db.orm.public.User.first({ id: userId }); const user = await db.orm.users.where({ _id: userId }).first(); ``` -If you want an error instead of `null` when nothing matches, call `.firstOrThrow()`. This is what replaces `findUniqueOrThrow` and `findFirstOrThrow`: +If you want an error instead of `null` when nothing matches, call `.firstOrThrow()`, the replacement for `findUniqueOrThrow` and `findFirstOrThrow`: ```typescript const user = await db.orm.public.User.where({ email }).limit(1).all().firstOrThrow(); ``` -`.limit(1)` keeps it to one row. `.firstOrThrow()` throws an error with code `RUNTIME.NO_ROWS` when nothing matches. There is no `.firstOrThrow()` on `.first()`. The error is an `Error` with a `code` property, so check `"code" in error` before you read it. The same form works on MongoDB, with `db.orm.users`. +`.limit(1)` keeps it to one row, and `.firstOrThrow()` throws an error with code `RUNTIME.NO_ROWS` when nothing matches. There is no `.firstOrThrow()` on `.first()`. The error is an `Error` with a `code` property, so check `"code" in error` before you read it. The same form works on MongoDB, with `db.orm.users`. ## Filter records -Use `.where(...)` to narrow a query. Pass an object to match fields by equality: +Use `.where(...)` to narrow a query, and pass an object to match fields by equality: ```typescript const drafts = await db.orm.public.Post.where({ published: false }).all(); ``` -Chain several `.where(...)` calls to combine conditions with AND. This is also how you express a range: +Chain several `.where(...)` calls to combine conditions with AND, which is also how you express a range: ```typescript const recentPosts = await db.orm.public.Post @@ -209,13 +209,13 @@ const notHello = await db.orm.public.Post ### Filter operators on MongoDB -On MongoDB, `.where(...)` does not take a callback. The object form covers equality: +On MongoDB, `.where(...)` does not take a callback, but the object form covers equality: ```typescript const drafts = await db.orm.posts.where({ published: false }).all(); ``` -For anything else, pass `.where(...)` a filter built with `MongoFieldFilter`, imported from `@prisma/orm-mongo/query-ast/execution`. `@prisma/orm-mongo` is the package `db.ts` imports from. `MongoFieldFilter` has one static method per operator: `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `nin` (not in), `isNull`, and `isNotNull`. Each one takes the field name first: +For anything else, pass `.where(...)` a filter built with `MongoFieldFilter`, imported from `@prisma/orm-mongo/query-ast/execution`. `@prisma/orm-mongo` is the package `db.ts` imports from. `MongoFieldFilter` has one static method per operator: `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `nin` (not in), `isNull`, and `isNotNull`, and each one takes the field name first: ```typescript import { MongoFieldFilter } from "@prisma/orm-mongo/query-ast/execution"; @@ -251,7 +251,7 @@ The full operator list is in [Filter conditions and operators](/orm/reference/or ## Select fields -Use `.select(...)` to fetch only the fields you need. On PostgreSQL the result type narrows to match. On MongoDB only the fetched document narrows; the type keeps every field, so a field you left out is `undefined` at runtime. On both, you can filter on fields you did not select: +Use `.select(...)` to fetch only the fields you need. On PostgreSQL the result type narrows to match, while on MongoDB only the fetched document narrows; the type keeps every field, so a field you left out is `undefined` at runtime. On both, you can filter on fields you did not select: ```typescript tab="PostgreSQL" const users = await db.orm.public.User.select("id", "email").all(); @@ -272,7 +272,7 @@ const users = await db.orm.users.select("_id", "email").all(); Use `.orderBy(...)` to sort, `.limit(n)` to limit, and `.offset(n)` to offset. -On PostgreSQL, sort with a callback that calls `.asc()` or `.desc()` on a field. On MongoDB, sort with MongoDB's own direction numbers: `1` for ascending and `-1` for descending. +On PostgreSQL, sort with a callback that calls `.asc()` or `.desc()` on a field, and on MongoDB, sort with MongoDB's own direction numbers: `1` for ascending and `-1` for descending. ```typescript tab="PostgreSQL" // Second page of posts, newest first @@ -292,7 +292,7 @@ const page = await db.orm.posts .all(); ``` -For a composite sort on PostgreSQL, pass an array of callbacks. Records are sorted by the first field, with the second as tiebreaker: +For a composite sort on PostgreSQL, pass an array of callbacks, and records are sorted by the first field, with the second as tiebreaker: ```typescript const posts = await db.orm.public.Post @@ -302,11 +302,11 @@ const posts = await db.orm.public.Post ### Cursor pagination -Offset gets slower on deep pages. For stable pagination over a large table, follow `.orderBy(...)` with `.cursor(...)` and resume from the last record you returned. `.cursor(...)` is PostgreSQL only. +Offset gets slower on deep pages, so for stable pagination over a large table, follow `.orderBy(...)` with `.cursor(...)` and resume from the last record you returned. `.cursor(...)` is PostgreSQL only. -The cursor record is excluded from the next page. Pages pick up strictly after it. +The cursor record is excluded from the next page, so pages pick up strictly after it. -Pass `.cursor(...)` a value for every field you sorted by, as in the example below. Keep the `id` tiebreaker in both the sort and the cursor. `createdAt` is not unique, and a cursor on a non-unique field alone can skip or repeat records that share the boundary value. With `id` in the cursor, pages never overlap even when timestamps tie: +Pass `.cursor(...)` a value for every field you sorted by, as in the example below. Keep the `id` tiebreaker in both the sort and the cursor, because `createdAt` is not unique, and a cursor on a non-unique field alone can skip or repeat records that share the boundary value. With `id` in the cursor, pages never overlap even when timestamps tie: ```typescript const page1 = await db.orm.public.Post @@ -370,7 +370,7 @@ const [result] = await (await db.runtime()).query(built); You can `await` a read for an array, or loop over it with `for await`. Pick one; once you iterate a result with `for await` it is used up (see below). -`await` runs the query and gives you an array. This is the right default. You get the whole result in memory and can read the array as often as you like. +`await` runs the query and gives you an array, which is the right default: you get the whole result in memory and can read the array as often as you like. ```typescript const posts = await db.orm.public.Post.all(); @@ -434,7 +434,7 @@ const users = await db.orm.public.User.where({ email }).all(); const user = users[0]; ``` -This fetches every matching record and throws away the rest. Use `.first()` instead. It returns one record or `null`, and it asks the database for at most one row: +This fetches every matching record and throws away the rest. Use `.first()` instead: it returns one record or `null`, and it asks the database for at most one row: ```typescript const user = await db.orm.public.User.where({ email }).first(); @@ -442,7 +442,7 @@ const user = await db.orm.public.User.where({ email }).first(); ### Forgetting that .all() has no limit -`.all()` returns every match. On a table that grows, yesterday's fast query becomes today's slow one. Add `.limit(n)` when you don't genuinely need every record. When you do need every record, page through the table: `.cursor(...)` on PostgreSQL, `.offset(n)` on MongoDB. +`.all()` returns every match, so on a table that grows, yesterday's fast query becomes today's slow one. Add `.limit(n)` when you don't genuinely need every record, and when you do need every record, page through the table: `.cursor(...)` on PostgreSQL, `.offset(n)` on MongoDB. ### Reusing an iterated result @@ -455,7 +455,7 @@ const posts = await db.orm.public.Post.all(); ## Prompt your coding agent -Projects created with `npm create prisma@latest` include the Prisma ORM skills for your coding agent; in an existing project, run `npx prisma skills sync`. Skills are instruction files a coding agent reads, and the [`prisma-8` skill](/ai/tools/skills#available-skills-for-prisma-8) covers everything on this page. Prompts that map to each section: +Projects created with `npm create prisma@latest` include the Prisma ORM skills for your coding agent, and in an existing project you run `npx prisma skills sync` to add them. Skills are instruction files a coding agent reads, and the [`prisma-8` skill](/ai/tools/skills#available-skills-for-prisma-8) covers everything on this page, so try prompts that map to each section: - "Using the prisma-8 skill, write a query that returns the 20 newest published posts." - "Add a case-insensitive title search to the posts query, using the .ilike operator." diff --git a/apps/docs/content/docs/orm/fundamentals/relations-and-joins.mdx b/apps/docs/content/docs/orm/fundamentals/relations-and-joins.mdx index 40e1692b115..867b31cdcc1 100644 --- a/apps/docs/content/docs/orm/fundamentals/relations-and-joins.mdx +++ b/apps/docs/content/docs/orm/fundamentals/relations-and-joins.mdx @@ -28,7 +28,7 @@ const posts = await db.orm.posts // posts[0].author is the referenced user document ``` -If this is your first Prisma ORM 8 query, start with the two files it needs. Your models live in `src/prisma/contract.prisma`. It is the same file Prisma ORM 7 called `schema.prisma`; the docs call it your contract. Both commands below write `src/prisma/db.ts`, which creates the client and exports it as `db`. It reads `DATABASE_URL` from `.env`. +If this is your first Prisma ORM 8 query, start with the two files it needs. Your models live in `src/prisma/contract.prisma`, the same file Prisma ORM 7 called `schema.prisma`; the docs call it your contract. Both commands below write `src/prisma/db.ts`, which creates the client and exports it as `db`, and it reads `DATABASE_URL` from `.env`. ```bash npm create prisma@latest -- my-app # a new project @@ -42,15 +42,15 @@ That is the file the examples above import as `./prisma/db`, from a file directl - You `await` the whole chain, and the last call says what you want back. `.all()` returns every matching record as an array. `.first()` returns one record, or `null` when nothing matches. - `.where(...)` takes two forms. Pass an object, like `.where({ published: true })`, to match fields to exact values. Pass a callback for a comparison, like `.where((p) => p.title.like("Hello%"))`. Chaining `.where(...)` twice requires both to match. -The name you pass to `.include(...)` is the relation field name you declared in your contract. It is not a table name. `.include(...)` fetches the relation in the same query as the parent: one SQL statement, which reads the relation with a correlated subquery. `.include("author")` replaces Prisma ORM 7's `include: { author: true }`; see [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7#queries) for the rest of the query API side by side. Use `.include(...)` when the caller needs the related data in the same response. Skip it when the foreign key already on the record is enough. This page walks through the three kinds of relationship, from the data model to the query and the result. If you already know how relational data is modeled, jump to [filtering by relation data](#filter-parent-records-by-relation-data) or the [limitations](#current-limitations). +The name you pass to `.include(...)` is the relation field name you declared in your contract, not a table name. `.include(...)` fetches the relation in the same query as the parent: one SQL statement, which reads the relation with a correlated subquery. `.include("author")` replaces Prisma ORM 7's `include: { author: true }`; see [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7#queries) for the rest of the query API side by side. Use `.include(...)` when the caller needs the related data in the same response, and skip it when the foreign key already on the record is enough. This page walks through the three kinds of relationship, from the data model to the query and the result. If you already know how relational data is modeled, jump to [filtering by relation data](#filter-parent-records-by-relation-data) or the [limitations](#current-limitations). ## One-to-one -One record is linked to at most one other record. For example, every profile belongs to exactly one user. +One record is linked to at most one other record: for example, every profile belongs to exactly one user. -The model that holds the foreign key declares the relation. The `@unique` on the foreign key is what makes it one-to-one: +The model that holds the foreign key declares the relation, and the `@unique` on the foreign key is what makes it one-to-one: ```prisma model Profile { @@ -71,7 +71,7 @@ const profileWithUser = await db.orm.public.Profile // { id, bio, userId, user: { id, email } } ``` -`.first()` returns `null` when the profile doesn't exist, so a user without a profile is a `null` check, not an error. You can also declare the matching field on the other side and start from users. An optional field typed as the other model is enough. It needs no `@relation` of its own: +`.first()` returns `null` when the profile doesn't exist, so a user without a profile is a `null` check, not an error. You can also declare the matching field on the other side and start from users: an optional field typed as the other model is enough, and it needs no `@relation` of its own: ```prisma model User { @@ -81,7 +81,7 @@ model User { } ``` -This works as long as the foreign key on the other side is unique. So `Profile.userId` must carry `@unique`. Without it, `npx prisma contract emit` fails with the error code `PSL_NON_UNIQUE_BACKRELATION`. +This works as long as the foreign key on the other side is unique, so `Profile.userId` must carry `@unique`. Without it, `npx prisma contract emit` fails with the error code `PSL_NON_UNIQUE_BACKRELATION`. Then run `npx prisma contract emit`, the command that regenerates the client types from your contract. Run it after every change you make to the contract. Then apply the change to the database with `npx prisma db update` (the replacement for `prisma db push`) during development, or with a migration: run `npx prisma migration plan` then `npx prisma db migrate`; see [Generating a migration](/orm/migrations/generating-a-migration). With the field in place, `include("profile")` returns one record or `null`: @@ -93,7 +93,7 @@ const usersWithProfiles = await db.orm.public.User // Array<{ id, email, profile: { id, bio, userId } | null }> ``` -`.select(...)` narrows the parent's own fields only. An included relation still comes back with all of its fields, which is why `profile` above has `bio` and `userId`. To narrow the included relation's fields, use the callback form shown in the next section. +`.select(...)` narrows the parent's own fields only, while an included relation still comes back with all of its fields, which is why `profile` above has `bio` and `userId`. To narrow the included relation's fields, use the callback form shown in the next section. ## One-to-many @@ -120,7 +120,7 @@ model Post { } ``` -Query it in either direction. From the parent, the children arrive as an array. From the child, the parent arrives as one object: +Query it in either direction: from the parent, the children arrive as an array, and from the child, the parent arrives as one object: ```typescript // Each user with their posts @@ -132,7 +132,7 @@ const postsWithAuthors = await db.orm.public.Post.include("author").all(); // Array<{ id, title, published, createdAt, authorId, author: User }> ``` -To choose what each relation returns, pass a callback as the second argument. Inside it, chain `.where`, `.select`, `.orderBy`, and `.limit` exactly like a top-level query. `.desc()` sorts newest first, and `.asc()` sorts the other way. This is how you fetch "each user with their five newest posts" in one query: +To choose what each relation returns, pass a callback as the second argument, and inside it, chain `.where`, `.select`, `.orderBy`, and `.limit` exactly like a top-level query. `.desc()` sorts newest first, and `.asc()` sorts the other way. This is how you fetch "each user with their five newest posts" in one query: ```typescript const usersWithRecentPosts = await db.orm.public.User @@ -157,7 +157,7 @@ const usersWithPostsAndComments = await db.orm.public.User // Array<{ id, email, posts: Array<{ id, title, published, createdAt, authorId, comments: Comment[] }> }> ``` -To write a parent and its children in one call, pass a callback for the list field and Prisma ORM fills in the foreign key on each child: `db.orm.public.User.create({ email: "jane@prisma.io", posts: (p) => p.create([{ title: "Hello", published: false }]) })` writes the user and the post together. The common mistake here is the N+1 loop: fetching users, then querying posts inside a `for` loop over them. That runs one query per user. One `.include("posts")` on the user query returns the same data in a single query. +To write a parent and its children in one call, pass a callback for the list field and Prisma ORM fills in the foreign key on each child: `db.orm.public.User.create({ email: "jane@prisma.io", posts: (p) => p.create([{ title: "Hello", published: false }]) })` writes the user and the post together. The common mistake here is the N+1 loop: fetching users, then querying posts inside a `for` loop over them, which runs one query per user. One `.include("posts")` on the user query returns the same data in a single query. ## Many-to-many @@ -165,7 +165,7 @@ Records on both sides connect to many on the other: a post has many tags, and a -Write three models. On each side, declare a list field typed as the other model. Then write a third model for the join table, and give it a primary key made of exactly two foreign keys, one to each side: +Write three models: on each side, declare a list field typed as the other model, then write a third model for the join table, and give it a primary key made of exactly two foreign keys, one to each side: ```prisma model Post { @@ -191,7 +191,7 @@ model PostTag { } ``` -Prisma ORM finds the model for the join table by that shape: the one model whose primary key is exactly a foreign key to `Post` and a foreign key to `Tag`. There is no other way to name it. If two models have that shape, `npx prisma contract emit` fails with `PSL_AMBIGUOUS_BACKRELATION`, and you pick one by adding the same `@relation("...")` name to the list field and to the foreign key pointing back at it. You do not add a `PostTag[]` field to `Post` or `Tag`. `.include(...)` then reads a post's tags in one query, and returns the tags themselves, not the link records. The callback that filters and narrows a relation works here like anywhere else: +Prisma ORM finds the model for the join table by that shape: the one model whose primary key is exactly a foreign key to `Post` and a foreign key to `Tag`, and there is no other way to name it. If two models have that shape, `npx prisma contract emit` fails with `PSL_AMBIGUOUS_BACKRELATION`, and you pick one by adding the same `@relation("...")` name to the list field and to the foreign key pointing back at it. You do not add a `PostTag[]` field to `Post` or `Tag`. `.include(...)` then reads a post's tags in one query, and returns the tags themselves, not the link records. The callback that filters and narrows a relation works here like anywhere else: ```typescript const postsWithTags = await db.orm.public.Post @@ -204,7 +204,7 @@ const postsWithTags = await db.orm.public.Post [{ title: 'Hello Prisma 8', tags: [{ id: 't2…', name: 'databases' }] }] ``` -Writes reach the join table the same way. Pass a callback for the relation and use `create`, `connect`, or `disconnect`: +Writes reach the join table the same way: pass a callback for the relation and use `create`, `connect`, or `disconnect`: ```typescript // Create a post and two new tags in one call @@ -226,7 +226,7 @@ await db.orm.public.Post.where({ id: post.id }).update({ `connect` and `disconnect` identify the tag by any primary key or `@unique` field, so `{ name: "typescript" }` works here because `Tag.name` is `@unique`. When that key is made of several columns, pass every one of them in the object. `update(...)` returns the updated post, or `null` when no post matched the filter. `create` always inserts a new tag record, so creating a tag whose name already exists fails on that same unique constraint. Use `connect` for tags that already exist. -There is no `connectOrCreate` in Prisma ORM 8. Upsert the tag first, which creates it or leaves an existing one alone, then connect it: +There is no `connectOrCreate` in Prisma ORM 8, so upsert the tag first, which creates it or leaves an existing one alone, then connect it: ```typescript const tag = await db.orm.public.Tag.upsert({ @@ -239,9 +239,9 @@ await db.orm.public.Post.where({ id: post.id }).update({ }); ``` -The upsert is safe when two requests create the same tag at once. The `connect` is not: if both requests also link that tag to the same post, the second fails with an error whose code is `ORM.RELATION_LINK_DUPLICATE`. Catch that code and treat it as done. +The upsert is safe when two requests create the same tag at once, but the `connect` is not: if both requests also link that tag to the same post, the second fails with an error whose code is `ORM.RELATION_LINK_DUPLICATE`. Catch that code and treat it as done. -See [what is not available yet](/orm/coming-from-prisma-orm-7#not-available-yet) for the rest of the gaps, and [Upsert a record](/orm/fundamentals/writing-data#upsert-a-record) for the top-level `.upsert(...)` call. These nested writes are for relational databases. On MongoDB, write the parent document and the child document in two calls, and set the reference field yourself: +See [what is not available yet](/orm/coming-from-prisma-orm-7#not-available-yet) for the rest of the gaps, and [Upsert a record](/orm/fundamentals/writing-data#upsert-a-record) for the top-level `.upsert(...)` call. These nested writes are for relational databases, so on MongoDB, write the parent document and the child document in two calls, and set the reference field yourself: ```typescript const user = await db.orm.users.create({ email: "jane@prisma.io" }); @@ -255,7 +255,7 @@ const links = await db.orm.public.PostTag.include("tag").all(); // Array<{ postId, tagId, tag: { id, name } }> ``` -A join table may carry columns beyond the two foreign keys. Those extra columns must not be part of the primary key. Nested `create` and `connect` are rejected when one of them is required, because neither can supply a value for it, so insert the link records yourself: +A join table may carry columns beyond the two foreign keys, but those extra columns must not be part of the primary key. Nested `create` and `connect` are rejected when one of them is required, because neither can supply a value for it, so insert the link records yourself: ```typescript const tag = await db.orm.public.Tag.first({ name: "typescript" }); @@ -264,7 +264,7 @@ await db.orm.public.PostTag.create({ postId: post.id, tagId: tag!.id, addedBy: " ## Filter parent records by relation data -On PostgreSQL, `.where(...)` can reach into a relation. `.some(...)` matches parents with at least one matching child. `.none(...)` matches parents with none. `.every(...)` matches parents whose children all match. A parent with no children also matches. +On PostgreSQL, `.where(...)` can reach into a relation: `.some(...)` matches parents with at least one matching child, `.none(...)` matches parents with none, and `.every(...)` matches parents whose children all match. A parent with no children also matches. ```typescript // Users who have at least one published post @@ -278,7 +278,7 @@ const taggedPosts = await db.orm.public.Post .all(); ``` -Chaining `.where(...)` combines conditions with AND. For OR, call the `or` helper inside a single callback. It comes from `@prisma/orm-postgres/orm-client`, already installed with `@prisma/orm-postgres`: +Chaining `.where(...)` combines conditions with AND, so for OR, call the `or` helper inside a single callback. It comes from `@prisma/orm-postgres/orm-client`, already installed with `@prisma/orm-postgres`: ```typescript import { or } from "@prisma/orm-postgres/orm-client"; @@ -288,7 +288,7 @@ const posts = await db.orm.public.Post .all(); ``` -See [filter operators](/orm/fundamentals/reading-data#filter-operators-on-postgresql) for the comparisons you can pass to `.where(...)`. On MongoDB, relation filters are not available. Query the child collection directly instead, and include the parent: +See [filter operators](/orm/fundamentals/reading-data#filter-operators-on-postgresql) for the comparisons you can pass to `.where(...)`. On MongoDB, relation filters are not available, so query the child collection directly instead, and include the parent: ```typescript // MongoDB: the published posts, each with its author @@ -331,7 +331,7 @@ model Post { } ``` -`Post.author` is a reference: the user is a separate document in the `users` collection, and `.include("author")` reads it. `User.address` is an embedded document, and you can tell by the declaration: `Address` is written with `type`, not `model`, so its fields are stored inside the user document itself. Embedded documents come back with every read, so you never include them. Calling `.include(...)` on an embedded relation throws an error with code `ORM.INCLUDE_UNSUPPORTED`. Use `.include(...)` only for documents stored in their own collection. [MongoDB data modeling](/orm/data-modeling/mongodb#embed-or-reference) covers which to choose. +`Post.author` is a reference: the user is a separate document in the `users` collection, and `.include("author")` reads it. `User.address` is an embedded document, and you can tell by the declaration: `Address` is written with `type`, not `model`, so its fields are stored inside the user document itself. Embedded documents come back with every read, so you never include them. Calling `.include(...)` on an embedded relation throws an error with code `ORM.INCLUDE_UNSUPPORTED`, so use `.include(...)` only for documents stored in their own collection. [MongoDB data modeling](/orm/data-modeling/mongodb#embed-or-reference) covers which to choose. ## Current limitations @@ -341,7 +341,7 @@ model Post { ## Prompt your coding agent -Projects created with `npm create prisma@latest -- my-app` include the [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8), instruction files for your coding agent. The `prisma-8` skill covers both relation queries and the relation fields in your contract. Prompts that map to each section: +Projects created with `npm create prisma@latest -- my-app` include the [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8), instruction files for your coding agent. The `prisma-8` skill covers both relation queries and the relation fields in your contract, so try prompts that map to each section: - "Using the prisma-8 skill, add a one-to-one Profile model with a unique foreign key to User." - "Using the prisma-8 skill, fetch each user with their five newest posts in one query." diff --git a/apps/docs/content/docs/orm/fundamentals/transactions.mdx b/apps/docs/content/docs/orm/fundamentals/transactions.mdx index fc72313d00f..36c59f1d607 100644 --- a/apps/docs/content/docs/orm/fundamentals/transactions.mdx +++ b/apps/docs/content/docs/orm/fundamentals/transactions.mdx @@ -8,11 +8,11 @@ metaDescription: 'Group writes atomically with db.transaction() on PostgreSQL, a Run several writes as one unit with `db.transaction(...)`: they all commit together, or they all roll back. -Prisma ORM 8 runs on PostgreSQL, SQLite, and MongoDB. There is no MySQL support. The examples on this page are PostgreSQL; `db.transaction(...)` works the same way on SQLite, and MongoDB is covered in [Transactions on MongoDB](#transactions-on-mongodb). +Prisma ORM 8 runs on PostgreSQL, SQLite, and MongoDB, but there is no MySQL support. The examples on this page are PostgreSQL; `db.transaction(...)` works the same way on SQLite, and MongoDB is covered in [Transactions on MongoDB](#transactions-on-mongodb). ## Run writes in a transaction -Use a transaction whenever one business operation spans more than one write: creating a user with their first records, moving a value between two rows, or deleting a parent after its children. Pass a callback to `db.transaction(...)`. Inside it, query through `tx` instead of `db`. `tx.orm` is the model API, what `prisma.user` was in Prisma ORM 7; `tx.sql` is the SQL query builder. Both work the same as `db.orm` and `db.sql`. Every query you make on `tx` runs inside the transaction, and `db.transaction(...)` returns whatever your callback returns: +Use a transaction whenever one business operation spans more than one write: creating a user with their first records, moving a value between two rows, or deleting a parent after its children. Pass a callback to `db.transaction(...)`, and inside it, query through `tx` instead of `db`. `tx.orm` is the model API, what `prisma.user` was in Prisma ORM 7; `tx.sql` is the SQL query builder, and both work the same as `db.orm` and `db.sql`. Every query you make on `tx` runs inside the transaction, and `db.transaction(...)` returns whatever your callback returns: ```typescript import { db } from "./prisma/db"; @@ -25,11 +25,11 @@ const result = await db.transaction(async (tx) => { `db` is the client you create once in `src/prisma/db.ts`, and `prisma orm init` writes that file for you. It writes the file once; it is your code, so edit it freely. Under `tx.orm.public`, the property name is the model name, so `User` and `Post` are spelled exactly as you spelled them in your contract. -`public` is the PostgreSQL schema, the namespace your tables live in. It is `public` unless you set one. `create(...)` takes the fields of the record directly, with no `data:` wrapper. Pass a field if it is required and has no default. Everything else is optional, and [Write data](/orm/fundamentals/writing-data) has the full rules. You don't need a transaction for a single write; every mutation is already atomic on its own. +`public` is the PostgreSQL schema, the namespace your tables live in, which is `public` unless you set one. `create(...)` takes the fields of the record directly, with no `data:` wrapper: pass a field if it is required and has no default, while everything else is optional, and [Write data](/orm/fundamentals/writing-data) has the full rules. You don't need a transaction for a single write; every mutation is already atomic on its own. ### Read inside a transaction -Reads work the same way as writes. Query through `tx.orm` and you see the rows the transaction has already written. `.first()` returns `null` when nothing matches. +Reads work the same way as writes: query through `tx.orm` and you see the rows the transaction has already written. `.first()` returns `null` when nothing matches. ```typescript await db.transaction(async (tx) => { @@ -54,8 +54,8 @@ await db.transaction(async (tx) => { }); ``` -Prisma ORM does not export a type for `tx`. Add this line to `src/prisma/db.ts`: `export type Tx = Parameters[0]>[0];`, then import `Tx` wherever a helper needs it. -Do not open a second transaction inside the helper. Calling `db.transaction(...)` again inside a callback does not nest: the inner call is a separate transaction that commits or rolls back on its own, and the outer one cannot roll it back. Pass `tx` down instead. +Prisma ORM does not export a type for `tx`, so add this line to `src/prisma/db.ts`: `export type Tx = Parameters[0]>[0];`, then import `Tx` wherever a helper needs it. +Do not open a second transaction inside the helper, because calling `db.transaction(...)` again inside a callback does not nest: the inner call is a separate transaction that commits or rolls back on its own, and the outer one cannot roll it back. Pass `tx` down instead. ### Options and isolation level @@ -72,7 +72,7 @@ There is no transaction timeout either, so a transaction stays open for as long ## Roll back on errors -The transaction commits when the callback returns and rolls back when it throws. Nothing inside the callback survives an error: +The transaction commits when the callback returns and rolls back when it throws, so nothing inside the callback survives an error: ```typescript try { @@ -120,11 +120,11 @@ await db.transaction(async (tx) => { ``` Two things differ from `tx.orm`. Under `tx.sql.public` the property name is the table name, so it is `tx.sql.public.post` where the ORM API has `tx.orm.public.Post`. The table name is the model name with a lowercase first letter, so the model `BlogPost` has the table `blogPost`, and you set `@@map` on the model to use a different table name. -The second difference is that you chain clauses and then call `.build()` to finish the query. `.build()` returns the query without running it. `tx.execute(...)` runs it and returns `{ affectedRows }`, the number of rows it changed. The `.where(...)` callback receives two arguments: `f` holds the columns, and `fns` holds the operators (`lt` is less-than). [Advanced queries](/orm/fundamentals/advanced-queries#build-a-query-and-run-it) covers both. +The second difference is that you chain clauses and then call `.build()` to finish the query: `.build()` returns the query without running it, and `tx.execute(...)` runs it and returns `{ affectedRows }`, the number of rows it changed. The `.where(...)` callback receives two arguments: `f` holds the columns, and `fns` holds the operators (`lt` is less-than). [Advanced queries](/orm/fundamentals/advanced-queries#build-a-query-and-run-it) covers both. ## Transactions on MongoDB -Prisma ORM does not support MongoDB transactions yet: there is no `db.transaction(...)` on the MongoDB client. To run a multi-document transaction today, use the MongoDB driver directly. Share one `MongoClient` between Prisma ORM and your code, and group the writes in a driver session. +Prisma ORM does not support MongoDB transactions yet: there is no `db.transaction(...)` on the MongoDB client. To run a multi-document transaction today, use the MongoDB driver directly: share one `MongoClient` between Prisma ORM and your code, and group the writes in a driver session. MongoDB only runs transactions on a replica set, and a standalone server rejects them. To turn a standalone server into a single-member replica set, follow the MongoDB guide on [converting a standalone to a replica set](https://www.mongodb.com/docs/manual/tutorial/convert-standalone-to-replica-set/). Install `@prisma/orm-mongo`, which a MongoDB project already has, together with version 7 of the `mongodb` driver: @@ -152,7 +152,7 @@ export const db = mongo({ ``` Keep the `contractJson` import and the `Contract` type, and run `prisma contract emit` after every change to your contract, on PostgreSQL and on MongoDB alike. -`dbName` is now the only thing that picks the database. Set it to the database your driver code reads, so `client.db("app")` and Prisma ORM stay on the same one. An empty `dbName` is an error. +`dbName` is now the only thing that picks the database, so set it to the database your driver code reads, and `client.db("app")` and Prisma ORM stay on the same one. An empty `dbName` is an error. ```typescript import { client, db } from "./prisma/db"; @@ -173,11 +173,11 @@ const jane = await db.orm.users.where({ email: "jane@prisma.io" }).first(); ``` On MongoDB there is no schema segment in the path, and the property name is the collection name. The collection name is the model name with a lowercase first letter, so a model `User` is `db.orm.user`. `db.orm.users` above is the collection name; the model is `User` with `@@map("users")`. -Writes you make through the driver skip the type-checking that Prisma ORM queries get. Put each driver transaction in its own small function, and write everything around it as Prisma ORM queries. +Writes you make through the driver skip the type-checking that Prisma ORM queries get, so put each driver transaction in its own small function, and write everything around it as Prisma ORM queries. ## Coming from Prisma ORM 7 -Prisma ORM 8 has no `$transaction`. Both of the Prisma ORM 7 forms become `db.transaction(async (tx) => ...)`. The interactive form maps one for one: rename the method and query through `tx`. +Prisma ORM 8 has no `$transaction`, so both of the Prisma ORM 7 forms become `db.transaction(async (tx) => ...)`. The interactive form maps one for one: rename the method and query through `tx`. ```diff - const result = await prisma.$transaction(async (prisma) => { @@ -191,7 +191,7 @@ Prisma ORM 8 has no `$transaction`. Both of the Prisma ORM 7 forms become `db.tr + }); ``` -The array form has no replacement of its own. Write the queries out in the callback, one after another: +The array form has no replacement of its own, so write the queries out in the callback, one after another: ```diff - const [user, post] = await prisma.$transaction([ @@ -205,7 +205,7 @@ The array form has no replacement of its own. Write the queries out in the callb + }); ``` -You get the same atomicity. You also get what the array form never allowed: one query's result (here `user.id`) can feed the next query in the same transaction. +You get the same atomicity, and you also get what the array form never allowed: one query's result (here `user.id`) can feed the next query in the same transaction. ## Common mistakes @@ -241,11 +241,11 @@ await db.transaction(async (tx) => { }); ``` -Queries on `db` run on their own connection, outside the open transaction. They commit immediately and won't roll back with the rest of the callback. Use `tx` for every query inside the callback: `tx.orm` for models, `tx.sql` and `tx.execute` for the SQL query builder. +Queries on `db` run on their own connection, outside the open transaction, so they commit immediately and won't roll back with the rest of the callback. Use `tx` for every query inside the callback: `tx.orm` for models, `tx.sql` and `tx.execute` for the SQL query builder. ## Prompt your coding agent -Projects created with `npm create prisma@latest` include the [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8), instruction files for coding agents; in an existing project, run `npx prisma skills sync`. The `prisma-8` skill covers transactions. Prompts that map to each section: +Projects created with `npm create prisma@latest` include the [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8), instruction files for coding agents, and in an existing project you run `npx prisma skills sync` to add them. The `prisma-8` skill covers transactions, so try prompts that map to each section: - "Using the prisma-8 skill, wrap this signup flow (create user, create welcome post) in a db.transaction so both writes commit together." - "Check this transaction callback for queries that use db instead of tx, and move the email send after the commit." diff --git a/apps/docs/content/docs/orm/fundamentals/writing-data.mdx b/apps/docs/content/docs/orm/fundamentals/writing-data.mdx index 59baf529923..a39083875b5 100644 --- a/apps/docs/content/docs/orm/fundamentals/writing-data.mdx +++ b/apps/docs/content/docs/orm/fundamentals/writing-data.mdx @@ -8,11 +8,11 @@ metaDescription: 'Create, update, delete, and upsert records in PostgreSQL and M This page shows how to write data with Prisma ORM: [creating](#create-one-record), [updating](#update-one-record), [deleting](#delete-one-record), and [upserting](#upsert-a-record) single records, and [writing many records at once](#write-many-records). -Every example imports `db`. New project: `npm create prisma@latest -- my-app`. Existing project: `npx prisma orm init`. Either way you get `src/prisma/db.ts`, which exports `db`. The import is `./prisma/db` from a file in `src/`. +Every example imports `db`. In a new project run `npm create prisma@latest -- my-app`, and in an existing project run `npx prisma orm init`. Either way you get `src/prisma/db.ts`, which exports `db`, and the import is `./prisma/db` from a file in `src/`. This page uses `db.orm`, which holds your models. The rest of `db` is `db.sql` for the SQL query builder, `db.raw.sql` for raw SQL, and `db.transaction` for running several writes together. -On PostgreSQL the path to a model is `db.orm..`, so the `User` model is `db.orm.public.User`. `public` is the PostgreSQL schema; you type it. It is `public` unless the model sits in a `namespace` block (shown under [Example schema](#example-schema)). On MongoDB there is no schema segment. The path is `db.orm.`, so the model below is `db.orm.users`. The collection name is the model's `@@map(...)`, or the model name with a lowercase first letter. +On PostgreSQL the path to a model is `db.orm..`, so the `User` model is `db.orm.public.User`. `public` is the PostgreSQL schema; you type it. It is `public` unless the model sits in a `namespace` block (shown under [Example schema](#example-schema)). On MongoDB there is no schema segment, and the path is `db.orm.`, so the model below is `db.orm.users`. The collection name is the model's `@@map(...)`, or the model name with a lowercase first letter. ## Example schema @@ -82,7 +82,7 @@ The model is then only at `db.orm.billing.Invoice`, and the block name is the Po ## Create one record -Use `.create(...)` to insert one record. Pass the fields directly. Prisma ORM returns the inserted record, including generated values such as IDs and database defaults: +Use `.create(...)` to insert one record, and pass the fields directly. Prisma ORM returns the inserted record, including generated values such as IDs and database defaults: ```typescript tab="PostgreSQL" import { db } from "./prisma/db"; @@ -111,7 +111,7 @@ The returned record is complete, so you can use the generated values right away { id: 'cuid20000000000000000003', email: 'jane@prisma.io', name: 'Jane', createdAt: 2026-07-06T09:09:56.119Z } ``` -To get back only some fields, chain `.select(...)` before `.create(...)`. The record is still inserted in full. You only get back the fields you listed: +To get back only some fields, chain `.select(...)` before `.create(...)`: the record is still inserted in full, but you only get back the fields you listed: ```typescript const account = await db.orm.public.User @@ -125,7 +125,7 @@ const account = await db.orm.public.User `.select(...)` works the same before `update`, `delete`, `upsert`, `createAll`, `updateAll`, and `deleteAll`. The `AndCount` methods give you back a number, so `.select(...)` has no effect on them. -When a write breaks a unique constraint, the call throws. Errors have no shared class. A PostgreSQL database error carries `sqlState` (a five-character SQL state code), a MongoDB driver error carries a numeric `code`, and a Prisma ORM error carries a string `code` such as `RUNTIME.ITERATOR_CONSUMED`. On PostgreSQL, `23505` is the SQL state code for a unique violation. The [error reference](/orm/reference/error-reference) lists Prisma ORM's own codes: +When a write breaks a unique constraint, the call throws, and errors have no shared class: a PostgreSQL database error carries `sqlState` (a five-character SQL state code), a MongoDB driver error carries a numeric `code`, and a Prisma ORM error carries a string `code` such as `RUNTIME.ITERATOR_CONSUMED`. On PostgreSQL, `23505` is the SQL state code for a unique violation. The [error reference](/orm/reference/error-reference) lists Prisma ORM's own codes: ```typescript try { @@ -137,11 +137,11 @@ try { } ``` -On MongoDB the driver throws its own error. A duplicate key gives you an error whose `error.code` is `11000`. +On MongoDB the driver throws its own error, and a duplicate key gives you an error whose `error.code` is `11000`. :::note -MongoDB contracts do not support `@default`, so pass timestamp fields such as `createdAt` yourself. On PostgreSQL the database fills them in. +MongoDB contracts do not support `@default`, so pass timestamp fields such as `createdAt` yourself, while on PostgreSQL the database fills them in. `@map("_id")` renames the id field to `_id` everywhere, so the returned document has an `_id` key and not an `id` key. @@ -160,7 +160,7 @@ const user = await db.orm.public.User.create({ }); ``` -You do not pass `authorId` on the nested posts. Prisma ORM fills it in from the user it just inserted. +You do not pass `authorId` on the nested posts, because Prisma ORM fills it in from the user it just inserted. `connect` links a record that already exists, and works in `.update(...)` too: @@ -170,13 +170,13 @@ await db.orm.public.User .update({ posts: (p) => p.connect([{ id: existingPostId }]) }); ``` -`p.disconnect(...)` unlinks a related record. It applies on `.update(...)` only, not on `.create(...)`. +`p.disconnect(...)` unlinks a related record, but it applies on `.update(...)` only, not on `.create(...)`. -`connectOrCreate`, the relation `set`, and nested updates, upserts, and deletes do not exist. See [Not available](/orm/coming-from-prisma-orm-7#not-available-yet). For the full picture of relations, see [Relations and joins](/orm/fundamentals/relations-and-joins). +`connectOrCreate`, the relation `set`, and nested updates, upserts, and deletes do not exist: see [Not available](/orm/coming-from-prisma-orm-7#not-available-yet). For the full picture of relations, see [Relations and joins](/orm/fundamentals/relations-and-joins). ## Update one record -Use `.where(...)` to pick the record, then `.update(...)` with the fields to change. It updates **one** matching record and returns it: +Use `.where(...)` to pick the record, then `.update(...)` with the fields to change, which updates **one** matching record and returns it: ```typescript tab="PostgreSQL" const updatedUser = await db.orm.public.User @@ -194,7 +194,7 @@ const updatedUser = await db.orm.users { id: 'cuid20000000000000000003', email: 'jane@prisma.io', name: 'Jane Doe', createdAt: 2026-07-06T09:09:56.119Z } ``` -When nothing matches the filter, `.update(...)` returns `null`. It does not throw. +When nothing matches the filter, `.update(...)` returns `null` rather than throwing. When the filter matches more than one record, `.update(...)` still changes only one of the matching records, with no guaranteed order. Use [`updateAll` or `updateAndCount`](#write-many-records) if you mean all of them. @@ -208,9 +208,9 @@ await db.orm.posts The callback returns an array, so you can apply several operations in one update. -`set` and `unset` work on any field. `inc` and `mul` are on number fields only. `push`, `pull`, `addToSet`, and `pop` are for array fields, and you call them the same way: `p.tags.push("news")`. These field operations are MongoDB only. PostgreSQL has no callback form of `update`, so on PostgreSQL you pass an object. See [Field update operations](/orm/reference/orm-client#field-update-operations) in the reference. +`set` and `unset` work on any field, while `inc` and `mul` are on number fields only. `push`, `pull`, `addToSet`, and `pop` are for array fields, and you call them the same way: `p.tags.push("news")`. These field operations are MongoDB only: PostgreSQL has no callback form of `update`, so on PostgreSQL you pass an object. See [Field update operations](/orm/reference/orm-client#field-update-operations) in the reference. -There is no `increment` on PostgreSQL. To add to a number in place, write the update as raw SQL. For a `views Int` column added to `Post`, `db.raw.sql` writes a raw statement, `.affectedCount()` says you want the row count back as `{ affectedRows }`, `.build()` finishes it, and `db.runtime().execute(...)` runs it; [Advanced queries](/orm/fundamentals/advanced-queries) explains raw SQL: +There is no `increment` on PostgreSQL, so to add to a number in place, write the update as raw SQL. For a `views Int` column added to `Post`, `db.raw.sql` writes a raw statement, `.affectedCount()` says you want the row count back as `{ affectedRows }`, `.build()` finishes it, and `db.runtime().execute(...)` runs it; [Advanced queries](/orm/fundamentals/advanced-queries) explains raw SQL: ```typescript const query = db.raw.sql`UPDATE post SET views = views + 1 WHERE id = ${postId}` @@ -221,7 +221,7 @@ const { affectedRows } = await db.runtime().execute(query); ## Delete one record -Use `.where(...)` then `.delete()`. It deletes **one** matching record and returns it: +Use `.where(...)` then `.delete()`, which deletes **one** matching record and returns it: ```typescript tab="PostgreSQL" const deletedUser = await db.orm.public.User @@ -239,7 +239,7 @@ const deletedUser = await db.orm.users ## Upsert a record -Use `.upsert(...)` to update a record if it exists and create it otherwise. Pass the two branches separately: +Use `.upsert(...)` to update a record if it exists and create it otherwise, and pass the two branches separately: ```typescript tab="PostgreSQL" await db.orm.public.User.upsert({ @@ -258,7 +258,7 @@ await db.orm.users.where({ email: "eve@prisma.io" }).upsert({ `conflictOn` repeats the unique field and its value from `create`; Prisma ORM uses it to look for an existing row. For a unique constraint over several columns, pass them all in one object: `conflictOn: { tenantId, email }`. Without `conflictOn` on PostgreSQL, the upsert looks for a row by primary key, which a new record does not have, so the insert runs and fails on the unique constraint. Always pass `conflictOn`. -On MongoDB, there is no `conflictOn`. Put the match in `.where(...)` before `.upsert(...)`. +On MongoDB, there is no `conflictOn`, so put the match in `.where(...)` before `.upsert(...)`. ## Write many records @@ -294,7 +294,7 @@ const deletedCount = await db.orm.public.Post.where((p) => p.title.ilike("draft% [{ id: 'cuid20000000000000000101', title: 'One', published: false, /* ... */ }, { id: 'cuid20000000000000000102', title: 'Two', published: false, /* ... */ }] ``` -The `AndCount` methods return a plain number. If three posts match, `updatedCount` is `3`, not an object. +The `AndCount` methods return a plain number, so if three posts match, `updatedCount` is `3`, not an object. `.where((p) => p.title.ilike("draft%"))` is the callback form of a filter, for conditions an object cannot express. Its argument gives you one entry per field, and each field carries the comparisons you can apply to it. [Reading data](/orm/fundamentals/reading-data) covers the filters you can write. @@ -302,7 +302,7 @@ The bulk methods work the same on MongoDB, on the collection: `db.orm.posts`. Pa ## Return rows or counts -Each write comes in three forms. Pick by what you need back: +Each write comes in three forms, and you pick by what you need back: | Form | What it writes | What you get back | | --- | --- | --- | @@ -360,7 +360,7 @@ Use `updateAll` or `deleteAll` when you also need the changed records back, and ### Wrapping create fields in a data object -You wrote the Prisma ORM 7 shape, and it fails type-checking. There is no field named `data` on your models: +You wrote the Prisma ORM 7 shape, which fails type-checking because there is no field named `data` on your models: ```diff - await db.orm.public.User.create({ data: { email, name } }); @@ -392,7 +392,7 @@ const user = await db.orm.public.User.create({ email, name }); const post = await db.orm.public.Post.create({ title, published: false, authorId: user.id }); ``` -If the second write fails, the first has already committed, and you're left with half the operation. When writes must succeed together, run them in a [transaction](/orm/fundamentals/transactions). Inside the callback, query through `tx` instead of `db`: +If the second write fails, the first has already committed, and you're left with half the operation. When writes must succeed together, run them in a [transaction](/orm/fundamentals/transactions), and inside the callback, query through `tx` instead of `db`: ```typescript await db.transaction(async (tx) => { @@ -403,11 +403,11 @@ await db.transaction(async (tx) => { ### Passing an array of queries to a transaction -Prisma ORM 7 supported `$transaction([query1, query2])`. Prisma ORM 8 does not: there is no `$transaction`, and queries don't queue up in arrays. Put the calls inside one `db.transaction(...)` callback instead. The [Transactions page](/orm/fundamentals/transactions) shows the pattern. +Prisma ORM 7 supported `$transaction([query1, query2])`, but Prisma ORM 8 does not: there is no `$transaction`, and queries don't queue up in arrays. Put the calls inside one `db.transaction(...)` callback instead, and the [Transactions page](/orm/fundamentals/transactions) shows the pattern. ## Prompt your coding agent -Projects created with `npm create prisma@latest` include the [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8) for your coding agent; in an existing project, run `npx prisma skills sync`. Skills are instruction files that tell the agent how Prisma ORM 8 works, and the `prisma-8` skill covers everything on this page. Prompts that map to each section: +Projects created with `npm create prisma@latest` include the [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8) for your coding agent, and in an existing project you run `npx prisma skills sync` to add them. Skills are instruction files that tell the agent how Prisma ORM 8 works, and the `prisma-8` skill covers everything on this page, so try prompts that map to each section: - "Using the prisma-8 skill, add a signup function that creates a User and returns only its id and email." - "Write an upsert that creates a user by email or updates their name if they exist." diff --git a/apps/docs/content/docs/orm/index.mdx b/apps/docs/content/docs/orm/index.mdx index ae850839c38..f5409b4061c 100644 --- a/apps/docs/content/docs/orm/index.mdx +++ b/apps/docs/content/docs/orm/index.mdx @@ -6,7 +6,7 @@ metaTitle: What is Prisma ORM? metaDescription: 'Prisma ORM 8 is the next major version. It introduces a TypeScript runtime, contract-based data models, a new query API, and a revised migration workflow.' --- -Prisma ORM 8 rewrites Prisma ORM in TypeScript. The schema-first workflow and model-first queries from earlier versions stay. What changes is what your schema produces and how queries run. This page gives you the mental model, and the [quickstart](/prisma-orm/quickstart/postgresql) gets you to a running query. +Prisma ORM 8 rewrites Prisma ORM in TypeScript. The schema-first workflow and model-first queries from earlier versions stay, but what changes is what your schema produces and how queries run. This page gives you the mental model, and the [quickstart](/prisma-orm/quickstart/postgresql) gets you to a running query. :::note[Using Prisma ORM 7?] diff --git a/apps/docs/content/docs/orm/middleware/authoring-custom-middleware.mdx b/apps/docs/content/docs/orm/middleware/authoring-custom-middleware.mdx index c18cbee2d60..6a0c35b70e3 100644 --- a/apps/docs/content/docs/orm/middleware/authoring-custom-middleware.mdx +++ b/apps/docs/content/docs/orm/middleware/authoring-custom-middleware.mdx @@ -160,7 +160,7 @@ You have a working, configurable middleware. The rest of this page describes the ## The hooks -`afterQuery` is one hook on one of two lifecycles. Row queries and non-returning writes take different paths, and `beforeCompile` is the only hook they share. Implement any subset: +`afterQuery` is one hook on one of two lifecycles: row queries and non-returning writes take different paths, and `beforeCompile` is the only hook they share. Implement any subset: | Hook | Lifecycle | Runs | Use it to | | --- | --- | --- | --- | @@ -179,7 +179,7 @@ There is no generic `intercept` and no fallback between the two lifecycles. A mi ### Rewrite queries with beforeCompile -`beforeCompile` sees the typed AST before it becomes SQL. Return a new draft to rewrite the query. This middleware adds a predicate to every `SELECT` on the `user` table: +`beforeCompile` sees the typed AST before it becomes SQL, so return a new draft to rewrite the query. This middleware adds a predicate to every `SELECT` on the `user` table: ```ts title="src/prisma/scope-user-selects.ts" import type { SqlMiddleware } from '@prisma/orm-postgres/family-runtime'; diff --git a/apps/docs/content/docs/orm/middleware/built-in-cache.mdx b/apps/docs/content/docs/orm/middleware/built-in-cache.mdx index c549540646e..1b1fa687037 100644 --- a/apps/docs/content/docs/orm/middleware/built-in-cache.mdx +++ b/apps/docs/content/docs/orm/middleware/built-in-cache.mdx @@ -80,7 +80,7 @@ By default the cache key is a content hash of the executed query: the post-lower Cache hits are still visible to the rest of the middleware chain: `beforeQuery` hooks have already run, the driver and `onRow` are skipped, and `afterQuery` fires on every middleware with `result.source` set to `'middleware'` instead of `'driver'`, so logging and metrics keep working for cached reads. -The cache only acts at runtime scope. Queries executed inside a transaction or on an explicitly checked-out connection always go to the database, because read-after-write consistency is the caller's expectation there. +The cache only acts at runtime scope: queries executed inside a transaction or on an explicitly checked-out connection always go to the database, because read-after-write consistency is the caller's expectation there. ## Options @@ -94,7 +94,7 @@ The cache only acts at runtime scope. Queries executed inside a transaction or o :::warning -The default store is in-memory and per-process. Two instances of your app do not share it, and a deploy clears it. That makes it a good fit for hot, tolerant-of-staleness reads, and a poor fit as a source of truth. For shared caching, implement `store` against your infrastructure. +The default store is in-memory and per-process, so two instances of your app do not share it, and a deploy clears it. That makes it a good fit for hot, tolerant-of-staleness reads, and a poor fit as a source of truth. For shared caching, implement `store` against your infrastructure. ::: diff --git a/apps/docs/content/docs/orm/middleware/built-in-lints.mdx b/apps/docs/content/docs/orm/middleware/built-in-lints.mdx index b11b0fb44bf..759d964a9c6 100644 --- a/apps/docs/content/docs/orm/middleware/built-in-lints.mdx +++ b/apps/docs/content/docs/orm/middleware/built-in-lints.mdx @@ -71,7 +71,7 @@ There are five severity keys: `deleteWithoutWhere`, `updateWithoutWhere`, `noLim Queries built with the ORM client or the SQL query builder carry a typed AST, and the rules above inspect it structurally. -A whole-query raw statement built with [`db.raw.sql`](/orm/reference/raw-queries) does have an AST, of kind `raw-query`. It has no structural shape the runtime authored, so the structural rules are skipped for it and the text heuristics always run instead: they flag `select *`, a missing `LIMIT`, and a mutation smuggled through a query marked read-only (`LINT.READ_ONLY_MUTATION`). The raw heuristics carry their own defaults: `select *` escalates to `error` for raw plans (stricter than the AST rule's `warn`), a missing `LIMIT` stays a warning, and a read-only-intent mutation is an `error`. The `severities` keys above override raw findings too. +A whole-query raw statement built with [`db.raw.sql`](/orm/reference/raw-queries) does have an AST, of kind `raw-query`, but it has no structural shape the runtime authored, so the structural rules are skipped for it and the text heuristics always run instead: they flag `select *`, a missing `LIMIT`, and a mutation smuggled through a query marked read-only (`LINT.READ_ONLY_MUTATION`). The raw heuristics carry their own defaults: `select *` escalates to `error` for raw plans (stricter than the AST rule's `warn`), a missing `LIMIT` stays a warning, and a read-only-intent mutation is an `error`. The `severities` keys above override raw findings too. `fallbackWhenAstMissing` matters only for a plan that has no `plan.ast` at all, not for `raw-query` plans: diff --git a/apps/docs/content/docs/orm/middleware/how-middleware-works.mdx b/apps/docs/content/docs/orm/middleware/how-middleware-works.mdx index 31ae1435dcb..d1d983963b2 100644 --- a/apps/docs/content/docs/orm/middleware/how-middleware-works.mdx +++ b/apps/docs/content/docs/orm/middleware/how-middleware-works.mdx @@ -112,7 +112,7 @@ Prisma ORM groups databases into families: the SQL family (PostgreSQL and other - `budgets` and `lints` declare `familyId: 'sql'`. They register on PostgreSQL and are rejected on MongoDB. - The cache declares no `familyId`. It works against the family-neutral parts of a query, so it runs on both PostgreSQL and MongoDB. -The runtime checks this when you construct the client. A middleware that does not match the runtime fails at startup with `RUNTIME.MIDDLEWARE_FAMILY_MISMATCH`, not at query time in production. +The runtime checks this when you construct the client, so a middleware that does not match the runtime fails at startup with `RUNTIME.MIDDLEWARE_FAMILY_MISMATCH`, not at query time in production. ## When a middleware or the driver throws diff --git a/apps/docs/content/docs/orm/migrations/applying-a-migration.mdx b/apps/docs/content/docs/orm/migrations/applying-a-migration.mdx index 9953420a4f8..f17cc31e3e4 100644 --- a/apps/docs/content/docs/orm/migrations/applying-a-migration.mdx +++ b/apps/docs/content/docs/orm/migrations/applying-a-migration.mdx @@ -12,7 +12,7 @@ Applying is the one step that touches a database, via a single command: npx prisma@latest db migrate ``` -Note that the command is `db migrate`, not `migration apply`. The `migration ...` subcommands manage files on disk; `db migrate` moves a database. It reads where the database currently is (its **marker**), finds a path through the [migration graph](/orm/migrations/the-migration-graph) to the target, and applies each migration along the path, running every operation's precheck, execute, and postcheck as it goes. +Note that the command is `db migrate`, not `migration apply`: the `migration ...` subcommands manage files on disk, while `db migrate` moves a database. It reads where the database currently is (its **marker**), finds a path through the [migration graph](/orm/migrations/the-migration-graph) to the target, and applies each migration along the path, running every operation's precheck, execute, and postcheck as it goes. By default, the connection comes from `db.connection` in `prisma.config.ts`. Pass `--db` to override it: @@ -97,7 +97,7 @@ npx prisma@latest db migrate --to prod --db $DATABASE_URL If the graph has branched and more than one tip is reachable, `db migrate` stops and asks for an explicit `--to`. That's the graph protecting you: two feature branches may both be valid futures, and picking one is a human decision. -`--advance-ref` moves a named ref to the post-apply state in the same step, which is what keeps [`migration plan`](/orm/migrations/generating-a-migration#the-db-ref-skipping---from) incremental. Plain `db migrate` never moves a ref. That is deliberate: a deploy or a CI apply should not move a ref that lives in the repository, so in development you pass `--advance-ref db` and in production you leave it off. The [`db migrate` reference](/cli/db-migrate#options) has the full `--to` grammar (refs, hashes, migration names, `^`) and the flag contract. +`--advance-ref` moves a named ref to the post-apply state in the same step, which is what keeps [`migration plan`](/orm/migrations/generating-a-migration#the-db-ref-skipping---from) incremental. Plain `db migrate` never moves a ref, which is deliberate: a deploy or a CI apply should not move a ref that lives in the repository, so in development you pass `--advance-ref db` and in production you leave it off. The [`db migrate` reference](/cli/db-migrate#options) has the full `--to` grammar (refs, hashes, migration names, `^`) and the flag contract. ## When something goes wrong @@ -119,7 +119,7 @@ Before running any DDL, `db migrate` also verifies the database's marker is a st ## Development vs. production -The commands are the same everywhere. What changes is where the files come from and who runs them. +The commands are the same everywhere: what changes is where the files come from and who runs them. **In development**, you're the one planning and editing, and you apply immediately: @@ -137,7 +137,7 @@ npx prisma@latest db migrate --db $DATABASE_URL The runner executes only `ops.json`, which is plain data, so your `migration.ts` files, and any TypeScript they import, are never executed with production credentials. -Concurrent deploys are safe: on PostgreSQL the whole apply runs inside a transaction guarded by an advisory lock, so two `db migrate` runs serialize instead of interleaving. On MongoDB, cross-collection DDL transactions don't exist. Instead, each migration advances the marker with compare-and-swap, and the runner verifies the resulting schema before committing the marker, so a re-run converges rather than double-applying. +Concurrent deploys are safe: on PostgreSQL the whole apply runs inside a transaction guarded by an advisory lock, so two `db migrate` runs serialize instead of interleaving. On MongoDB, cross-collection DDL transactions don't exist: instead, each migration advances the marker with compare-and-swap, and the runner verifies the resulting schema before committing the marker, so a re-run converges rather than double-applying. ## Extension spaces diff --git a/apps/docs/content/docs/orm/migrations/editing-a-migration.mdx b/apps/docs/content/docs/orm/migrations/editing-a-migration.mdx index 9347995c8b2..6cbd3a617f5 100644 --- a/apps/docs/content/docs/orm/migrations/editing-a-migration.mdx +++ b/apps/docs/content/docs/orm/migrations/editing-a-migration.mdx @@ -57,7 +57,7 @@ override get operations() { } ``` -A `dataTransform` takes two closures. `check` asks "are there rows that still need this?" It must be a row-returning query where *any row* means work remains. The conventional shape is `select('id').where().limit(1)`. `run` performs the change. Fill them with the typed SQL query builder, wired to the contract snapshot the migration imports from `migrations/snapshots/`: +A `dataTransform` takes two closures. `check` asks "are there rows that still need this?", so it must be a row-returning query where *any row* means work remains. The conventional shape is `select('id').where().limit(1)`. `run` performs the change. Fill them with the typed SQL query builder, wired to the contract snapshot the migration imports from `migrations/snapshots/`: ```ts title="migration.ts (filled in)" import type { Contract as End } from '../../snapshots/e6b5c2849eca8d24ff1e8e88ab2a4234db8e74c497c035cb7ce42e814f31cd63/contract'; @@ -119,7 +119,7 @@ These two details should be noted: - **The types come from the migration's end-contract snapshot** (`migrations/snapshots//contract`), not your live app contract. You're type-checking against the schema *as it will exist when this step runs*. That's why the builder happily references `displayName` even though the column doesn't exist yet, and why a migration written months ago keeps compiling after your contract moves on. - **The query is real application-grade TypeScript.** You can import shared constants, and typos in column names fail the type check instead of failing in production. -The wiring block at the top (deserializing the contract, building the `db` handle) is the current shape. Expect it to shrink to a one-liner. It's the price of a query builder that's typed against this migration's snapshot instead of your live app client. +The wiring block at the top (deserializing the contract, building the `db` handle) is the current shape, but expect it to shrink to a one-liner. It's the price of a query builder that's typed against this migration's snapshot instead of your live app client. Recompile with `node migration.ts` and inspect what the backfill became: @@ -174,7 +174,7 @@ rawSql({ }), ``` -The prechecks and postchecks are optional, but they're what makes a failed run resumable and a mistake diagnosable: skipping them trades away most of what this system gives you. If you write the same `rawSql` twice, lift it into a function. The built-in factories are plain functions doing exactly this. +The prechecks and postchecks are optional, but they're what makes a failed run resumable and a mistake diagnosable: skipping them trades away most of what this system gives you. If you write the same `rawSql` twice, lift it into a function: the built-in factories are plain functions doing exactly this. ## The same pattern on MongoDB diff --git a/apps/docs/content/docs/orm/migrations/generating-a-migration.mdx b/apps/docs/content/docs/orm/migrations/generating-a-migration.mdx index 53fb5d2627b..aa842c4a52b 100644 --- a/apps/docs/content/docs/orm/migrations/generating-a-migration.mdx +++ b/apps/docs/content/docs/orm/migrations/generating-a-migration.mdx @@ -6,7 +6,7 @@ metaTitle: Generating a migration in Prisma ORM metaDescription: 'A step-by-step tutorial for the migration plan command, from contract change to a planned, reviewable migration, entirely offline.' --- -This tutorial takes a contract change from your editor to a planned migration on disk. Planning is **fully offline**. `migration plan` reads your emitted contract and your existing migrations; it never connects to a database. This means you can plan on a plane, in CI, or in a sandbox with no credentials. +This tutorial takes a contract change from your editor to a planned migration on disk. Planning is **fully offline**: `migration plan` reads your emitted contract and your existing migrations, and it never connects to a database. This means you can plan on a plane, in CI, or in a sandbox with no credentials. Start from a minimal project; you can scaffold one with `npm create prisma@latest`. @@ -24,13 +24,13 @@ model User { } ``` -First, emit the contract. This compiles the schema into the `contract.json` artifact that every other command reads: +First, emit the contract, which compiles the schema into the `contract.json` artifact that every other command reads: ```npm npx prisma@latest contract emit ``` -Now plan. `--name` sets the human-readable part of the directory name. If you omit it, the directory will be called `migration`: +Now plan. `--name` sets the human-readable part of the directory name, and if you omit it, the directory will be called `migration`: ```npm npx prisma@latest migration plan --name init @@ -114,7 +114,7 @@ export default class M extends Migration { MigrationCLI.run(import.meta.url, M); ``` -When reading it, note two things: the `col(...)` calls mirror your contract fields, and you never write `codecRef` by hand. The planner fills it in to pin how values convert between TypeScript and Postgres. Treat it as noise when reviewing. +When reading it, note two things: the `col(...)` calls mirror your contract fields, and you never write `codecRef` by hand. The planner fills it in to pin how values convert between TypeScript and Postgres, so treat it as noise when reviewing. For a simple change you don't need to touch this file. When you do (to add a data backfill, reorder operations, or drop in raw SQL), refer to [Editing a migration](/orm/migrations/editing-a-migration). @@ -159,7 +159,7 @@ ALTER TABLE "public"."user" ADD COLUMN "phone" text; ### The db ref: skipping --from -You don't have to pass `--from` every time. If a [ref](/orm/migrations/the-migration-graph#name-important-states-with-refs) named **`db`** exists, planning starts from whatever it points at by default. Four things move it forward: +You don't have to pass `--from` every time: if a [ref](/orm/migrations/the-migration-graph#name-important-states-with-refs) named **`db`** exists, planning starts from whatever it points at by default. Four things move it forward: - `db init` and `db update` advance it after a successful run, but only when you leave `--db` off and let the connection come from `prisma.config.ts`. Passing `--db` suppresses the advancement unless you also pass `--advance-ref db`. - `db sign` advances it after a successful signature, with or without `--db`. This is what makes the first plan after adopting an existing database contain only your change. Pass `--no-advance-ref` to sign without moving it. @@ -178,7 +178,7 @@ npx prisma@latest migration plan --name next_change # starts from the db ref a If there's no `db` ref and you omit `--from`, `migration plan` refuses with `MIGRATION.PLAN_ORIGIN_UNKNOWN` whenever migrations already exist on disk. Planning from empty there would produce a full `CREATE`-everything package that no database carrying the earlier migrations could apply, so the command stops instead and names the three exits: `migration ref set db `, `--from `, or `--from @empty` to plan from an empty database deliberately. -Only an empty `migrations/app/` still plans from empty on its own. That is the first migration in a new project, described above. The command prints `No db ref set — planning from an empty database` under its summary when that happens, so you can tell a fresh first plan from a plan that recreates a database you already have. +Only an empty `migrations/app/` still plans from empty on its own: that is the first migration in a new project, described above. The command prints `No db ref set — planning from an empty database` under its summary when that happens, so you can tell a fresh first plan from a plan that recreates a database you already have. ::: @@ -222,7 +222,7 @@ npx prisma@latest migration check :::note[What's early] -Planning covers tables, columns, indexes, constraints, and the backfill scaffold shown above. Rename inference is not built yet: renaming a field plans as **drop column + add column**, flagged destructive with a data-loss warning. For a true rename, edit the migration and replace the pair with a `rawSql` `ALTER TABLE ... RENAME COLUMN`. There is also no interactive mode. The planner writes its best answer and leaves refinement to you in `migration.ts`. +Planning covers tables, columns, indexes, constraints, and the backfill scaffold shown above. Rename inference is not built yet: renaming a field plans as **drop column + add column**, flagged destructive with a data-loss warning. For a true rename, edit the migration and replace the pair with a `rawSql` `ALTER TABLE ... RENAME COLUMN`. There is also no interactive mode: the planner writes its best answer and leaves refinement to you in `migration.ts`. ::: diff --git a/apps/docs/content/docs/orm/migrations/how-migrations-work.mdx b/apps/docs/content/docs/orm/migrations/how-migrations-work.mdx index a47396f6119..504199a1ed8 100644 --- a/apps/docs/content/docs/orm/migrations/how-migrations-work.mdx +++ b/apps/docs/content/docs/orm/migrations/how-migrations-work.mdx @@ -67,7 +67,7 @@ migrations/ Contract snapshots live once per migrations root, in `migrations/snapshots/`, keyed by the contract's hash. A migration imports the states it moves between from there (a baseline migration starts from nothing and imports only its end contract), so a contract state shared by several migrations is stored once. -Three files matter day to day. Each has one job: +Three files matter day to day, and each has one job: | File | Purpose | | --- | --- | @@ -77,7 +77,7 @@ Three files matter day to day. Each has one job: ### migration.ts: the file you edit -`migration.ts` is the authoring file. It contains the migration steps as ordinary TypeScript: +`migration.ts` is the authoring file and contains the migration steps as ordinary TypeScript: ```ts this.addColumn(...) @@ -91,9 +91,9 @@ When you want to review or change what the migration does, use this file. For mo ### ops.json: the file Prisma runs -`ops.json` is the compiled form of the migration. When `migration.ts` runs, Prisma turns the TypeScript steps into JSON operations. The planner runs it for you. After an edit, re-run it yourself with `node migration.ts`. The migration runner reads only this file. +`ops.json` is the compiled form of the migration: when `migration.ts` runs, Prisma turns the TypeScript steps into JSON operations. The planner runs it for you, but after an edit, re-run it yourself with `node migration.ts`. The migration runner reads only this file. -That means production never executes your TypeScript. It reads the compiled operations, so no application code runs with production credentials. +That means production never executes your TypeScript: it reads the compiled operations, so no application code runs with production credentials. ```text migration.ts -> ops.json @@ -191,7 +191,7 @@ This structure is what makes Prisma ORM migrations safe in the situations where ## The commands -These commands live under the [Prisma ORM CLI](/cli). Run them with `npx prisma@latest `. The planning and inspection commands work offline. They read files, not your database: +These commands live under the [Prisma ORM CLI](/cli), and you run them with `npx prisma@latest `. The planning and inspection commands work offline, because they read files, not your database: | Command | What it does | | ------------------------- | ----------------------------------------------------------------------- | diff --git a/apps/docs/content/docs/orm/migrations/rollbacks-and-recovery.mdx b/apps/docs/content/docs/orm/migrations/rollbacks-and-recovery.mdx index 2a6eea6d05e..e8191774b9e 100644 --- a/apps/docs/content/docs/orm/migrations/rollbacks-and-recovery.mdx +++ b/apps/docs/content/docs/orm/migrations/rollbacks-and-recovery.mdx @@ -48,7 +48,7 @@ The planner diffs the two contract states: it writes the operations that undo th npx prisma@latest db migrate --to 20260707T1008_add_display_name^ ``` -A rollback also leaves a cycle in the graph, so the next `migration plan` cannot pick its starting point automatically. It fails with `MIGRATION.NO_TARGET` and lists the reachable states. Plan with an explicit `--from ` until history moves forward again. +A rollback also leaves a cycle in the graph, so the next `migration plan` cannot pick its starting point automatically: it fails with `MIGRATION.NO_TARGET` and lists the reachable states. Plan with an explicit `--from ` until history moves forward again. One loose end remains after the database moves back: your contract source still contains the change, so `db verify` reports a hash mismatch until you revert the schema and re-run `contract emit`. Roll back the contract in the same commit as the rollback migration and the two stay in step. @@ -91,7 +91,7 @@ The playbook: 2. **Nothing to clean up.** On PostgreSQL the whole run was one transaction, so the failure rolled it back completely: the database is exactly where it was before the run, and migrations applied in earlier runs are untouched. There is no "half-applied migration" to untangle by hand. 3. **Fix the cause, then re-run `db migrate`.** Sometimes the cause is the environment (extension missing, permissions). Sometimes it's the migration itself; here you'd [edit the migration](/orm/migrations/editing-a-migration) to add a backfill before the `setNotNull`, recompile with `node migration.ts`, and apply again. Re-running is safe, for the reasons covered in [the failure model](/orm/migrations/applying-a-migration#when-something-goes-wrong). -The precheck also protects you when a migration works in development but would fail in production. Your dev database had no `NULL`s, but production does. The precheck halts production *before* the destructive `ALTER` touches anything, with an error pointing at the exact rows-with-NULLs condition instead of a generic constraint violation mid-statement. +The precheck also protects you when a migration works in development but would fail in production. Your dev database had no `NULL`s, but production does, so the precheck halts production *before* the destructive `ALTER` touches anything, with an error pointing at the exact rows-with-NULLs condition instead of a generic constraint violation mid-statement. ### Drift: when the database isn't where migrations left it @@ -103,7 +103,7 @@ If someone changed the database outside of migrations (a hand-run `ALTER`, a res :::note[What's early] -Reverse planning (`--to ^`), destructive-operation warnings, resumable re-runs, and the ledger all work today, as shown above. Some gaps remain. There's no rehearsal mode that executes a migration against a shadow copy first. Recovery from drift beyond the `db verify`/`db update`/`db init` trio is manual. The planning caveat after cycles (explicit `--from`) is a real papercut we expect to smooth out. For deep or unusual situations, `#prisma-next` on [Discord](https://pris.ly/discord) is the fastest route. +Reverse planning (`--to ^`), destructive-operation warnings, resumable re-runs, and the ledger all work today, as shown above. Some gaps remain: there's no rehearsal mode that executes a migration against a shadow copy first, and recovery from drift beyond the `db verify`/`db update`/`db init` trio is manual. The planning caveat after cycles (explicit `--from`) is a real papercut we expect to smooth out. For deep or unusual situations, `#prisma-next` on [Discord](https://pris.ly/discord) is the fastest route. ::: diff --git a/apps/docs/content/docs/orm/migrations/the-migration-graph.mdx b/apps/docs/content/docs/orm/migrations/the-migration-graph.mdx index e6e51e64aad..f32e6556f51 100644 --- a/apps/docs/content/docs/orm/migrations/the-migration-graph.mdx +++ b/apps/docs/content/docs/orm/migrations/the-migration-graph.mdx @@ -6,7 +6,7 @@ metaTitle: The migration graph in Prisma ORM metaDescription: 'How Prisma ORM models migrations as a graph of contract states, why that makes branching and parallel work safe, and the commands to inspect it.' --- -Consider this situation: you add a `phone` column to the schema on your branch. A teammate adds an `avatarUrl` column on theirs. Both branches merge the same afternoon. Now your laptop, your teammate's laptop, staging, and production each have a slightly different version of the schema. Something has to bring them all to the merged state, without losing work or running the same change twice. +Consider this situation: you add a `phone` column to the schema on your branch, a teammate adds an `avatarUrl` column on theirs, and both branches merge the same afternoon. Now your laptop, your teammate's laptop, staging, and production each have a slightly different version of the schema. Something has to bring them all to the merged state, without losing work or running the same change twice. Classic migration tools make this a complicated and time-consuming process. Migrations are kept in a single folder and run just once, in timestamp order; as a result, when multiple branches each add a migration, the timestamps interleave. This breaks the "run once in order" assumption, and someone has to spend an afternoon renaming files to force a clean line. Prisma ORM avoids this with a different approach: the migration graph. @@ -45,13 +45,13 @@ Usually you won't ever touch the graph directly. To see it, run `migration graph ## How it works -Every time you emit your contract, you get a deterministic JSON artifact. Hashing it produces an identifier for that exact schema shape. Those hashes are the **nodes** of the graph. +Every time you emit your contract, you get a deterministic JSON artifact, and hashing it produces an identifier for that exact schema shape. Those hashes are the **nodes** of the graph. -A migration is an **edge**. It records the contract hash it starts `from` and the hash it moves the database `to`. Nothing about a migration's place in history comes from its file name: the timestamps in directory names are for humans, and the real linkage lives in the `from`/`to` hashes inside each `migration.json`. +A migration is an **edge**: it records the contract hash it starts `from` and the hash it moves the database `to`. Nothing about a migration's place in history comes from its file name: the timestamps in directory names are for humans, and the real linkage lives in the `from`/`to` hashes inside each `migration.json`. -A database always sits at exactly one node. Its **marker**, a record Prisma ORM keeps in the database itself, holds the hash of the contract it currently matches. Applying a migration means walking one edge and moving the marker. +A database always sits at exactly one node: its **marker**, a record Prisma ORM keeps in the database itself, holds the hash of the contract it currently matches. Applying a migration means walking one edge and moving the marker. ## A worked example @@ -80,7 +80,7 @@ npx prisma@latest migration graph Read it bottom-up. From an empty database (`-`), `init` establishes state `705b1a6`. Alice and Bob both branch from `705b1a6`: her migration produces `93be6c2`, his produces `7e3fa7f`. Each branch then has a merge migration into the combined state `f9a41d7`, which the `prod` ref points at. This shape is a diamond: one start, two parallel branches, one shared end. -The payoff is in the last column and the two merge edges. A database that follows Alice's branch is sitting at `93be6c2` and reaches `f9a41d7` through `merge_alice`. A database that follows Bob's is at `7e3fa7f` and takes `merge_bob`. Neither developer needed to know about the other's migration, and no files were renamed to make the timestamps line up. +The payoff is in the last column and the two merge edges. A database that follows Alice's branch is sitting at `93be6c2` and reaches `f9a41d7` through `merge_alice`, while a database that follows Bob's is at `7e3fa7f` and takes `merge_bob`. Neither developer needed to know about the other's migration, and no files were renamed to make the timestamps line up. (The summary line counts contract *spaces*: independent migration lanes, one for your app plus one per [database extension](/orm/migrations/applying-a-migration#extension-spaces).) @@ -94,7 +94,7 @@ When you run `db migrate`, the runner walks the graph for you: 4. It applies each edge in order, moving the marker after each one. 5. If any migration's precheck fails, it stops before running SQL and names the mismatch. -If two branch tips are both reachable and neither is clearly the target, the runner does not guess. It stops and asks you to pick one with `--to`. +If two branch tips are both reachable and neither is clearly the target, the runner does not guess: it stops and asks you to pick one with `--to`. ## Inspecting the graph @@ -134,7 +134,7 @@ npx prisma@latest db migrate --to prod Because refs are committed, "where production should be" is a reviewable fact in a pull request instead of tribal knowledge in someone's terminal history. -One ref name is special. A ref called `db` is the default starting point `migration plan` uses when you do not pass `--from`. Keep it current and planning stays incremental on its own. `db init` and `db update` advance it after a successful run, but only when you leave `--db` off and let the connection come from `prisma.config.ts`; with `--db` you have to add `--advance-ref db`. `db sign` advances it whenever it signs, `--db` or not. At apply time, `db migrate --advance-ref db` moves it; plain `db migrate` never does. +One ref name is special: a ref called `db` is the default starting point `migration plan` uses when you do not pass `--from`. Keep it current and planning stays incremental on its own. `db init` and `db update` advance it after a successful run, but only when you leave `--db` off and let the connection come from `prisma.config.ts`; with `--db` you have to add `--advance-ref db`. `db sign` advances it whenever it signs, `--db` or not. At apply time, `db migrate --advance-ref db` moves it; plain `db migrate` never does. Three reserved tokens also work where a command accepts a contract reference, without setting up a ref: @@ -146,7 +146,7 @@ Three reserved tokens also work where a command accepts a contract reference, wi ### Parallel work without ordering conflicts -Alice and Bob each planned a migration from `705b1a6` on their own branches. In a timestamp-ordered system, whoever merges second inherits a broken history. Here, both edges are in the graph and every environment finds its own path to the merged state. This matters even more when the "two developers" are two AI agents planning migrations concurrently, since neither has to know anything at all about the other. +Alice and Bob each planned a migration from `705b1a6` on their own branches. In a timestamp-ordered system, whoever merges second inherits a broken history, but here both edges are in the graph and every environment finds its own path to the merged state. This matters even more when the "two developers" are two AI agents planning migrations concurrently, since neither has to know anything at all about the other. ### History you can trust @@ -166,7 +166,7 @@ A database that already exists can become the graph's starting node without repl For a deployed database you must not touch, `--from @empty` is the explicit retrofit: bring the contract source back to the deployed state, emit, plan the baseline, then restore the current contract and plan the delta from it. -A baseline is never replayed against a database that already carries a marker. The runner starts at the marker and applies only the edges past it. +A baseline is never replayed against a database that already carries a marker: the runner starts at the marker and applies only the edges past it. ## Database-specific details diff --git a/apps/docs/content/docs/orm/reference/index.mdx b/apps/docs/content/docs/orm/reference/index.mdx index 2114cbe9a2a..208090e5d7e 100644 --- a/apps/docs/content/docs/orm/reference/index.mdx +++ b/apps/docs/content/docs/orm/reference/index.mdx @@ -6,7 +6,7 @@ metaTitle: Prisma ORM API reference metaDescription: 'Reference index for the Prisma ORM client, SQL query builder, pipeline builder, raw queries, and runtime APIs.' --- -Prisma ORM gives you three typed ways to write a query. In Prisma ORM 7 you wrote `findMany({ where, include })`. Now each of those arguments is a method you chain: `.where(...).include(...)`. See [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7), which maps each argument to its method. +Prisma ORM gives you three typed ways to write a query. In Prisma ORM 7 you wrote `findMany({ where, include })`, but now each of those arguments is a method you chain: `.where(...).include(...)`. See [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7), which maps each argument to its method. - The **ORM client** is at `db.orm`. It has model methods such as `where()`, `create()`, and `include()`, it works on both PostgreSQL and MongoDB, and it covers everyday reads and writes across your models. - The **SQL query builder** is at `db.sql`. It has table methods that match SQL, such as `select()`, `innerJoin()`, and `groupBy()`, it is PostgreSQL only, and it returns whatever shape your `select()` asks for, not objects shaped like your models. @@ -16,9 +16,9 @@ When none of the three can express the query you need, write raw SQL on PostgreS ## Running a query -The ORM client sends its own queries. `first()`, `create()`, `update()`, `upsert()`, `delete()`, and `aggregate()` send the query the moment you call them. `all()`, `createAll()`, `updateAll()`, and `deleteAll()` return a result object and send the query when you `await` that object or loop over it with `for await`. `all()` is what replaced `findMany`. +The ORM client sends its own queries: `first()`, `create()`, `update()`, `upsert()`, `delete()`, and `aggregate()` send the query the moment you call them. `all()`, `createAll()`, `updateAll()`, and `deleteAll()` return a result object and send the query when you `await` that object or loop over it with `for await`. `all()` is what replaced `findMany`. -The SQL query builder and the pipeline builder send nothing on their own. `.build()` gives you a built query. Pass it to `runtime.query(...)` for rows, or to `runtime.execute(...)` for a write that returns no rows. Get `runtime` from `db.runtime()` once and reuse it. +The SQL query builder and the pipeline builder send nothing on their own: `.build()` gives you a built query. Pass it to `runtime.query(...)` for rows, or to `runtime.execute(...)` for a write that returns no rows. Get `runtime` from `db.runtime()` once and reuse it. ### PostgreSQL @@ -29,7 +29,7 @@ const plan = db.sql.public.user.select('id', 'email').where((f, fns) => fns.eq(f const rows = await runtime.query(plan); ``` -`User` is the model name and `user` is the table name. The ORM client compares on the column, `u.kind.eq(...)`. The SQL query builder compares through `fns`, `fns.eq(f.kind, ...)`. +`User` is the model name and `user` is the table name. The ORM client compares on the column, `u.kind.eq(...)`, while the SQL query builder compares through `fns`, `fns.eq(f.kind, ...)`. ### MongoDB @@ -40,7 +40,7 @@ const plan = db.query.from('users').match((f) => f.role.eq('admin')).build(); const docs = await runtime.query(plan); ``` -`db.runtime()` returns a promise on MongoDB, so `await` it. `users` is the collection name, and both `db.orm` and `db.query.from()` are keyed by it. The ORM client's `where()` takes an object of field values. The pipeline builder's `match()` takes a callback that compares on the field, `f.role.eq(...)`. Two different things are named `query`: `db.query` builds a pipeline, and `runtime.query(...)` runs a query that has already been built. +`db.runtime()` returns a promise on MongoDB, so `await` it. `users` is the collection name, and both `db.orm` and `db.query.from()` are keyed by it. The ORM client's `where()` takes an object of field values, while the pipeline builder's `match()` takes a callback that compares on the field, `f.role.eq(...)`. Two different things are named `query`: `db.query` builds a pipeline, and `runtime.query(...)` runs a query that has already been built. }>Every ORM client method, with PostgreSQL and MongoDB behavior documented side by side. diff --git a/apps/docs/content/docs/orm/reference/orm-client.mdx b/apps/docs/content/docs/orm/reference/orm-client.mdx index e8e3fc26037..102e35ffcbe 100644 --- a/apps/docs/content/docs/orm/reference/orm-client.mdx +++ b/apps/docs/content/docs/orm/reference/orm-client.mdx @@ -12,7 +12,7 @@ This page assumes your tables already exist. [Coming from Prisma ORM 7](/orm/com ## Example schema -All examples on this page run against the schema below, except the grouped-aggregate ones, which use a separate `Customer` / `Order` schema shown under [Grouped aggregates](#grouped-aggregates). `contract.prisma` replaces `schema.prisma`. This page calls it the contract. Running `npx prisma contract emit` compiles it into two generated files: `contract.json`, read at run time, and `contract.d.ts`, read by TypeScript. You import both, as the examples below do. Run `npx prisma contract emit` again after every change to `contract.prisma`. A new project keeps all three files in `src/prisma/`. +All examples on this page run against the schema below, except the grouped-aggregate ones, which use a separate `Customer` / `Order` schema shown under [Grouped aggregates](#grouped-aggregates). `contract.prisma` replaces `schema.prisma`, and this page calls it the contract. Running `npx prisma contract emit` compiles it into two generated files: `contract.json`, read at run time, and `contract.d.ts`, read by TypeScript. You import both, as the examples below do. Run `npx prisma contract emit` again after every change to `contract.prisma`. A new project keeps all three files in `src/prisma/`.
@@ -199,7 +199,7 @@ Some parts of the schema the examples rely on: ## Setting up the client -Create a client with `postgres(...)` or `mongo(...)`, then read and write your models through `db.orm`. The same `db` also has a query builder, `db.sql` on PostgreSQL and `db.query` on MongoDB, plus `db.raw` for raw queries. This page covers none of those three. See [Advanced queries](/orm/fundamentals/advanced-queries). Create `db` once per process, and close it with `await db.close()` when the process shuts down. The two databases have different entry points, and they name the models differently. +Create a client with `postgres(...)` or `mongo(...)`, then read and write your models through `db.orm`. The same `db` also has a query builder, `db.sql` on PostgreSQL and `db.query` on MongoDB, plus `db.raw` for raw queries. This page covers none of those three: see [Advanced queries](/orm/fundamentals/advanced-queries). Create `db` once per process, and close it with `await db.close()` when the process shuts down. The two databases have different entry points, and they name the models differently. ### PostgreSQL @@ -245,7 +245,7 @@ const users = await db.orm.users.all(); ## Query-building methods -These methods narrow a query. Each returns a collection. On this page, a collection is the query object you chain more methods on, such as `db.orm.public.User` or `db.orm.public.User.where(...)`. When this page says "a MongoDB collection", it means that query object. MongoDB itself calls its store of documents a collection too, and the name is the same one. You run the query with a [read method](#read-terminals) such as `all()` or `first()`. MongoDB supports fewer of these methods than PostgreSQL, and each method's Remarks say which databases it works on. +These methods narrow a query, and each returns a collection. On this page, a collection is the query object you chain more methods on, such as `db.orm.public.User` or `db.orm.public.User.where(...)`. When this page says "a MongoDB collection", it means that query object. MongoDB itself calls its store of documents a collection too, and the name is the same one. You run the query with a [read method](#read-terminals) such as `all()` or `first()`. MongoDB supports fewer of these methods than PostgreSQL, and each method's Remarks say which databases it works on. Some examples import a filter helper you can use on its own. On PostgreSQL those come from `@prisma/orm-postgres/orm-client`, on MongoDB from `@prisma/orm-mongo/query-ast/execution`. See [Filter conditions and operators](#filter-conditions-and-operators). @@ -408,7 +408,7 @@ For a task-oriented guide to loading related records, see [Relations and joins]( ### Refinements, aggregates, and combine -On PostgreSQL, the callback you pass to `include()` receives the related rows as a collection. This page calls that callback a refinement, which is the word in the heading above. You can filter, order, and paginate the related rows in it. You can also reduce them to a single value, or use `combine()` to return several results at once. To count or sum a whole model instead of a relation, see [Grouped aggregates](#grouped-aggregates), where `aggregate((agg) => ({ n: agg.count() }))` counts every row. +On PostgreSQL, the callback you pass to `include()` receives the related rows as a collection. This page calls that callback a refinement, which is the word in the heading above. You can filter, order, and paginate the related rows in it, and you can also reduce them to a single value, or use `combine()` to return several results at once. To count or sum a whole model instead of a relation, see [Grouped aggregates](#grouped-aggregates), where `aggregate((agg) => ({ n: agg.count() }))` counts every row. #### Remarks @@ -493,7 +493,7 @@ const users = await db.orm.public.User.include('posts', (posts) => ``` :::note[`min()` and `max()` over a date column] -`min('createdAt')` over a `DateTime` column compiles. It returns a `Temporal.Instant`, the standard JavaScript object for a point in time. Not every JavaScript engine has `Temporal` yet. Check with `typeof Temporal === 'undefined'`, and if yours does not, install `temporal-polyfill` and add `import 'temporal-polyfill/full/global'`. +`min('createdAt')` over a `DateTime` column compiles and returns a `Temporal.Instant`, the standard JavaScript object for a point in time. Not every JavaScript engine has `Temporal` yet. Check with `typeof Temporal === 'undefined'`, and if yours does not, install `temporal-polyfill` and add `import 'temporal-polyfill/full/global'`. `sum('createdAt')` and `avg('createdAt')` do not compile. ::: @@ -747,9 +747,9 @@ const tutorials = await db.orm.posts.variant('Tutorial').all(); ## Read methods [#read-terminals] -A read method runs the query and gives you the rows. `all()` and `first()` are available on both databases. `aggregate()` and `groupBy()` are PostgreSQL only and are documented under [Grouped aggregates](#grouped-aggregates). +A read method runs the query and gives you the rows. `all()` and `first()` are available on both databases, while `aggregate()` and `groupBy()` are PostgreSQL only and are documented under [Grouped aggregates](#grouped-aggregates). -There is no `count()` method on either database. On PostgreSQL, count with `aggregate((a) => ({ n: a.count() }))`. On MongoDB, counting takes two steps. `db.query`, the [pipeline builder](/orm/fundamentals/advanced-queries#mongodb-pipeline-builder), builds the pipeline, and `runtime.query(...)` runs it: +There is no `count()` method on either database, so on PostgreSQL, count with `aggregate((a) => ({ n: a.count() }))`. On MongoDB, counting takes two steps: `db.query`, the [pipeline builder](/orm/fundamentals/advanced-queries#mongodb-pipeline-builder), builds the pipeline, and `runtime.query(...)` runs it. ```typescript const built = db.query.from('posts').count('total').build(); @@ -912,14 +912,14 @@ const features = await tasks.Task.features().all(); ## Write methods [#mutation-terminals] -A write method changes the database. `create`, `createAll`, `createAndCount`, `update`, `updateAll`, `updateAndCount`, `delete`, `deleteAll`, `deleteAndCount`, and `upsert` are available on both databases. The differences between the two are listed under each method. +A write method changes the database. `create`, `createAll`, `createAndCount`, `update`, `updateAll`, `updateAndCount`, `delete`, `deleteAll`, `deleteAndCount`, and `upsert` are available on both databases, and the differences between the two are listed under each method. For Prisma ORM 7 users: `createMany` is `createAll()` or `createAndCount()`, `updateMany` is `updateAll()` or `updateAndCount()`, `deleteMany` is `deleteAll()` or `deleteAndCount()`, and `count` is [`aggregate()`](#grouped-aggregates) on PostgreSQL, or the two steps shown under [Read methods](#read-terminals) on MongoDB. There is no `skipDuplicates` option. To catch one of the errors named below, wrap the call in `try` and `catch`, then compare `error.code` with the string given here. For a task-oriented walkthrough, see [Writing data](/orm/fundamentals/writing-data). On MongoDB, a write method rejects a chain that already has `orderBy()`, `limit()`, or `offset()` on it, and every write except `updateAll()` and `deleteAll()` also rejects `include()`. Both throw an error whose `code` is `ORM.OPERATION_UNSUPPORTED`. For grouping several writes into one unit, see [Transactions](/orm/fundamentals/transactions). :::warning[Filter before you update or delete] -Always call `where()` before an update or a delete. On MongoDB the six update and delete methods and `upsert()` check for a filter and throw an error whose `code` is `ORM.WHERE_MISSING`, with a message naming the method you called, such as `updateAll() requires a .where() filter. Call .where() before .updateAll()`. On PostgreSQL, these methods do not compile without a `where()`. Nothing checks at run time. If a call without a filter does reach the database, `update()` and `delete()` change a single row and you cannot predict which one, and `updateAll()` and `deleteAll()` change every row. +Always call `where()` before an update or a delete. On MongoDB the six update and delete methods and `upsert()` check for a filter and throw an error whose `code` is `ORM.WHERE_MISSING`, with a message naming the method you called, such as `updateAll() requires a .where() filter. Call .where() before .updateAll()`. On PostgreSQL, these methods do not compile without a `where()`, but nothing checks at run time. If a call without a filter does reach the database, `update()` and `delete()` change a single row and you cannot predict which one, and `updateAll()` and `deleteAll()` change every row. ::: The examples below use `aliceId`, `bobId`, `carolId`, `postId`, `tagId`, `tutorialId`, and `userId` for the ids of rows that already exist. Substitute your own. @@ -1433,7 +1433,7 @@ For Prisma ORM 7 users, `upsert()` replaces the `where` argument with `conflictO ## Grouped aggregates -These methods exist only on PostgreSQL collections. `aggregate()` treats every row the query matches as one group. `groupBy().aggregate()` aggregates per group, and `having()` filters those groups. A MongoDB model's collection has none of them. To aggregate on MongoDB, use the pipeline builder, which is Prisma ORM's API for MongoDB's aggregation pipeline. It is described in [MongoDB: Pipeline builder](/orm/fundamentals/advanced-queries#mongodb-pipeline-builder). +These methods exist only on PostgreSQL collections. `aggregate()` treats every row the query matches as one group, while `groupBy().aggregate()` aggregates per group and `having()` filters those groups. A MongoDB model's collection has none of them, so to aggregate on MongoDB, use the pipeline builder, which is Prisma ORM's API for MongoDB's aggregation pipeline and is described in [MongoDB: Pipeline builder](/orm/fundamentals/advanced-queries#mongodb-pipeline-builder). The examples below use a separate `Customer` / `Order` schema, where `Order.amount` is a numeric column: @@ -1487,7 +1487,7 @@ Compute aggregates over the rows the query matches. `aggregate()` runs the query | `Int`, `BigInt` | `number \| null` | `number \| null` | `sumBigInt()`, `avgDecimal()` | | `Decimal` | `string \| null` | `string \| null` | `avgDecimal()` | -Use `sumBigInt()` when the total can pass `Number.MAX_SAFE_INTEGER`. A `sum()` whose total passes it throws an error whose `code` is `RUNTIME.DECODE_FAILED`, thrown when the query result is read. Use `avgDecimal()` when you need the exact mean as a string, such as `'10.3333333333333333'`. Over a `Decimal` column `sum()` and `avg()` are already exact, and `sumBigInt()` is not available. +Use `sumBigInt()` when the total can pass `Number.MAX_SAFE_INTEGER`, because a `sum()` whose total passes it throws an error whose `code` is `RUNTIME.DECODE_FAILED`, thrown when the query result is read. Use `avgDecimal()` when you need the exact mean as a string, such as `'10.3333333333333333'`. Over a `Decimal` column `sum()` and `avg()` are already exact, and `sumBigInt()` is not available. #### Options @@ -1654,7 +1654,7 @@ const bigSpenders = await db.orm.public.Order.groupBy('customerId') ## Filter conditions and operators -A condition says which rows match. You write one inside `where()`, and inside a relation filter such as `u.posts.some(...)`. If you call `where()` more than once, the conditions are ANDed. One call can take the object form and the next the callback form. PostgreSQL and MongoDB write conditions differently. +A condition says which rows match. You write one inside `where()`, and inside a relation filter such as `u.posts.some(...)`. If you call `where()` more than once, the conditions are ANDed, and one call can take the object form and the next the callback form. PostgreSQL and MongoDB write conditions differently. - **PostgreSQL** compares a field with a method on that field, such as `u.email.eq(...)`, inside a callback. You name the callback's argument yourself, so the `u` and `p` below are only names. To combine conditions, import `and`, `or`, `not`, and `all` from `@prisma/orm-postgres/orm-client`. - **MongoDB** has no callback form. `where()` takes either the shorthand object or a filter such as `MongoFieldFilter.eq('name', 'Alice')`. The full list is under [`MongoFieldFilter`](#mongofieldfilter). Import `MongoFieldFilter`, `MongoAndExpr`, and `MongoOrExpr` from `@prisma/orm-mongo/query-ast/execution`. To combine conditions, use `.and()`, `.not()`, and `MongoOrExpr.of([...])`, described under [Combinators](#combinators). @@ -1917,7 +1917,7 @@ import { MongoFieldFilter } from '@prisma/orm-mongo/query-ast/execution'; const usersInSf = await db.orm.users.where(MongoFieldFilter.eq('address.city', 'San Francisco')).all(); ``` -`MongoFieldFilter.eq` is the recommended form. The object form needs a cast, and the cast drops type checking for every key in that object: +`MongoFieldFilter.eq` is the recommended form, because the object form needs a cast, and the cast drops type checking for every key in that object: ```typescript const usersInSf = await db.orm.users @@ -1926,12 +1926,12 @@ const usersInSf = await db.orm.users ``` ## Field update operations -On MongoDB, `update()`, `updateAll()`, `updateAndCount()`, and `upsert()` take a callback in place of a data object. The callback gets one argument, the update builder, written `u` in these examples, and returns an array of the operations you want. `updateAndCount()` takes the callback in the same place as `update()`. `upsert()` takes it as its `update` key, beside `create`. PostgreSQL has no callback form: pass an object to [`update()`](#update) instead. +On MongoDB, `update()`, `updateAll()`, `updateAndCount()`, and `upsert()` take a callback in place of a data object. The callback gets one argument, the update builder, written `u` in these examples, and returns an array of the operations you want. `updateAndCount()` takes the callback in the same place as `update()`, while `upsert()` takes it as its `update` key, beside `create`. PostgreSQL has no callback form: pass an object to [`update()`](#update) instead. - For a top-level field, read it off the update builder as a property: `u.bio.set('Hello')`. - For a field inside an embedded object, call the update builder as a function with the dot path: `u('address.city').set('San Francisco')`. The `upsert()` callback cannot use a dot path. Given one, `upsert()` throws an error whose `code` is `ORM.OPERATION_UNSUPPORTED`. Set the whole embedded object on its top-level field instead, as in `u.address.set({ street: '1 Market St', city: 'San Francisco', zip: '94105', country: 'US' })`. Give every field of the embedded object, including the optional ones. -All four methods require `.where()`. Without one they throw an error whose `code` is `ORM.WHERE_MISSING`. `.where({})` does not satisfy that rule, because an empty object adds no condition. To change every document, pass a filter that matches them all, such as `MongoFieldFilter.isNotNull('_id')`. A `_id` filter takes the hex string that a read gives you back, as the examples below show. On `upsert()`, `.where()` picks the document to update, exactly as on `update()`, and the full example is under [`upsert()`](#upsert). `update()` returns the updated document, or `null` when nothing matched. `updateAll()` returns an [`AsyncIterableResult`](#asynciterableresult) of the changed documents. [`updateAndCount()`](#updateandcount) exists on both databases, and it returns the number of changed documents. +All four methods require `.where()`, and without one they throw an error whose `code` is `ORM.WHERE_MISSING`. `.where({})` does not satisfy that rule, because an empty object adds no condition. To change every document, pass a filter that matches them all, such as `MongoFieldFilter.isNotNull('_id')`. A `_id` filter takes the hex string that a read gives you back, as the examples below show. On `upsert()`, `.where()` picks the document to update, exactly as on `update()`, and the full example is under [`upsert()`](#upsert). `update()` returns the updated document, or `null` when nothing matched, and `updateAll()` returns an [`AsyncIterableResult`](#asynciterableresult) of the changed documents. [`updateAndCount()`](#updateandcount) exists on both databases, and it returns the number of changed documents. ### Available operations (MongoDB) @@ -1950,7 +1950,7 @@ These examples use the MongoDB accessors from [Setting up the client](#setting-u ##### `set()` (MongoDB) -Return as many operations as you want. They are applied together, in one write. +Return as many operations as you want, and they are applied together, in one write. ```typescript const updated = await db.orm.users @@ -2044,7 +2044,7 @@ The methods `all()` / `createAll()` / `updateAll()` / `deleteAll()` return an `A - `await` the result (or call `.toArray()`) to get every row in one array. - Loop over it with `for await ... of` to take rows one at a time. -`createAll()`, `updateAll()`, and `deleteAll()` exist on PostgreSQL as well as MongoDB. [Write methods](#mutation-terminals) documents what each one takes, with examples. If you need the type by name, import it from `@prisma/orm-postgres/components/runtime` on PostgreSQL, or from `@prisma/orm-mongo/components/runtime` on MongoDB. +`createAll()`, `updateAll()`, and `deleteAll()` exist on PostgreSQL as well as MongoDB, and [Write methods](#mutation-terminals) documents what each one takes, with examples. If you need the type by name, import it from `@prisma/orm-postgres/components/runtime` on PostgreSQL, or from `@prisma/orm-mongo/components/runtime` on MongoDB. #### Single consumption and mode switching @@ -2053,7 +2053,7 @@ Each result is read once, one way: - Re-`await`ing (or calling `.toArray()` on) a result you already awaited is **safe**. It returns the same array. - Looping a result a second time with `for await` throws an error whose `code` is `RUNTIME.ITERATOR_CONSUMED`. So does switching from one way to the other, such as awaiting a result and then looping it. The message contains `already been consumed`. To recognise the error, import `isRuntimeError` from `@prisma/orm-postgres/components/runtime` on PostgreSQL, or from `@prisma/orm-mongo/components/runtime` on MongoDB, and check `error.code`. -PostgreSQL and MongoDB behave the same here. The example below uses the PostgreSQL accessor. On MongoDB the same methods are on `db.orm.users`, see [Setting up the client](#setting-up-the-client). `all()` returns the result object straight away, and the `await` is what runs the query, so the example holds the result in a variable. +PostgreSQL and MongoDB behave the same here, and the example below uses the PostgreSQL accessor. On MongoDB the same methods are on `db.orm.users`, see [Setting up the client](#setting-up-the-client). `all()` returns the result object straight away, and the `await` is what runs the query, so the example holds the result in a variable. ```typescript import { isRuntimeError } from '@prisma/orm-postgres/components/runtime'; diff --git a/apps/docs/content/docs/orm/reference/pipeline-builder.mdx b/apps/docs/content/docs/orm/reference/pipeline-builder.mdx index af9175d0d3d..3d28460be8e 100644 --- a/apps/docs/content/docs/orm/reference/pipeline-builder.mdx +++ b/apps/docs/content/docs/orm/reference/pipeline-builder.mdx @@ -8,7 +8,7 @@ metaDescription: 'Reference for the Prisma ORM MongoDB pipeline builder''s stage The pipeline builder gives you a typed way to build MongoDB aggregation pipelines. You reach it through `db.query`, chain aggregation stages onto a starting collection, and finish the chain with the call that builds the query. It is the MongoDB counterpart to the [SQL query builder](/orm/reference/sql-query-builder): a lower-level API for the queries the [ORM client](/orm/reference/orm-client) doesn't express. -Everything the builder produces becomes a MongoDB aggregation pipeline. There is no `find()` or `distinct()` here. Use `db.orm` or [`rawCommand()`](#rawcommand) for those. To fetch a single document, filter and then `limit(1)`. Use the ORM client (`db.orm`) for everyday reads and writes across models and relations. Reach for the pipeline builder when you need to group documents, to join another collection and keep transforming the result, or to write the result into a collection (`$out` and `$merge`). This page documents every stage, accumulator, expression helper, and write method. +Everything the builder produces becomes a MongoDB aggregation pipeline. There is no `find()` or `distinct()` here, so use `db.orm` or [`rawCommand()`](#rawcommand) for those. To fetch a single document, filter and then `limit(1)`. Use the ORM client (`db.orm`) for everyday reads and writes across models and relations. Reach for the pipeline builder when you need to group documents, to join another collection and keep transforming the result, or to write the result into a collection (`$out` and `$merge`). This page documents every stage, accumulator, expression helper, and write method. :::note[This is the MongoDB pipeline builder] The pipeline builder targets MongoDB only. PostgreSQL has no pipeline builder: use the [SQL query builder](/orm/reference/sql-query-builder) for PostgreSQL joins and aggregates, and the [ORM client](/orm/reference/orm-client) for everyday queries on either database. @@ -79,7 +79,7 @@ model Tutorial { ## Entry points -Every pipeline starts with `db.query.from('posts')`, naming the collection to aggregate over. It ends with a call that turns the chain into a query you can run. Between them you chain stages. +Every pipeline starts with `db.query.from('posts')`, naming the collection to aggregate over. It ends with a call that turns the chain into a query you can run, and between them you chain stages. `db` is the client you create with `mongo(...)`, whose `dbName` option is the name of the MongoDB database to use. See [Transactions and runtime](/orm/reference/transactions-and-runtime#mongooptions) for every option and for how to create the client. Three Prisma ORM import paths appear on this page: `@prisma/orm-mongo/runtime` holds the `mongo()` client, `@prisma/orm-mongo/query-builder` holds `mongoQuery`, `fn`, `acc`, and `expr`, and `@prisma/orm-mongo/query-ast/execution` holds the `Mongo*Stage` classes, `MongoAggFieldRef`, and `MongoFieldFilter`. Every example holds the built query in a variable named `plan`. @@ -203,7 +203,7 @@ const recent = await runtime.query(plan); ``` :::warning[`_id` equality filters do not work through the pipeline builder] -Filtering by `_id` equality inside `match()` **never matches any document**. Neither a hex string (`f._id.eq('507f...')`) nor an `ObjectId` matches. There is no supported way to filter by `_id` equality through the typed pipeline builder. To fetch one document by its id, use the [ORM client](/orm/reference/orm-client), which converts the hex string for you. The pipeline builder does not convert it: +Filtering by `_id` equality inside `match()` **never matches any document**: neither a hex string (`f._id.eq('507f...')`) nor an `ObjectId` matches. There is no supported way to filter by `_id` equality through the typed pipeline builder. To fetch one document by its id, use the [ORM client](/orm/reference/orm-client), which converts the hex string for you, while the pipeline builder does not convert it: ```typescript const postId = '6650f1c2a1b2c3d4e5f60003'; @@ -833,7 +833,7 @@ const [{ firstTitle, lastTitle }] = await runtime.query(plan); ##### `push()` and `addToSet()` -Both take one expression: `group((f) => ({ _id: null, allTitles: acc.push(f.title), distinctKinds: acc.addToSet(f.kind) }))`. `allTitles` holds one entry per document. `distinctKinds` holds each value once. +Both take one expression: `group((f) => ({ _id: null, allTitles: acc.push(f.title), distinctKinds: acc.addToSet(f.kind) }))`. `allTitles` holds one entry per document, while `distinctKinds` holds each value once. ##### `firstN()` (an `N`-variant) @@ -851,7 +851,7 @@ const [{ firstTwo, newestTitle }] = await runtime.query(plan); ## Expression helpers -Expression helpers (`fn.*`) build the computed values used inside stages like `addFields()`, `project()`, `group()` accumulators, and `match()` (via `expr()`). Inside a stage, write `f.title` or `fn.toUpper(...)`, never `'hello'` or `3` on their own. Wrap a constant in `fn.literal('hello')`. The update operators on the [write methods](#write-methods) and the `MongoFieldFilter` methods work the other way round. They take ordinary JavaScript values, so write `f.bio.set('hello')`, not `f.bio.set(fn.literal('hello'))`. Import the helpers from `@prisma/orm-mongo/query-builder`: +Expression helpers (`fn.*`) build the computed values used inside stages like `addFields()`, `project()`, `group()` accumulators, and `match()` (via `expr()`). Inside a stage, write `f.title` or `fn.toUpper(...)`, never `'hello'` or `3` on their own, so wrap a constant in `fn.literal('hello')`. The update operators on the [write methods](#write-methods) and the `MongoFieldFilter` methods work the other way round: they take ordinary JavaScript values, so write `f.bio.set('hello')`, not `f.bio.set(fn.literal('hello'))`. Import the helpers from `@prisma/orm-mongo/query-builder`: ```typescript import { fn } from '@prisma/orm-mongo/query-builder'; @@ -1063,7 +1063,7 @@ const [insertManyResult] = await runtime.query(manyPlan); ##### `upsertOne()` on the root (filter, then updater) -`upsertOne(filterCallback, updaterCallback)`. The first callback returns the filter. The second returns the array of update operations. +`upsertOne(filterCallback, updaterCallback)`. The first callback returns the filter, and the second returns the array of update operations. ```typescript const plan = db.query.from('users') @@ -1113,7 +1113,7 @@ const [result] = await runtime.query(plan); ##### `findOneAndUpdate()` and `returnDocument` -`findOneAndUpdate()` returns the matched document. Its second argument is optional. That argument takes `returnDocument`, which controls which version you get back: `'before'` returns the document before the update, `'after'` returns it after. The default is `'after'`. The same argument takes `upsert`, which defaults to `false` and inserts a document when nothing matches. +`findOneAndUpdate()` returns the matched document. Its second argument is optional and takes `returnDocument`, which controls which version you get back: `'before'` returns the document before the update, `'after'` returns it after. The default is `'after'`. The same argument takes `upsert`, which defaults to `false` and inserts a document when nothing matches. ```typescript const plan = db.query.from('users').match((f) => f.email.eq('alice@example.com')) @@ -1123,7 +1123,7 @@ const [beforeDoc] = await runtime.query(plan); ``` :::warning[The option is `returnDocument`, not `returnNewDocument`] -Use `returnDocument: 'before' | 'after'`. `returnNewDocument` is not a valid option name. TypeScript rejects it and suggests `returnDocument`. If a cast gets it past TypeScript, the unknown key is ignored and you silently get the default `'after'`. +Use `returnDocument: 'before' | 'after'`. `returnNewDocument` is not a valid option name, so TypeScript rejects it and suggests `returnDocument`. If a cast gets it past TypeScript, the unknown key is ignored and you silently get the default `'after'`. ::: ##### `findOneAndDelete()` diff --git a/apps/docs/content/docs/orm/reference/raw-queries.mdx b/apps/docs/content/docs/orm/reference/raw-queries.mdx index 251a6205bb4..30e9c402311 100644 --- a/apps/docs/content/docs/orm/reference/raw-queries.mdx +++ b/apps/docs/content/docs/orm/reference/raw-queries.mdx @@ -19,7 +19,7 @@ A **raw fragment** is a piece of SQL you put inside a builder query. Write it as A **whole raw statement** is a complete SQL statement written with `db.raw.sql`. If it returns rows, end it with `.returnsRow(spec).build()`, where the spec is a column list [defined below](#the-client-level-dbrawsql-tag), and pass the result to `runtime.query(...)`. If it does not, end it with `.affectedCount().build()` and pass the result to `runtime.execute(...)`. :::warning[Only `.returnsRow()` converts values for you] -A type id such as `pg/text@1` tells Prisma ORM what JavaScript type a column should become. The property is named `codecId`. `.returnsRow(spec)` names a type id for every column of a whole `db.raw.sql` statement, so you get real JavaScript values back. A fragment's `.returns(...)` does not convert anything. It only tells TypeScript what type to expect, so the value is whatever the `pg` package, the PostgreSQL driver underneath, gives you. For a `text` or an `integer` column that is already a JavaScript string or number. A `numeric` column arrives as a string. A date column gets no conversion by Prisma ORM, so you get the `pg` package's own value for that column. +A type id such as `pg/text@1` tells Prisma ORM what JavaScript type a column should become, and the property is named `codecId`. `.returnsRow(spec)` names a type id for every column of a whole `db.raw.sql` statement, so you get real JavaScript values back. A fragment's `.returns(...)` does not convert anything: it only tells TypeScript what type to expect, so the value is whatever the `pg` package, the PostgreSQL driver underneath, gives you. For a `text` or an `integer` column that is already a JavaScript string or number, while a `numeric` column arrives as a string. A date column gets no conversion by Prisma ORM, so you get the `pg` package's own value for that column. ::: The examples in this section run against the `user` / `post` schema from the SQL query builder page. See its [example schema](/orm/reference/sql-query-builder#example-schema) for the full model definitions. As on that page, create the client with `postgres(...)` and reach tables through `db.sql.public`, which is keyed by table name (the model name with a lowercase first letter unless the model sets the `@@map` attribute, which renames a table). `db.runtime()` gives you the connection that runs a built query. @@ -47,7 +47,7 @@ const rows = await runtime.query(plan); // rows === [{ id: aliceId, upperEmail: 'ALICE@EXAMPLE.COM' }] ``` -`.returns(...)` only tells TypeScript what to expect. It does not cast the value in SQL and it does not change how the `pg` package reads it. It takes a type id string, or an object of the form `{ codecId, nullable }`. See [Binding a bare value with `param()`](#binding-a-bare-value-with-param) for the type ids you are most likely to write, and the SQL query builder's [`fns.raw` and `.returns()`](/orm/reference/sql-query-builder#fnsraw-and-returns) for the full treatment. +`.returns(...)` only tells TypeScript what to expect: it does not cast the value in SQL and it does not change how the `pg` package reads it. It takes a type id string, or an object of the form `{ codecId, nullable }`. See [Binding a bare value with `param()`](#binding-a-bare-value-with-param) for the type ids you are most likely to write, and the SQL query builder's [`fns.raw` and `.returns()`](/orm/reference/sql-query-builder#fnsraw-and-returns) for the full treatment. ### `fns.raw` as a `where()` predicate @@ -133,7 +133,7 @@ const plan = db.sql.public.user const rows = await runtime.query(plan); ``` -Use `.returnsRow(spec)` to run a whole statement and get converted rows back. The spec names the columns of every returned row. Each entry is either a column from your contract or a type id. A column such as `db.sql.public.user.columns.id` brings its own conversion, nullability, and TypeScript type. Write a type id for a column your contract has no match for, and the object form `{ codecId: 'pg/text@1', nullable: true }` when that column can be `NULL`, as a `LEFT JOIN` column can. If a returned row has no column your spec names, the query throws an error whose `code` is `RUNTIME.RAW_ROW_COLUMN_MISSING`. A failing statement throws a `SqlQueryError` whose `sqlState` is the PostgreSQL error code for the failure, such as `23505` for a unique-constraint violation. +Use `.returnsRow(spec)` to run a whole statement and get converted rows back. The spec names the columns of every returned row, and each entry is either a column from your contract or a type id. A column such as `db.sql.public.user.columns.id` brings its own conversion, nullability, and TypeScript type. Write a type id for a column your contract has no match for, and the object form `{ codecId: 'pg/text@1', nullable: true }` when that column can be `NULL`, as a `LEFT JOIN` column can. If a returned row has no column your spec names, the query throws an error whose `code` is `RUNTIME.RAW_ROW_COLUMN_MISSING`. A failing statement throws a `SqlQueryError` whose `sqlState` is the PostgreSQL error code for the failure, such as `23505` for a unique-constraint violation. ```ts const user = db.sql.public.user; @@ -154,7 +154,7 @@ const rows = await runtime.query(plan); // row.email is a string; row.postCount is a bigint ``` -Use `.affectedCount()` for a statement that returns no rows. It takes no spec. Pass the built query to `runtime.execute(...)`, which resolves to `{ affectedRows }`, the number of rows the statement changed. You can also run a built raw query inside a transaction: [`db.transaction()`](/orm/reference/transactions-and-runtime#dbtransactioncallback) gives you a `tx`, and `tx.query(...)` and `tx.execute(...)` take the same built queries as `runtime.query(...)` and `runtime.execute(...)` do. +Use `.affectedCount()`, which takes no spec, for a statement that returns no rows. Pass the built query to `runtime.execute(...)`, which resolves to `{ affectedRows }`, the number of rows the statement changed. You can also run a built raw query inside a transaction: [`db.transaction()`](/orm/reference/transactions-and-runtime#dbtransactioncallback) gives you a `tx`, and `tx.query(...)` and `tx.execute(...)` take the same built queries as `runtime.query(...)` and `runtime.execute(...)` do. ```ts const plan = db.raw.sql` @@ -167,7 +167,7 @@ const stats = await runtime.execute(plan); // stats.affectedRows ``` -You can nest a `.returnsRow()` query inside another. That is how you write a subquery or a CTE, which is a `WITH` query. Interpolate it before `.build()`, and call `.build()` on the outer one only. Its `.returns` property holds its declared columns, so `outer.returnsRow({ postCount: inner.returns.postCount })` reuses a declaration. You cannot nest a statement that ends with `.affectedCount()`, because it produces no rows. You can nest an `UPDATE ... RETURNING`, because it does. +You can nest a `.returnsRow()` query inside another: that is how you write a subquery or a CTE, which is a `WITH` query. Interpolate it before `.build()`, and call `.build()` on the outer one only. Its `.returns` property holds its declared columns, so `outer.returnsRow({ postCount: inner.returns.postCount })` reuses a declaration. You cannot nest a statement that ends with `.affectedCount()`, because it produces no rows. You can nest an `UPDATE ... RETURNING`, because it does. :::note[Three names look alike] `.returns(typeId)` is a method on a fragment, `.returnsRow(spec)` is a method on a statement, and `.returns` is a property on the object `.returnsRow()` gives you. @@ -204,7 +204,7 @@ To bind any value whose type is not in the list, wrap it in [`param(value, { cod `db.raw.collection(collectionName)` returns an object with nine methods for running MongoDB commands directly against a collection. The collection name is the one your contract declares (`'users'`, `'posts'`), the same name the ORM client uses (`db.orm.users`). MongoDB has no schemas, so there is no `public` segment here and none on `db.orm`. There is no raw `find()`, so use `aggregate()` with a `$match` stage. MongoDB has no transactions in Prisma ORM, so a raw command cannot run inside one. See [Transactions (MongoDB)](/orm/reference/transactions-and-runtime#transactions-mongodb). -Each of these nine methods builds a command. Call `.build()` on it, then run every one of them with `runtime.query(...)`, including the writes. The raw collection methods all run through `query()`. A write returns a one-element array, so destructure it with `const [result] = ...`. +Each of these nine methods builds a command: call `.build()` on it, then run every one of them with `runtime.query(...)`, including the writes. The raw collection methods all run through `query()`. A write returns a one-element array, so destructure it with `const [result] = ...`. :::warning[MongoDB raw results are native BSON values] Nothing on the MongoDB side of this page is converted for you. An `_id` comes back as it is stored: one that MongoDB generated is an `ObjectId` instance from the `mongodb` package, the MongoDB driver underneath, not a hex string. Compare one with `String(row._id) === aliceId`. Raw filters take native BSON values too, so build ids with that package's `ObjectId` class. The write methods give you the counts and ids under the `mongodb` package's own key names, `{ insertedId }`, `{ matchedCount, modifiedCount, upsertedCount, upsertedId }`, and `{ deletedCount }`, and you read those keys directly. @@ -250,7 +250,7 @@ const [manyResult] = await runtime.query(manyPlan); ### `updateOne()` and `updateMany()` -Update one or every matching document. The first argument is a raw filter document. The second is either an update document such as `{ $set: ... }` or an aggregation pipeline, which is an array of stages. Both return `{ matchedCount, modifiedCount, upsertedCount, upsertedId }`. +Update one or every matching document. The first argument is a raw filter document, and the second is either an update document such as `{ $set: ... }` or an aggregation pipeline, which is an array of stages. Both return `{ matchedCount, modifiedCount, upsertedCount, upsertedId }`. #### Update document form @@ -346,7 +346,7 @@ const rows = (await runtime.query(plan)) as { total: number }[]; // [{ total: 2 }] ``` -`rawCommand()` is the way out when the typed pipeline builder can't express a query, such as filtering by `_id` equality. The command you pass is sent unchanged, so a real `ObjectId` in a pipeline document matches. `aggregate()` on `db.raw.collection()` handles `_id` filtering too, and it gives you a row type. Use `rawCommand()` when you are already working in `db.query` and need one raw command there. For the full treatment see the pipeline builder's [`rawCommand()`](/orm/reference/pipeline-builder#rawcommand) section. +`rawCommand()` is the way out when the typed pipeline builder can't express a query, such as filtering by `_id` equality. The command you pass is sent unchanged, so a real `ObjectId` in a pipeline document matches. `aggregate()` on `db.raw.collection()` handles `_id` filtering too, and it gives you a row type, so use `rawCommand()` when you are already working in `db.query` and need one raw command there. For the full treatment see the pipeline builder's [`rawCommand()`](/orm/reference/pipeline-builder#rawcommand) section. ```ts const plan = db.query.rawCommand(new RawAggregateCommand('posts', [{ $match: { _id: new ObjectId('6539a1f2c3d4e5f60718293c') } }])); diff --git a/apps/docs/content/docs/orm/reference/sql-query-builder.mdx b/apps/docs/content/docs/orm/reference/sql-query-builder.mdx index 138417b8dfb..d9e2f4d488c 100644 --- a/apps/docs/content/docs/orm/reference/sql-query-builder.mdx +++ b/apps/docs/content/docs/orm/reference/sql-query-builder.mdx @@ -130,7 +130,7 @@ model Feature { In Prisma ORM 8 your `schema.prisma` is called `contract.prisma`. [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7) gets you from a Prisma ORM 7 project to that file, and has the rest of the setup steps. Install the client with `npm install @prisma/orm-postgres`. Run `npx prisma contract emit` to write `contract.json` and `contract.d.ts` beside `contract.prisma`. The examples below assume your own file is in `src/prisma/`, next to those two files, and they write the type import path as `./contract.d`, exactly like that. -You build queries from the client's `sql` property. Create a Postgres client with `postgres(...)`, then reach tables through `db.sql.public`. `public` is the PostgreSQL schema your tables are in. A model goes in `public` unless your contract puts it in a `namespace` block. Table accessors are keyed by table name, not model name. A table name is the model name with a lowercase first letter unless the model sets `@@map`, so the `User` model mapped to `user` is reached as `db.sql.public.user`, and the model for the `Post`/`Tag` join table, mapped to `post_tag`, is reached as `db.sql.public.post_tag`. Column names are the field names unless a field sets `@map`. +You build queries from the client's `sql` property. Create a Postgres client with `postgres(...)`, then reach tables through `db.sql.public`. `public` is the PostgreSQL schema your tables are in, and a model goes in `public` unless your contract puts it in a `namespace` block. Table accessors are keyed by table name, not model name. A table name is the model name with a lowercase first letter unless the model sets `@@map`, so the `User` model mapped to `user` is reached as `db.sql.public.user`, and the model for the `Post`/`Tag` join table, mapped to `post_tag`, is reached as `db.sql.public.post_tag`. Column names are the field names unless a field sets `@map`. ### The `db.sql` builder [#the-dbsql-facet] @@ -150,7 +150,7 @@ const plan = db.sql.public.user.select('id', 'email').build(); const users = await runtime.query(plan); ``` -SQLite works the same way. Create the client with `sqlite(...)` from `@prisma/orm-sqlite/runtime`. SQLite has no schemas, so there is no `public` in the path and the same table is `db.sql.user`. Of the SELECT methods on this page, `lateralJoin()`, `outerLateralJoin()`, and `distinctOn()` are not available on SQLite. Every other one is. The rest of this page shows PostgreSQL and writes the root as `db.sql.public`. +SQLite works the same way: create the client with `sqlite(...)` from `@prisma/orm-sqlite/runtime`. SQLite has no schemas, so there is no `public` in the path and the same table is `db.sql.user`. Of the SELECT methods on this page, `lateralJoin()`, `outerLateralJoin()`, and `distinctOn()` are not available on SQLite, but every other one is. The rest of this page shows PostgreSQL and writes the root as `db.sql.public`. ### Table access @@ -167,11 +167,11 @@ Call `.as(alias)` on a `SELECT` chain before `build()` to use that query as a su ## SELECT queries -These methods build and refine a `SELECT`. They chain, and you resolve the query with [`build()`](#build) followed by `runtime.query(...)`. Chain the methods in any order, with two exceptions. A join must come before any call that names the joined table's columns. The columns you pass to `distinctOn()` must be the first sort keys in `orderBy()`. `SelectQuery` in the tables below is the type they return. +These methods build and refine a `SELECT`. They chain, and you resolve the query with [`build()`](#build) followed by `runtime.query(...)`. Chain the methods in any order, with two exceptions: a join must come before any call that names the joined table's columns, and the columns you pass to `distinctOn()` must be the first sort keys in `orderBy()`. `SelectQuery` in the tables below is the type they return. -The set of column names a query can use is its scope. It starts as the columns of the table you began with, and every join adds the joined table's columns to it. +The set of column names a query can use is its scope: it starts as the columns of the table you began with, and every join adds the joined table's columns to it. -The `where()`, `select()`, `orderBy()`, `update()`, and other callback forms receive two arguments. The first is `f`, which has one property per column in scope. After a join, every column is on `f` under its table name, such as `f.user.email` or `f.post.id`. A column name that only one table has is also on `f` directly. The second is `fns`, an object of [expression helpers](#expressions-and-functions) such as `fns.eq()` and `fns.raw`. For an enum column you pass the value stored in the database. That is the string after `=`, so `High = "high"` is matched by `fns.eq(f.priority, 'high')`. A member with no `=`, such as `admin` in `user_type`, is matched by its own name, `fns.eq(f.kind, 'admin')`. +The `where()`, `select()`, `orderBy()`, `update()`, and other callback forms receive two arguments. The first is `f`, which has one property per column in scope. After a join, every column is on `f` under its table name, such as `f.user.email` or `f.post.id`. A column name that only one table has is also on `f` directly. The second is `fns`, an object of [expression helpers](#expressions-and-functions) such as `fns.eq()` and `fns.raw`. For an enum column you pass the value stored in the database, which is the string after `=`, so `High = "high"` is matched by `fns.eq(f.priority, 'high')`. A member with no `=`, such as `admin` in `user_type`, is matched by its own name, `fns.eq(f.kind, 'admin')`. ### `select()` @@ -187,7 +187,7 @@ Reduce each row to a chosen set of columns or computed expressions. | Aliased expression | `select(alias, (f, fns) => expr)` | Add one computed column under `alias`. | | Object of expressions | `select((f, fns) => ({ alias: expr, ... }))` | Add several computed columns at once. The row type is inferred from the object's shape. | -Every form adds to what the query already returns. Calling `select()` a second time never replaces the first call's columns, whichever form you use. To return fewer columns, start a new query instead. +Every form adds to what the query already returns: calling `select()` a second time never replaces the first call's columns, whichever form you use. To return fewer columns, start a new query instead. A computed expression's result type comes from `.returns(...)` on `fns.raw`, or from the `fns.*` function you called. Every `fns.raw` fragment needs `.returns()`. A type id is `pg/` plus the PostgreSQL type name plus a version, always `@1` today, such as `pg/int4@1` or `pg/uuid@1`. A few use a longer name, such as `pg/timestamptz-temporal@1` for `DateTime`. The common ones are `pg/text@1`, `pg/int4@1`, `pg/int8@1`, `pg/float8@1`, `pg/bool@1`, `pg/uuid@1`, and `pg/timestamptz-temporal@1`. The full list is what your contract uses. Read any column's id from `db.sql.public.
.columns..codecId`, and see [Binding a bare value with `param()`](/orm/reference/raw-queries#binding-a-bare-value-with-param) for which JavaScript type becomes which id. See [`fns.raw` and `.returns()`](#fnsraw-and-returns). @@ -382,7 +382,7 @@ Sort the result set by a column or a computed expression, in a direction you cho | `key` | Column name (`string`) or `(f, fns) => Expression` | Yes | The column or computed value to sort by. | | `options.direction` | `'asc' \| 'desc'` | No | Sort direction. | -TypeScript also accepts a `nulls` option. It does nothing, and no `NULLS FIRST` or `NULLS LAST` reaches the SQL. To put nulls last, sort by a computed value first, then by the column: +TypeScript also accepts a `nulls` option, but it does nothing, and no `NULLS FIRST` or `NULLS LAST` reaches the SQL. To put nulls last, sort by a computed value first, then by the column: ```ts db.sql.public.user @@ -504,7 +504,7 @@ const rows = await runtime.query(plan); ### Subquery via `.as()` -Call `.as(alias)` on a `SELECT` chain before `build()` to use it as a subquery. Pass the result to any join method as the `other` argument. `SELECT` queries and [grouped queries](#grouped-queries) have `.as()`. Queries started with `insert()`, `update()`, or `delete()` do not, so you cannot join against the rows they return. +Call `.as(alias)` on a `SELECT` chain before `build()` to use it as a subquery. Pass the result to any join method as the `other` argument. `SELECT` queries and [grouped queries](#grouped-queries) have `.as()`, but queries started with `insert()`, `update()`, or `delete()` do not, so you cannot join against the rows they return. #### Options @@ -617,7 +617,7 @@ Annotate with the type PostgreSQL computes, not the type you want. Here `pg/nume .groupBy((f, fns) => fns.raw`EXTRACT(YEAR FROM ${f.placedAt})`.returns('pg/numeric@1')) ``` -A name that is neither a column of the table you started from nor an alias you selected throws an error whose `code` is `ORM.COLUMN_UNKNOWN`, at the moment you call `groupBy()`. If you select a column you did not group or aggregate, the query fails when it runs, not when you build it. That failure comes back as PostgreSQL's own error, not a Prisma ORM code. +A name that is neither a column of the table you started from nor an alias you selected throws an error whose `code` is `ORM.COLUMN_UNKNOWN`, at the moment you call `groupBy()`. If you select a column you did not group or aggregate, the query fails when it runs, not when you build it, and that failure comes back as PostgreSQL's own error, not a Prisma ORM code. ### `having()` @@ -691,7 +691,7 @@ const rows = await runtime.query(plan); // the single highest-spending customer ## Mutations :::warning[An update or delete with no `where()` hits every row] -`where()` is optional on `update()` and `delete()`. Leave it out and every row in the table is changed, with no warning. +`where()` is optional on `update()` and `delete()`: leave it out and every row in the table is changed, with no warning. ::: Mutations start from a table with `insert()`, `update()`, or `delete()`, and end with [`build()`](#build). Add [`returning()`](#returning) to get the affected rows back. Run a mutation with no `returning()` using `runtime.execute(plan)`, which resolves to `{ affectedRows }`. With `returning()`, run it with `runtime.query(plan)`. The examples below go back to this page's example models, so they use `db.sql.public.tag` and `db.sql.public.user` again. @@ -772,7 +772,7 @@ Update matched rows. Set columns with a values object, or derive new values from #### Options -`update()` accepts a values object or an expression callback. You cannot mix the two forms in one call. +`update()` accepts a values object or an expression callback, but you cannot mix the two forms in one call. | Form | Signature | Description | |---|---|---| @@ -910,7 +910,7 @@ const authors = db.sql.public.user .build(); ``` -Which aggregate functions exist depends on your database. PostgreSQL gives you these eight. The contract type is the one you write in `contract.prisma`, and the PostgreSQL type is in brackets: +Which aggregate functions exist depends on your database, and PostgreSQL gives you these eight. The contract type is the one you write in `contract.prisma`, and the PostgreSQL type is in brackets: | Function | Returns | |---|---| @@ -927,7 +927,7 @@ On PostgreSQL you also get `ilike(expr, pattern)` for text columns, as in `fns.i ### `fns.raw` and `.returns()` -Write a raw SQL fragment as a tagged template. Interpolate columns, typed expressions, and bare JavaScript values with `${...}`. For a bare value, the builder picks the type id from its JavaScript type. [Binding a bare value with `param()`](/orm/reference/raw-queries#binding-a-bare-value-with-param) lists which JavaScript type becomes which type id, and shows how to choose a different one with `param(...)`. Call `.returns(typeId)` to declare the fragment's result type. +Write a raw SQL fragment as a tagged template, and interpolate columns, typed expressions, and bare JavaScript values with `${...}`. For a bare value, the builder picks the type id from its JavaScript type. [Binding a bare value with `param()`](/orm/reference/raw-queries#binding-a-bare-value-with-param) lists which JavaScript type becomes which type id, and shows how to choose a different one with `param(...)`. Call `.returns(typeId)` to declare the fragment's result type. #### Options @@ -1003,6 +1003,6 @@ const rows = await runtime.query(plan); // Row[] ### Streaming vs. collecting -`await runtime.query(plan)` collects every row into an array. This is the common case and what every example on this page uses. You can also loop the same result with `for await` to handle one row at a time. On PostgreSQL every row is loaded before the loop starts, so `for await` saves nothing there. Use `await`. +`await runtime.query(plan)` collects every row into an array, which is the common case and what every example on this page uses. You can also loop the same result with `for await` to handle one row at a time. On PostgreSQL every row is loaded before the loop starts, so `for await` saves nothing there. Use `await`. Keep the result in a variable, as in `const result = runtime.query(plan)`, and you can `await result` as many times as you like. You cannot mix `await` and `for await` on the same result, and you cannot `for await` it twice. Either one throws an error whose `code` is `RUNTIME.ITERATOR_CONSUMED`. See [`AsyncIterableResult`](/orm/reference/orm-client#asynciterableresult). diff --git a/apps/docs/content/docs/orm/reference/transactions-and-runtime.mdx b/apps/docs/content/docs/orm/reference/transactions-and-runtime.mdx index 25c63cc7747..b71d63d397f 100644 --- a/apps/docs/content/docs/orm/reference/transactions-and-runtime.mdx +++ b/apps/docs/content/docs/orm/reference/transactions-and-runtime.mdx @@ -6,9 +6,9 @@ metaTitle: Prisma ORM transactions and runtime reference metaDescription: 'Reference for the Prisma ORM client lifecycle, transactions, prepared statements, and execution options.' --- -Prisma ORM 8 renames `schema.prisma` to `contract.prisma`. Run `npx prisma contract emit`. It writes two files next to it: `contract.json`, which your app imports, and `contract.d.ts`, which gives you the TypeScript types. A new project keeps all three files in `src/prisma/`. Every client you create needs `contract.json`. The query you came for, `prisma.user.findMany({ where })`, is now `db.orm.public.User.where(...).all()`. [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7) lists the packages to install, the commands to run, and the rest of the renamed calls. +Prisma ORM 8 renames `schema.prisma` to `contract.prisma`. Run `npx prisma contract emit`, which writes two files next to it: `contract.json`, which your app imports, and `contract.d.ts`, which gives you the TypeScript types. A new project keeps all three files in `src/prisma/`. Every client you create needs `contract.json`. The query you came for, `prisma.user.findMany({ where })`, is now `db.orm.public.User.where(...).all()`. [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7) lists the packages to install, the commands to run, and the rest of the renamed calls. -`db` is the client that `postgres(...)` or `mongo(...)` returns. `db.runtime()` gives you the object that actually runs queries. That object is called the runtime. +`db` is the client that `postgres(...)` or `mongo(...)` returns. `db.runtime()` gives you the object that actually runs queries, and that object is called the runtime. Create, connect, and close the client, run transactions, prepare statements, and set per-query execution options, on PostgreSQL and MongoDB. For a task-oriented walkthrough, see the Fundamentals guide to [Transactions](/orm/fundamentals/transactions). For the ways to query inside a transaction, see the [ORM client reference](/orm/reference/orm-client), the [SQL query builder reference](/orm/reference/sql-query-builder), and the [raw queries reference](/orm/reference/raw-queries). @@ -71,7 +71,7 @@ import contractJson from './contract.json' with { type: 'json' }; const db = postgres({ contractJson, url: process.env.DATABASE_URL! }); ``` -These examples assume the file is beside `contract.json` in `src/prisma/`. From anywhere else, change the relative paths. Write `./contract.d` exactly like that, with no extension. The line `import contractJson from './contract.json' with { type: 'json' }` is what `prisma orm init` writes. It needs Node.js 22.18 or newer, and these `tsconfig.json` settings: `resolveJsonModule: true`, `module: "preserve"`, and `moduleResolution: "bundler"`. TypeScript types every `process.env` value as possibly undefined, so the examples write `!` after it, the same way `prisma orm init` does. +These examples assume the file is beside `contract.json` in `src/prisma/`, so from anywhere else, change the relative paths. Write `./contract.d` exactly like that, with no extension. The line `import contractJson from './contract.json' with { type: 'json' }`, which `prisma orm init` writes, needs Node.js 22.18 or newer, and these `tsconfig.json` settings: `resolveJsonModule: true`, `module: "preserve"`, and `moduleResolution: "bundler"`. TypeScript types every `process.env` value as possibly undefined, so the examples write `!` after it, the same way `prisma orm init` does. ##### Bind to an existing `pg` pool @@ -420,7 +420,7 @@ await db.transaction(async (tx) => { ``` :::warning[An escaped `AsyncIterableResult` throws after the transaction ends] -Collect the rows inside the callback by awaiting the result there, and return the array instead. A result is tied to the transaction connection. If you return one from the callback and read it after the transaction has committed, it rejects with an error whose `code` is `RUNTIME.TRANSACTION_CLOSED`, before yielding any row. +Collect the rows inside the callback by awaiting the result there, and return the array instead. A result is tied to the transaction connection, so if you return one from the callback and read it after the transaction has committed, it rejects with an error whose `code` is `RUNTIME.TRANSACTION_CLOSED`, before yielding any row. ```typescript const escaped = await db.transaction(async (tx) => { @@ -560,7 +560,7 @@ await connection.destroy(); ## Transactions (MongoDB) :::note[MongoDB transactions are not available in Prisma ORM yet] -There is no `db.transaction(...)` on the MongoDB client, and the MongoDB runtime has no `connection()`, `prepare()`, or `telemetry()`: it has `query`, `execute`, and `close` and nothing else. Single-document write operations are atomic on their own. Multi-document transactions through Prisma ORM are planned, not shipped. To run one today, create your own `MongoClient`, pass it as the `mongoClient` option when you create the client, and group the writes in `session.withTransaction(...)`, as the [Fundamentals transactions guide](/orm/fundamentals/transactions#transactions-on-mongodb) shows. The reading rules under [`AsyncIterableResult`](#asynciterableresult) apply to MongoDB too. +There is no `db.transaction(...)` on the MongoDB client, and the MongoDB runtime has no `connection()`, `prepare()`, or `telemetry()`: it has `query`, `execute`, and `close` and nothing else. Single-document write operations are atomic on their own, while multi-document transactions through Prisma ORM are planned, not shipped. To run one today, create your own `MongoClient`, pass it as the `mongoClient` option when you create the client, and group the writes in `session.withTransaction(...)`, as the [Fundamentals transactions guide](/orm/fundamentals/transactions#transactions-on-mongodb) shows. The reading rules under [`AsyncIterableResult`](#asynciterableresult) apply to MongoDB too. ::: ## Prepared statements (PostgreSQL) @@ -772,6 +772,6 @@ const after = runtime.telemetry(); [`all()`](/orm/reference/orm-client#all), [`createAll()`](/orm/reference/orm-client#createall), and the runtime's `query(...)` return an `AsyncIterableResult`. `all()` and `query(...)` read rows, and `createAll()` writes rows and returns the ones it wrote. `await` the result to collect an array, or `for await` it to take rows one at a time. -Use `await`. Use `for await` only to handle rows as they arrive. On PostgreSQL it does not reduce memory, because every row is loaded first. +Use `await`, and use `for await` only to handle rows as they arrive. On PostgreSQL it does not reduce memory, because every row is loaded first. -Pick `await` or `for await` for a given result and do not mix the two. Calling `.toArray()` does the same thing as `await`. Re-`await`ing a result you already awaited is safe and returns the same array. Switching between `await` and `for await`, or looping a second time with `for await`, throws an error whose `code` is `RUNTIME.ITERATOR_CONSUMED`. For the full rules on reading a result, shared identically by PostgreSQL and MongoDB, see [`AsyncIterableResult`](/orm/reference/orm-client#asynciterableresult) in the ORM client reference. +Pick `await` or `for await` for a given result and do not mix the two. Calling `.toArray()` does the same thing as `await`. Re-`await`ing a result you already awaited is safe and returns the same array, but switching between `await` and `for await`, or looping a second time with `for await`, throws an error whose `code` is `RUNTIME.ITERATOR_CONSUMED`. For the full rules on reading a result, shared identically by PostgreSQL and MongoDB, see [`AsyncIterableResult`](/orm/reference/orm-client#asynciterableresult) in the ORM client reference. diff --git a/apps/docs/content/docs/orm/release-status.mdx b/apps/docs/content/docs/orm/release-status.mdx index 2d7ad032289..52e36218a86 100644 --- a/apps/docs/content/docs/orm/release-status.mdx +++ b/apps/docs/content/docs/orm/release-status.mdx @@ -6,7 +6,7 @@ metaTitle: Prisma ORM release status metaDescription: 'Prisma ORM 8 is a release candidate, with general availability expected in October 2026. What npm installs today, how long Prisma ORM 7 is supported and Prisma ORM 6 receives security patches, and how to stay on version 7.' --- -Prisma ORM 8 is a **release candidate**. You can install it and build with it today. Until the final release, some details of the API may still change, so code you write now may need small edits later. Each release candidate fixes bugs found in the previous one. What Prisma ORM 8 changes, and why, is on the [Prisma ORM overview](/orm). +Prisma ORM 8 is a **release candidate**: you can install it and build with it today. Until the final release, some details of the API may still change, so code you write now may need small edits later. Each release candidate fixes bugs found in the previous one. What Prisma ORM 8 changes, and why, is on the [Prisma ORM overview](/orm). It needs Node.js 22.18 or newer (on the 24 line, 24.11 or newer; 24.0 to 24.10 do not work) and TypeScript 5.9 or newer. @@ -22,7 +22,7 @@ Prisma ORM 7 keeps receiving bug fixes and security updates for 18 months from t ## Prisma ORM 6 receives security patches until 19 November 2026 -Prisma ORM 6 receives security patches, but no other updates, for 12 months after the release of Prisma ORM 7. That period ends on 19 November 2026. Its documentation stays at [/orm/v6](/orm/v6). +Prisma ORM 6 receives security patches, but no other updates, for 12 months after the release of Prisma ORM 7, a period that ends on 19 November 2026. Its documentation stays at [/orm/v6](/orm/v6). ## What you get when you install today @@ -45,11 +45,11 @@ The two packages have different version numbers, and that is normal: they are re Versions were checked on 14 September 2026; run `npm view prisma dist-tags`, `npm view @prisma/orm-postgres dist-tags`, and `npm view @prisma/orm-mongo dist-tags` for today's, since the command-line tool and the libraries are versioned separately. -`@prisma/client`, the library that Prisma ORM 7 apps import, stays at `7.10.0`. It belongs to Prisma ORM 7 and is not part of Prisma ORM 8. +`@prisma/client`, the library that Prisma ORM 7 apps import, stays at `7.10.0`, because it belongs to Prisma ORM 7 and is not part of Prisma ORM 8. :::warning[If you have a Prisma ORM 7 project] -`npm install prisma` and `npx prisma` now give you the Prisma ORM 8 command-line tool. It does not read `schema.prisma` and has no `generate`, `migrate dev`, or `db push` commands. If your project is on Prisma ORM 7, run `npm install --save-dev prisma@7` now, and lock the version in `package.json` as shown below. If version 8 is already installed, that same command downgrades it. To see what you have now, run `npm ls prisma @prisma/client`. +`npm install prisma` and `npx prisma` now give you the Prisma ORM 8 command-line tool, which does not read `schema.prisma` and has no `generate`, `migrate dev`, or `db push` commands. If your project is on Prisma ORM 7, run `npm install --save-dev prisma@7` now, and lock the version in `package.json` as shown below. If version 8 is already installed, that same command downgrades it. To see what you have now, run `npm ls prisma @prisma/client`. :::