From 429ed5cd4b1686042801f1a55f60c20bc8af2741 Mon Sep 17 00:00:00 2001 From: willbot Date: Sat, 12 Sep 2026 09:03:12 +0200 Subject: [PATCH 01/11] docs(orm8): orm-client reference: banned terms replaced, anchors kept Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- .../content/docs/orm/reference/orm-client.mdx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/docs/content/docs/orm/reference/orm-client.mdx b/apps/docs/content/docs/orm/reference/orm-client.mdx index 80a577e27a4..8a8c41cc816 100644 --- a/apps/docs/content/docs/orm/reference/orm-client.mdx +++ b/apps/docs/content/docs/orm/reference/orm-client.mdx @@ -219,7 +219,7 @@ PostgreSQL exposes models under their contract root names (matching PSL model na ## Query-building methods -These methods narrow a query. They return a collection you can chain further methods on, and you resolve the query with a [read terminal](#read-terminals) such as `all()` or `first()`. MongoDB supports a smaller set than PostgreSQL; each method's Remarks state its availability. +These methods narrow a query. They return a collection you can chain further methods on, and you run the query with a [read method](#read-terminals) such as `all()` or `first()`. MongoDB supports a smaller set than PostgreSQL; each method's Remarks state its availability. Import the standalone filter combinators used in some examples from `@prisma/orm-postgres/orm-client` (PostgreSQL) or `@prisma/orm-mongo/query-ast/execution` (MongoDB). See [Filter conditions and operators](#filter-conditions-and-operators). @@ -244,7 +244,7 @@ Restrict a query to rows matching a filter. | Return type | Example | Description | |---|---|---| -| `Collection` | `db.orm.public.User.where(...)` | A collection narrowed by the filter, chainable and awaitable through a terminal. | +| `Collection` | `db.orm.public.User.where(...)` | A collection narrowed by the filter, chainable; a read or write method runs it. | #### Examples @@ -327,7 +327,7 @@ Eagerly load a relation onto the returned rows. #### Remarks -- Available for PostgreSQL and MongoDB, with different capabilities. +- Available for PostgreSQL and MongoDB, with differences noted below. - On PostgreSQL, `include(relationName, refineFn?)` loads to-one and to-many relations, and the optional callback receives a nested `Collection` you can filter, order, limit, and aggregate (see [Refinements, aggregates, and combine](#refinements-aggregates-and-combine)). - On MongoDB, `include(relationName)` adds a `$lookup` for a reference relation and takes only a relation name; there is no refinement-callback overload. - On MongoDB, a looked-up sub-document's `_id` comes back as a raw driver `ObjectId`, not decoded to a hex string the way top-level `_id` fields are. Compare it with `String(...)`. @@ -706,9 +706,9 @@ const bugs = await db.orm.public.Task.variant('Bug').all(); const tutorials = await db.orm.posts.variant('Tutorial').all(); ``` -## Read terminals +## Read methods [#read-terminals] -Read terminals resolve a query. `all()` and `first()` are available on both databases; `aggregate()` and `groupBy()` are PostgreSQL only and are documented under [Grouped aggregates](#grouped-aggregates). For a task-oriented walkthrough, see [Reading data](/orm/fundamentals/reading-data). +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). For a task-oriented walkthrough, see [Reading data](/orm/fundamentals/reading-data). ### `all()` @@ -863,7 +863,7 @@ const bugs = await db.Task.bugs().all(); const features = await db.Task.features().all(); ``` -## Mutation terminals +## Write methods [#mutation-terminals] Mutations write to the database. `create`, `createAll`, `createAndCount`, `update`, `updateAll`, `updateAndCount`, `delete`, `deleteAll`, `deleteAndCount`, and `upsert` are available on both databases, with several behavioral differences called out per method. For a task-oriented walkthrough, see [Writing data](/orm/fundamentals/writing-data); for grouping several writes into one unit, see [Transactions](/orm/fundamentals/transactions). @@ -1577,7 +1577,7 @@ Column-level comparison methods on a field accessor. #### Remarks -- PostgreSQL. Each method is gated by a codec trait: `eq`/`neq`/`in`/`notIn` need equality; `gt`/`lt`/`gte`/`lte` need ordering; `like` needs the textual trait; `isNull`/`isNotNull` are available on every field. +- PostgreSQL. Which methods a field has depends on its type: `eq`/`neq`/`in`/`notIn` on any field that can be compared for equality; `gt`/`lt`/`gte`/`lte` on any field that can be ordered; `like` on text fields; `isNull`/`isNotNull` on every field. - `like()` matches a SQL pattern case-sensitively. - `ilike()` matches case-insensitively. It is a Postgres-adapter-registered operation attached to any textual-trait field, not a core comparison method. @@ -1880,7 +1880,7 @@ Use `Shape` to declare a function's return type once and let the compiler check ### `AsyncIterableResult` -The read terminals `all()` / `createAll()` / `updateAll()` / `deleteAll()` return an `AsyncIterableResult`, imported (if you need the type) from `@prisma/orm-postgres/components/runtime`. It has a dual interface: +The methods `all()` / `createAll()` / `updateAll()` / `deleteAll()` return an `AsyncIterableResult`, imported (if you need the type) from `@prisma/orm-postgres/components/runtime`. It has a dual interface: - **Buffered**: `await` the result (or call `.toArray()`) to collect every row into an array. - **Streaming**: use `for await ... of` to iterate rows one at a time. From 5165f9562a4cf0a371d1e8c170f3e2deff6b3f7a Mon Sep 17 00:00:00 2001 From: willbot Date: Sat, 12 Sep 2026 09:03:55 +0200 Subject: [PATCH 02/11] docs(orm8): orm-client reference, reader-review round one applied (unverified: fixer agents were stopped before reporting) The four fixer agents finished writing their slices but were stopped before they reported their source lookups. The fact re-check has not run. Reader reports, the page rules, and the shared conventions are committed under docs/orm-docs-audit/c21-orm-client/ for the next agent. Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- .../content/docs/orm/reference/orm-client.mdx | 466 ++++++++++++------ .../c21-orm-client/conventions.md | 24 + .../c21-orm-client/page-conventions.md | 9 + .../c21-orm-client/part1.round1.md | 86 ++++ .../c21-orm-client/part2.round1.md | 101 ++++ .../c21-orm-client/part3.round1.md | 82 +++ .../c21-orm-client/part4.round1.md | 65 +++ 7 files changed, 686 insertions(+), 147 deletions(-) create mode 100644 docs/orm-docs-audit/c21-orm-client/conventions.md create mode 100644 docs/orm-docs-audit/c21-orm-client/page-conventions.md create mode 100644 docs/orm-docs-audit/c21-orm-client/part1.round1.md create mode 100644 docs/orm-docs-audit/c21-orm-client/part2.round1.md create mode 100644 docs/orm-docs-audit/c21-orm-client/part3.round1.md create mode 100644 docs/orm-docs-audit/c21-orm-client/part4.round1.md diff --git a/apps/docs/content/docs/orm/reference/orm-client.mdx b/apps/docs/content/docs/orm/reference/orm-client.mdx index 8a8c41cc816..ef9b1bdd373 100644 --- a/apps/docs/content/docs/orm/reference/orm-client.mdx +++ b/apps/docs/content/docs/orm/reference/orm-client.mdx @@ -8,13 +8,15 @@ metaDescription: 'Reference for the Prisma ORM client''s query, mutation, filter The ORM client gives you model-level methods for reading and writing data across PostgreSQL and MongoDB. This page documents every method, its availability on each database, and the behavior that differs between the two. -Availability is stated per method. When a method behaves the same on both databases, the Remarks say so; when it differs, exists on only one, or is type-checked but not enforced at runtime, the Remarks call that out. +Each method says which databases it works on. Where a method behaves differently on one of them, the Remarks say how. Where TypeScript rejects something the database itself would accept, the Remarks say that too. For task-oriented walkthroughs, see the Fundamentals guides: [Reading data](/orm/fundamentals/reading-data), [Writing data](/orm/fundamentals/writing-data), [Relations and joins](/orm/fundamentals/relations-and-joins), and [Transactions](/orm/fundamentals/transactions). ## Example schema -All examples on this page run against the following schema, and every example is transcribed from an executable test suite that runs against a live database. The grouped-aggregate examples use a separate `Customer` / `Order` schema, shown under [Grouped aggregates](#grouped-aggregates). +All examples on this page run against the schema below. Every example is copied from a test suite that runs against a live database. The grouped-aggregate examples use a separate `Customer` / `Order` schema, shown under [Grouped aggregates](#grouped-aggregates). + +You write this schema in your contract, the `contract.prisma` file that replaced `schema.prisma`. After every change to it, run `npx prisma contract emit`. That command writes `contract.json` and `contract.d.ts`, the two files the examples below import.
@@ -177,13 +179,24 @@ model Tutorial {
+Some parts of the schema the examples rely on: + +- `@@map("user")` sets the table or collection name. Without it, the table name is the model name with a lowercase first letter. +- `Uuid` is the type for a PostgreSQL `uuid` column. `@default(uuid())` fills the id in when you insert a row. +- `@@type("pg/text@1")` on an `enum` block stores the enum as a text column, with a check constraint that allows only the declared values. This page calls that a text-backed enum. +- A `native_enum` block is the other kind. It creates a real PostgreSQL enum type with `CREATE TYPE ... AS ENUM`. This page calls that a native enum. The two sort differently; see [`orderBy()`](#orderby). +- An enum member's stored value is the string after `=`, or the member name when there is no `=`. That string is what you pass to `.eq()`. So `Urgent = "urgent"` is matched by `.eq('urgent')`, and a bare `admin` member is matched by `.eq('admin')`. +- `@@base(Task, "bug")` makes `Bug` a variant of `Task`: a model stored together with the model it is based on. `@@discriminator(type)` names the column that says which variant a row is. `variant('Bug')` narrows a collection to that variant, and the name you pass is the model name, not the string in `@@base`. +- `PostTag` is required. A many-to-many needs the model for the join table, and a pair of list fields with no such model is rejected. See [Relations and joins](/orm/fundamentals/relations-and-joins#many-to-many). +- `Embedding1536` comes from the pgvector extension. See [PostgreSQL](#postgresql) for the extra line it needs when you create the client. + ## Setting up the client -Create an ORM client with `postgres(...)` or `mongo(...)`, then query models through the client's `.orm` facet. The two databases have different entry points and different accessor conventions. +Create a client with `postgres(...)` or `mongo(...)`, then read and write your models through `db.orm`. The two databases have different entry points, and they name the models differently. ### PostgreSQL -Create a Postgres client with `postgres(...)`. Models hang off `db.orm.public` and use the contract's **root names**, which match your Prisma Schema Language (PSL) model names (`db.orm.public.User`, `db.orm.public.Post`). `public` is the default PostgreSQL schema namespace. +Create a PostgreSQL client with `postgres(...)`. `db.orm` holds your models by model name, grouped by database schema: `db.orm.public.User`, `db.orm.public.Post`. `public` is the PostgreSQL schema your tables are in unless you put them in another one. ```typescript import postgres from '@prisma/orm-postgres/runtime'; @@ -195,13 +208,23 @@ const db = postgres({ contractJson, url: process.env.DATABASE_URL }); const users = await db.orm.public.User.all(); ``` -If your contract uses types from an extension pack, pass the pack when you create the client. The example schema's `Embedding1536` (pgvector) type needs `extensions: [pgvector]`, with `pgvector` imported from `@prisma/orm-extension-pgvector/runtime`. +If your contract uses a type from an extension package, pass that package to `extensions` when you create the client. The example schema's `Embedding1536` type comes from pgvector, so it needs this: -To attach domain methods to a model, register a custom `Collection` subclass when you build the client. That path uses the `orm(...)` factory instead of the client's built-in facet; see [Custom `Collection` subclass](#custom-collection-subclass). +```typescript +import pgvector from '@prisma/orm-extension-pgvector/runtime'; + +const db = postgres({ + contractJson, + url: process.env.DATABASE_URL, + extensions: [pgvector], +}); +``` + +To give a model your own methods on top of the built-in ones, subclass `Collection` and register the subclass with the `orm(...)` function instead of calling `postgres(...)`. [Custom `Collection` subclass](#custom-collection-subclass) has the code. ### MongoDB -Create a Mongo client with `mongo(...)`. Models hang off `db.orm` and use the contract's **root names too, but those are the lowercase plural collection names** from the contract's `roots` map, not the PSL model names (`db.orm.users`, `db.orm.posts`, not `db.orm.User`). +Create a MongoDB client with `mongo(...)`. `db.orm` holds your models by collection name, with no schema in between: `db.orm.users`, `db.orm.posts`, not `db.orm.User`. The collection name is the one you set with `@@map`, or the model name with a lowercase first letter when you did not set one. ```typescript import mongo from '@prisma/orm-mongo/runtime'; @@ -213,15 +236,25 @@ const db = mongo({ contractJson, url: process.env.MONGODB_URL, dbName: const users = await db.orm.users.all(); ``` -:::note[Accessor naming differs by database] -PostgreSQL exposes models under their contract root names (matching PSL model names) and namespaced by schema: `db.orm.public.User`. MongoDB exposes them under the registered collection root names (lowercase plural), with no namespace: `db.orm.users`. The examples below follow each database's convention. +:::note[The two databases name models differently] +On PostgreSQL you write the model name under its schema: `db.orm.public.User`. On MongoDB you write the collection name with nothing in front of it: `db.orm.users`. The examples below follow each database's own form. ::: ## Query-building methods -These methods narrow a query. They return a collection you can chain further methods on, and you run the query with a [read method](#read-terminals) such as `all()` or `first()`. MongoDB supports a smaller set than PostgreSQL; each method's Remarks state its availability. +These methods narrow a query. Each returns a collection you can chain more methods on. You run the query with a [read method](#read-terminals) such as `all()` or `first()`. MongoDB supports fewer of them 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). -Import the standalone filter combinators used in some examples from `@prisma/orm-postgres/orm-client` (PostgreSQL) or `@prisma/orm-mongo/query-ast/execution` (MongoDB). See [Filter conditions and operators](#filter-conditions-and-operators). +The examples below use these ids, which stand for rows the test suite inserted first: + +```typescript +const aliceId = '00000000-0000-4000-8000-000000000001'; +const carolId = '00000000-0000-4000-8000-000000000003'; +const postId = '00000000-0000-4000-8000-000000000010'; +const acmeId = '00000000-0000-4000-8000-000000000020'; +const emptyCustomerId = '00000000-0000-4000-8000-000000000021'; +``` ### `where()` @@ -230,9 +263,9 @@ Restrict a query to rows matching a filter. #### Remarks - Available for PostgreSQL and MongoDB, but the accepted filter shapes differ. -- On PostgreSQL, `where()` accepts a callback with column-level operators (`u.email.eq(...)`) or a shorthand object of equality matches. -- On MongoDB, `where()` accepts a shorthand object of equality matches or a `MongoFieldFilter` expression. The plain-object form supports equality only, not nested operators; for comparisons use `MongoFieldFilter` (see [MongoFieldFilter](#mongofieldfilter)). -- Chaining multiple `where()` calls ANDs the filters together. +- On PostgreSQL, `where()` takes either a callback that calls an operator on a column (`u.email.eq(...)`) or an object of field-and-value pairs that must match exactly. +- On MongoDB, `where()` takes either an object of field-and-value pairs that must match exactly or a `MongoFieldFilter`. The object form can only test for equality. For anything else, such as greater-than, use `MongoFieldFilter` (see [MongoFieldFilter](#mongofieldfilter)). +- Calling `where()` more than once on the same query requires a row to match all of the filters. #### Options @@ -244,7 +277,7 @@ Restrict a query to rows matching a filter. | Return type | Example | Description | |---|---|---| -| `Collection` | `db.orm.public.User.where(...)` | A collection narrowed by the filter, chainable; a read or write method runs it. | +| `Collection` | `db.orm.public.User.where(...)` | A collection narrowed by the filter. Chain more methods on it, then run it with a read or write method. | #### Examples @@ -294,7 +327,7 @@ Project a row down to a subset of scalar fields. - Available for PostgreSQL and MongoDB. - On PostgreSQL, `select()` narrows the returned row shape at the type level: fields you didn't select are absent from the result type. -- On MongoDB, `select()` narrows the projected fields at runtime, but does not strip fields from the returned row type at compile time. The returned type is unchanged, even though the query projects fewer fields. +- On MongoDB, `select()` changes what the database returns but not the TypeScript type. Only the fields you named come back, while the type still lists every field on the model. #### Options @@ -328,17 +361,17 @@ Eagerly load a relation onto the returned rows. #### Remarks - Available for PostgreSQL and MongoDB, with differences noted below. -- On PostgreSQL, `include(relationName, refineFn?)` loads to-one and to-many relations, and the optional callback receives a nested `Collection` you can filter, order, limit, and aggregate (see [Refinements, aggregates, and combine](#refinements-aggregates-and-combine)). -- On MongoDB, `include(relationName)` adds a `$lookup` for a reference relation and takes only a relation name; there is no refinement-callback overload. -- On MongoDB, a looked-up sub-document's `_id` comes back as a raw driver `ObjectId`, not decoded to a hex string the way top-level `_id` fields are. Compare it with `String(...)`. -- On MongoDB, passing a second argument to `include()` does **not** throw. JavaScript does not arity-check, so the extra argument is silently ignored and a plain, unrefined `$lookup` runs. This differs from calling a genuinely absent method, which throws `TypeError`. +- On PostgreSQL, `include(relationName, refineFn?)` loads both to-one and to-many relations. The optional callback receives the related rows as a collection, so you can filter, order, limit, and aggregate them. See [Refinements, aggregates, and combine](#refinements-aggregates-and-combine). +- On MongoDB, `include(relationName)` loads a relation that is stored as a reference. It takes the relation name and nothing else: there is no callback. +- On MongoDB, the `_id` of a loaded related document comes back as a driver `ObjectId`, not as the hex string you get for a top-level `_id`. Wrap it in `String(...)` before you compare it. +- On MongoDB, a second argument to `include()` is ignored rather than rejected, and the relation loads unfiltered. #### Options | Name | Type | Required | Description | |---|---|---|---| | `relationName` | `string` | Yes | The relation to load. | -| `refineFn` | `(relation: Collection) => Collection \| reducer` | No | PostgreSQL only. Refines the loaded relation. | +| `refineFn` | `(relation: Collection) => Collection \| aggregate` | No | PostgreSQL only. Narrows the loaded relation, or reduces it to one value. | #### Return type @@ -366,22 +399,26 @@ const users = await db.orm.public.User.include('posts').where({ id: aliceId }).a ```typescript const posts = await db.orm.posts.include('author').where({ title: 'Hello world' }).all(); -// posts[0].author._id is a raw ObjectId — compare with String(posts[0].author._id) +// posts[0].author._id is an ObjectId; compare it as String(posts[0].author._id) ``` For a task-oriented guide to loading related records, see [Relations and joins](/orm/fundamentals/relations-and-joins). ### Refinements, aggregates, and combine -On PostgreSQL, the callback you pass to `include()` receives the nested relation as a full `Collection`. You can filter, order, and paginate it, reduce it to a single value, or `combine()` several sub-views into one shape. +On PostgreSQL, the callback you pass to `include()` receives the related rows as a collection. You can filter, order, and paginate them. You can also reduce them to a single value, or use `combine()` to return several results at once. #### Remarks -- PostgreSQL only. MongoDB's `include()` takes no callback, and `count`/`sum`/`avg`/`min`/`max`/`combine` do not exist on the Mongo collection at all (calling them throws `TypeError`). -- `count`, `sum`, `avg`, `min`, `max` and `combine` are only callable **inside** an `include()` callback. Called anywhere else on a PostgreSQL collection, they throw `Error`. -- Which fields each of those functions accepts comes from the aggregate map in your emitted contract, not from a fixed rule. On PostgreSQL, `sum()` and `avg()` take numeric columns, and `min()` and `max()` also take textual, date/time-string, and Temporal-backed columns, so `min('createdAt')` type-checks. A field the contract's map does not claim for that operation is a compile error at the call site. -- `count()` returns a `number` and has a zero-argument form; `countBigInt()` is the lossless `bigint` variant. `sum()` over an integer column returns `number` and raises `RUNTIME.DECODE_FAILED` outside ±(2^53 − 1); `avg()` over an integer column returns a `float8` `number`; `sumBigInt(field)` and `avgDecimal(field)` are the lossless forms. -- Over a to-many relation with no rows, `sum()` and `avg()` resolve to `null`, not `0`. +- PostgreSQL only. On MongoDB, `include()` takes no callback, and `count`, `sum`, `avg`, `min`, `max`, and `combine` do not exist on a collection at all. Calling one throws a `TypeError`. +- On PostgreSQL, those six are only callable **inside** an `include()` callback. Called anywhere else, they throw an `Error`. +- `sum()` and `avg()` take a numeric column: an integer, a floating-point number, or a decimal. They also take an interval or a time-of-day column. They do not take a date or a timestamp, and calling them on one is a compile error that names the operation the database has no version of. +- `min()` and `max()` take more: numeric and text columns, dates, times, timestamps, intervals, IP addresses, and text arrays. They do not take a boolean, a `uuid`, binary data, a bit string, or JSON. +- `count()` takes no argument and returns a `number`. `countBigInt()` returns a `bigint` instead, for a count too large for a JavaScript `number`. +- `sum()` over an integer column returns a `number`. Past ±(2^53 − 1) a `number` cannot hold the total, and reading it throws an error whose `code` is `RUNTIME.DECODE_FAILED`. Use `sumBigInt(field)` for totals that large; it returns a `bigint`. +- `avg()` over an integer column returns a `number`, which rounds. `avgDecimal(field)` returns the exact average instead. +- When a to-many relation has no rows, `sum()` and `avg()` come back as `null`, not `0`. +- `combine(shape)` takes an object whose keys you choose. Each value is either the relation with more methods chained on it, which comes back as an array of rows, or an aggregate such as `posts.count()`, which comes back as that single value. Each value must start from the collection the callback was given, which is why the example below writes `posts` inside its own callback. #### Options @@ -389,15 +426,15 @@ On PostgreSQL, the callback you pass to `include()` receives the nested relation |---|---|---|---| | `filter` / `orderBy` / `limit` / `offset` | Chained on the nested collection | No | Refine which related rows load. | | `count()` | Aggregate, no arguments | No | Reduces the relation to a row count. | -| `sum(field)` / `avg(field)` | Aggregate over a numeric field | No | Reduces the relation to a numeric aggregate; `null` over an empty relation. `sumBigInt` / `avgDecimal` are the lossless forms. | -| `min(field)` / `max(field)` | Aggregate over a field the contract's aggregate map claims | No | Reduces the relation to a minimum/maximum, in the column's own type. | -| `combine(shape)` | Object of sub-views and aggregates | No | Projects several refinements into one shape. | +| `sum(field)` / `avg(field)` | Aggregate over a numeric field | No | Reduces the relation to one number. | +| `min(field)` / `max(field)` | Aggregate over a sortable field | No | Reduces the relation to its smallest or largest value, in the column's own type. | +| `combine(shape)` | Object of named collections and aggregates | No | Returns several results under one relation key. | #### Return type | Return type | Example | Description | |---|---|---| -| Refined relation, scalar, or combined shape | `include('posts', (p) => p.count())` | The relation is replaced by the refinement's result. | +| The narrowed relation, a single value, or an object of both | `include('posts', (p) => p.count())` | The relation key on each row holds the callback's result instead of the related rows. | #### Examples @@ -425,7 +462,7 @@ const users = await db.orm.public.User.include('posts', (posts) => posts.count() ##### `sum()` / `avg()` over a numeric relation (PostgreSQL) -This example uses the `Customer` / `Order` models from the aggregate example schema shown in [Grouped aggregates](#grouped-aggregates). +This example uses the `Customer` and `Order` models, which are in the separate schema shown under [Grouped aggregates](#grouped-aggregates). ```typescript const customers = await db.orm.public.Customer.include('orders', (orders) => orders.sum('amount')) @@ -462,8 +499,8 @@ const users = await db.orm.public.User.include('posts', (posts) => // users[0].posts.total is 2; users[0].posts.recent is a one-element array ``` -:::note[Which fields `min()` / `max()` accept] -`min()` and `max()` are typed from the aggregate map in your emitted contract. On PostgreSQL that map claims numeric and textual columns, and columns stored as date/time strings or backed by `Temporal`. So `min('createdAt')` over a `DateTime` column type-checks and returns a `Temporal.Instant`. `sum()` and `avg()` are narrower: PostgreSQL declares no `SUM`/`AVG` over a date or time-stamp column, so those calls are a compile error, naming the operation that is unavailable for that input. +:::note[`min()` and `max()` over a date column] +`min('createdAt')` over a `DateTime` column compiles, and it returns a `Temporal.Instant` (the standard JavaScript date and time object; Node.js 26 has it built in, earlier versions need `temporal-polyfill`). `sum('createdAt')` and `avg('createdAt')` do not compile, because PostgreSQL has no `SUM` or `AVG` over a date or a timestamp. The TypeScript error names the operation that is missing for that column. ::: ### `orderBy()` @@ -473,9 +510,9 @@ Sort the result set. #### Remarks - Available for PostgreSQL and MongoDB, with different argument shapes. -- On PostgreSQL, `orderBy()` takes a callback returning a per-column `.asc()`/`.desc()` directive, or an array of such callbacks for multiple sort keys. -- On MongoDB, `orderBy()` takes a plain object spec, `{ field: 1 }` for ascending or `{ field: -1 }` for descending. -- On PostgreSQL, **native** enum columns sort in the enum's declaration order, not alphabetically. A native `Priority` enum declared `Low`, `High`, `Urgent` sorts in that order under `.asc()`. A text-backed enum sorts by its stored value instead, so declaration order does not apply; for semantic ranking, use numeric enum values or an explicit ranking expression. +- On PostgreSQL, `orderBy()` takes a callback that returns `.asc()` or `.desc()` on one column. Pass an array of such callbacks to sort by more than one column. +- On MongoDB, `orderBy()` takes an object instead: `{ field: 1 }` sorts ascending and `{ field: -1 }` sorts descending. +- On PostgreSQL, a native enum column sorts in the order its members are declared, not alphabetically. A `native_enum Priority` declared `Low`, `High`, `Urgent` sorts in that order under `.asc()`. A text-backed enum sorts by its stored text instead, so `'high'` comes before `'low'` and `'urgent'`. To sort a text-backed enum by meaning, store numbers rather than words, or write your own ranking expression. #### Options @@ -510,7 +547,7 @@ const byPriorityThenDate = await db.orm.public.Post.orderBy([ (p) => p.priority.asc(), (p) => p.createdAt.asc(), ]).all(); -// a text-backed priority sorts by stored value ('high', 'low', 'urgent'); a native enum sorts in declaration order +// Priority is text-backed here, so it sorts as 'high', 'low', 'urgent' ``` ### `limit()` @@ -585,7 +622,7 @@ Resume pagination from a known position. #### Remarks - PostgreSQL only. MongoDB's collection has no `cursor()`; calling it throws `TypeError`. -- `cursor()` is typed to require a preceding `orderBy()`. This is a **type-only** guard: at runtime there is no such check. If you bypass the type system and call `cursor()` without an `orderBy()`, the cursor value is silently ignored and the full unfiltered result is returned. It does not throw. +- Always call `orderBy()` before `cursor()`. TypeScript rejects the call if you do not, but nothing checks at runtime. If you get past the type system, the cursor value is ignored and you get every row back, with no error. #### Options @@ -615,7 +652,7 @@ const page2 = await db.orm.public.Post.orderBy((p) => p.createdAt.asc()) ### `distinct()` -Emit `SELECT DISTINCT` on the given fields. +Remove duplicate rows, comparing only the fields you name. #### Remarks @@ -648,7 +685,7 @@ Keep the first row per key according to `orderBy()`. #### Remarks - PostgreSQL only. MongoDB's collection has no `distinctOn()`; calling it throws `TypeError`. -- Requires a prior `orderBy()` to be meaningful. Like `cursor()`, this ordering requirement is a type-level guard, not a runtime check. +- Call `orderBy()` first, so that "the first row per key" means something. As with `cursor()`, TypeScript checks this and the runtime does not. #### Options @@ -674,25 +711,26 @@ const latestPerUser = await db.orm.public.Post.orderBy([(p) => p.userId.asc(), ( ### `variant()` -Narrow a polymorphic (PostgreSQL) or discriminated (MongoDB) model to one variant. +Narrow a model to one of the variants declared with `@@base`. #### Remarks - Available for PostgreSQL and MongoDB. -- On PostgreSQL, variants use multi-table inheritance (a base table plus a variant table). This affects some mutations; see [`createAndCount()`](#createandcount) and [`upsert()`](#upsert). -- On MongoDB, variants are a single document shape discriminated by a field value, in one collection. +- Pass the variant's model name, such as `'Bug'`, not the string in its `@@base` attribute. +- On PostgreSQL, a variant's own fields are in a second table alongside the base model's table. That changes how some writes behave; see [`createAndCount()`](#createandcount) and [`upsert()`](#upsert). +- On MongoDB, every variant is a document in the one collection, told apart by the field named in `@@discriminator`. #### Options | Name | Type | Required | Description | |---|---|---|---| -| `variantName` | `string` | Yes | The variant to narrow to. | +| `variantName` | `string` | Yes | The model name of the variant. | #### Return type | Return type | Example | Description | |---|---|---| -| `Collection` (variant-narrowed) | `db.orm.public.Task.variant('Bug')` | A collection scoped to the variant. | +| `Collection`, narrowed to the variant | `db.orm.public.Task.variant('Bug')` | A collection holding only rows of that variant. | #### Examples @@ -708,7 +746,11 @@ 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). For a task-oriented walkthrough, see [Reading data](/orm/fundamentals/reading-data). +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). + +There is no `count()` method on either database. On PostgreSQL, count with `aggregate((a) => ({ n: a.count() }))`. On MongoDB, count with the pipeline builder. Both are shown under [Count records](/orm/fundamentals/reading-data#count-records). + +For a task-oriented walkthrough, see [Reading data](/orm/fundamentals/reading-data). ### `all()` @@ -718,8 +760,8 @@ Resolve the query to every matching row. - Available for PostgreSQL and MongoDB. - `all()` returns an [`AsyncIterableResult`](#asynciterableresult): you can `await` it to collect an array, or use `for await` to stream rows one at a time. -- Re-`await`ing (or calling `.toArray()` on) an already-buffered result is safe: the cached array is returned, with no re-query and no throw. -- **Switching consumption mode** on a consumed result throws `RUNTIME.ITERATOR_CONSUMED` (`already been consumed`). See [Single consumption and mode switching](#single-consumption-and-mode-switching). +- `await`ing a result you have already `await`ed is safe. You get the same array back, and the query does not run again. +- Each result can be used one way only. Awaiting it and then looping it with `for await`, or the reverse, throws an error whose `code` is `RUNTIME.ITERATOR_CONSUMED`. See [Single consumption and mode switching](#single-consumption-and-mode-switching). #### Options @@ -772,7 +814,8 @@ Resolve the query to the first matching row, or `null` if none matches. - Available for PostgreSQL and MongoDB. - On PostgreSQL, `first()` accepts an inline filter: a shorthand object or a callback (`first((p) => p.priority.eq('urgent'))`). -- On MongoDB, `first()` has no filter argument; filter first with `where(...)`, then call `first()`. +- On PostgreSQL, an inline filter is added to any earlier `where()` call. Both have to match. +- On MongoDB, `first()` takes no filter argument. Filter with `where(...)` first, then call `first()`. - Returns `null` when no row matches. #### Options @@ -828,7 +871,8 @@ On PostgreSQL you can subclass `Collection` to attach domain methods, and regist #### Remarks - PostgreSQL only. The MongoDB ORM has no equivalent collection-subclassing mechanism. -- Registering custom collections requires building the client with the `orm(...)` factory, not the `postgres()` client's built-in `.orm` facet. The `postgres()` client has no `collections` option, so its facet always resolves models to the base `Collection`. Pass a `collections` map to `orm({ runtime, context, collections })` to register subclasses. +- To register a subclass, build the client with the `orm(...)` function and pass a `collections` object. The client's own `.orm` cannot do this: `postgres(...)` has no `collections` option, so `db.orm` always uses the plain `Collection`. +- A method that returns `this.variant('Bug')` gives back a collection narrowed to the `Bug` model. It is chainable, so you finish it with `all()`, `first()`, or any other method. #### Examples @@ -852,12 +896,13 @@ class TaskCollection extends Collection { const client = postgres({ contractJson, url: process.env.DATABASE_URL }); const runtime = await client.connect(); -// The orm() factory accepts a `collections` map; the client's `.orm` facet does not. +// orm() takes a `collections` option; the client's own `.orm` does not. const db = orm({ runtime, + // client.context carries the contract, which orm() needs to build collections. context: client.context, collections: { Task: TaskCollection }, -}).public; +}).public; // .public is the PostgreSQL schema const bugs = await db.Task.bugs().all(); const features = await db.Task.features().all(); @@ -865,12 +910,16 @@ const features = await db.Task.features().all(); ## Write methods [#mutation-terminals] -Mutations write to the database. `create`, `createAll`, `createAndCount`, `update`, `updateAll`, `updateAndCount`, `delete`, `deleteAll`, `deleteAndCount`, and `upsert` are available on both databases, with several behavioral differences called out per method. For a task-oriented walkthrough, see [Writing data](/orm/fundamentals/writing-data); for grouping several writes into one unit, see [Transactions](/orm/fundamentals/transactions). +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. -:::warning[`where()` enforcement differs sharply between databases] -Always call `where()` before `update`, `updateAll`, `updateAndCount`, `delete`, `deleteAll`, `deleteAndCount`, or `upsert`. On **MongoDB** all seven enforce this at runtime: calling them with no filter throws `Error: () requires a .where() filter`. On **PostgreSQL** only `update()` and `delete()` are typed to require a prior `where()`, and even there it is a **type-only** guard with no runtime check. If you bypass the type system, `update()` and `delete()` do not throw and do not mass-mutate; they narrow to a single row by identity (a `SELECT ... LIMIT 1` over the current, possibly empty, filters, then act on that one row). The `*All`/`*Count` variants on PostgreSQL compile with no `WHERE` clause and affect every row. Always call `where()` first. +For a task-oriented walkthrough, see [Writing data](/orm/fundamentals/writing-data). 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 you get an error if you forget, with the message `updateAll() requires a .where() filter. Call .where() before .updateAll()`. On PostgreSQL you do not: `update()` and `delete()` change one arbitrary row, and `updateAll()` and `deleteAll()` change every row. TypeScript does refuse to compile `update()` and `delete()` without a `where()`, but nothing checks at run time. ::: +The examples below use `aliceId`, `bobId`, `carolId`, `postId`, `tagId`, `tutorialId`, and `userId` for the ids of rows that already exist. Substitute your own. + ### `create()` Insert a single row and return it. @@ -878,14 +927,19 @@ Insert a single row and return it. #### Remarks - Available for PostgreSQL and MongoDB. -- On MongoDB, `create()` returns the input data plus the server-assigned `_id`. It does not re-read the stored document, unlike PostgreSQL's `RETURNING`-backed `create()`. -- On PostgreSQL, `create()` supports nested `create()` on a child-owned relation and nested `connect()` on a parent-owned relation, all within one transaction. These nested mutators are type-checked but their runtime behavior on MongoDB is unverified on the current test contract. +- On PostgreSQL, leave out any field your contract gives a default, including the id. Nullable fields can be left out too. Every other field is required. +- On MongoDB, `_id` is the only field you can leave out. Give every other field a value, and write `null` for a field you want empty. +- On MongoDB, `create()` returns the values you passed plus the `_id` the server assigned. It does not read the stored document back. On PostgreSQL the returned row comes from the database. +- On PostgreSQL, `create()` can create or link related rows in the same transaction. Nest `create()` on the side that does not hold the foreign key, and `connect()` on the side that does. +- Nested `create()` and `connect()` are not available on MongoDB. #### Options | Name | Type | Required | Description | |---|---|---|---| -| `data` | Object of field values, optionally with relation mutators (`create`, `connect`) | Yes | The row to insert. | +| `data` | Object of field values | Yes | The row to insert. | + +On PostgreSQL, `data` can also carry a `create` or `connect` callback for a related row, as shown below. #### Return type @@ -912,7 +966,7 @@ const user = await db.orm.users.create({ // user._id is the server-assigned id ``` -##### Nested `create()` on a child-owned relation (PostgreSQL) +##### Nested `create()` on the side without the foreign key (PostgreSQL) [#nested-create-on-a-child-owned-relation-postgresql] ```typescript const author = await db.orm.public.User.create({ @@ -925,7 +979,7 @@ const author = await db.orm.public.User.create({ }); ``` -##### Nested `connect()` on a parent-owned relation (PostgreSQL) +##### Nested `connect()` on the side with the foreign key (PostgreSQL) [#nested-connect-on-a-parent-owned-relation-postgresql] ```typescript const post = await db.orm.public.Post.create({ @@ -988,13 +1042,14 @@ for await (const tag of db.orm.public.Tag.createAll([{ label: 'gamma' }, { label ### `createAndCount()` -Insert rows without materializing them, returning the count. +Insert rows and return how many were inserted, without reading them back. #### Remarks - Available for PostgreSQL and MongoDB. -- On PostgreSQL, `createAndCount()` is **not supported on a multi-table-inheritance variant**. The base and variant rows live in separate tables, so a single `RETURNING`-less `INSERT` can't populate both. It throws `Error: createAndCount() is not supported for MTI variant "" ... Use createAll() instead.` -- On MongoDB, `createAndCount()` on a discriminated variant works normally. Mongo variants are one document shape in one collection, so there is no split-table restriction. +- On PostgreSQL, `createAndCount()` does not work on a multi-table inheritance variant, a variant stored in its own table. Use `createAll()` instead. +- Calling it on such a variant throws an error whose `code` is `ORM.OPERATION_UNSUPPORTED`, with the message `createAndCount() is not supported for MTI variant "Tutorial" on model "Post". Use createAll() instead.` +- On MongoDB, a variant is one document in one collection, so `createAndCount()` works on a variant as usual. #### Options @@ -1028,7 +1083,7 @@ const inserted = await db.orm.posts.variant('Tutorial').createAndCount([ duration: 10, }, ]); -// 1 — no split-table restriction on Mongo variants +// 1 ``` ### `update()` @@ -1038,11 +1093,11 @@ Update the matched row and return it, or `null` if none matches. #### Remarks - Available for PostgreSQL and MongoDB. Requires a prior `where()` (see the warning at the top of this section). -- On MongoDB, `update()` accepts a data object **or** a field-operations callback (`(t) => [t.field.inc(5), t.other.set(...)]`). The callback groups operators into one `findOneAndUpdate`; see [Field update operations](#field-update-operations). -- On PostgreSQL, `update()` accepts a data object only. It has **no** field-operations callback overload: passing a function is silently a no-op (it resolves to `null`), not an error. A bare function has no enumerable own properties, so no column is targeted. -- On PostgreSQL, `update()` supports nested `connect()` (relink a parent-owned foreign key) and nested `disconnect()` (unlink a many-to-many row without deleting it). -- On MongoDB, `update()` is backed by `findOneAndUpdate` and is an atomic single-document update. -- Returns `null` when no row matches. +- On MongoDB, `update()` takes a data object or a callback that returns field operations, such as `(t) => [t.duration.inc(5)]`. See [Field update operations](#field-update-operations). +- On PostgreSQL, `update()` takes a data object only. Passing a function is a mistake that returns `null`; pass an object. +- On PostgreSQL, `update()` can also relink related rows. `connect()` points the foreign key at a different row, and `disconnect()` removes a many-to-many link without deleting either row. +- On MongoDB, `update()` changes one document in a single database operation. +- Returns `null` when no row matches. On PostgreSQL it also returns `null` when you passed a function instead of an object. #### Options @@ -1094,7 +1149,7 @@ const updated = await db.orm.public.Post.where({ id: postId }) .update({ tags: (tag) => tag.disconnect([{ id: tagId }]), }); -// only the junction row is removed; the Tag row itself still exists +// only the join-table row is removed; the Tag row itself still exists ``` For Prisma ORM 7 users, `update()` moves the filter into `where()` and drops the `data` wrapper: @@ -1111,8 +1166,9 @@ Update every matching row and collect the results. #### Remarks - Available for PostgreSQL and MongoDB. Call `where()` first: MongoDB enforces this at runtime; PostgreSQL does not (see the section warning above: without a filter these affect every row). -- On PostgreSQL, `updateAll()` is a single `UPDATE ... RETURNING` statement, and is atomic. -- On MongoDB, `updateAll()` is **not atomic**. It (1) reads the matching `_id`s, (2) runs an update against the original filter, then (3) re-reads by those captured `_id`s. A concurrent write between these steps could change which documents match or their values; the result reflects the `_id` set from step 1, not one atomic snapshot. +- On PostgreSQL, `updateAll()` runs as one statement, so every matching row changes together. +- On MongoDB, `updateAll()` is not one operation. It collects the matching ids, updates, then reads those documents back. A document someone else changes in between can come back with their values, and a document that starts matching only after the first step is updated but not returned. +- Prisma ORM has no `db.transaction()` on MongoDB, so there is no way to avoid this from the client. If it matters, run the writes yourself in a MongoDB driver session. See [Transactions on MongoDB](/orm/fundamentals/transactions#transactions-on-mongodb). #### Options @@ -1136,7 +1192,7 @@ const updated = await db.orm.public.Post.where({ userId: aliceId }).updateAll({ ```typescript tab="MongoDB" const updated = await db.orm.users.where({ role: 'author' }).updateAll({ role: 'admin' }); -// non-atomic: ids are captured, updated, then re-read +// see the remark above: this is not one operation ``` ### `updateAndCount()` @@ -1188,7 +1244,7 @@ Remove the matched row and return it, or `null` if none matches. #### Remarks - Available for PostgreSQL and MongoDB. Requires a prior `where()` (see the section warning above). -- On MongoDB, `delete()` is backed by `findOneAndDelete`. +- On MongoDB, `delete()` removes one document in a single database operation. - Returns `null` when no row matches. #### Options @@ -1227,7 +1283,8 @@ Remove every matching row and collect the results. #### Remarks -- Available for PostgreSQL and MongoDB. Call `where()` first: MongoDB enforces this at runtime; PostgreSQL does not (see the section warning above: without a filter these affect every row). +- Available for PostgreSQL and MongoDB. Call `where()` first: MongoDB enforces this at run time; PostgreSQL does not (see the section warning above: without a filter this deletes every row). +- Every matching row is deleted before the first row reaches you. Stopping the loop early still leaves nothing behind. #### Options @@ -1288,11 +1345,13 @@ Insert a row if none matches, otherwise update the existing row. #### Remarks -- Available for PostgreSQL and MongoDB. Requires a prior `where()` on MongoDB (see the section warning). -- On PostgreSQL, `upsert()` uses `conflictOn` to name the unique target. The `update` side is a plain data object. -- On MongoDB, the `update` side may be a data object **or** a field-operations callback. On insert, `update` fields apply via `$set` and the remaining create-only fields via `$setOnInsert`, so `update` values win over `create` values on any overlapping field. -- On PostgreSQL, `upsert()` is **not supported on a multi-table-inheritance variant** (same restriction as [`createAndCount()`](#createandcount)): it throws `Error: upsert() is not supported for MTI variant "" ...`. -- On MongoDB, the returned `_id` on the update path comes back as a raw driver `ObjectId`, not a decoded hex string. Compare it with `String(...)`. +- On MongoDB, call `where()` first; the filter decides which document is updated. +- On PostgreSQL, `where()` is not used. `conflictOn` decides which row counts as already existing. +- `conflictOn` takes one unique column with a value, such as `conflictOn: { label: 'typescript' }`. Only the column name is read: it names the unique constraint the database checks. The value is required by the type but is not used, so it does not have to match the value in `create`. +- Leave `conflictOn` out to use the primary key instead. +- On MongoDB, the `update` side takes a data object or a field-operations callback. If a field appears in both `create` and `update`, the `update` value is used, even when the row is inserted. +- On PostgreSQL, `upsert()` does not work on a multi-table inheritance variant, the same restriction as [`createAndCount()`](#createandcount). It throws an error whose `code` is `ORM.OPERATION_UNSUPPORTED`, with the message `upsert() is not supported for MTI variant "Tutorial" on model "Post". Use createAll() instead.` +- On MongoDB, the `_id` on the returned row comes back as the driver's `ObjectId`, not a hex string. Compare it as text: `String(user._id) === String(bobId)`. #### Options @@ -1300,7 +1359,7 @@ Insert a row if none matches, otherwise update the existing row. |---|---|---|---| | `create` | Data object | Yes | The row to insert if none matches. | | `update` | Data object, or (MongoDB) a field-operations callback | Yes | The changes to apply if a row matches. | -| `conflictOn` | Object naming the unique target (PostgreSQL) | PostgreSQL | The conflict target that decides insert vs. update. | +| `conflictOn` | Object with one unique column and a value (PostgreSQL) | No | Names the unique constraint that decides insert or update. Omit to use the primary key. | #### Return type @@ -1313,14 +1372,14 @@ Insert a row if none matches, otherwise update the existing row. ##### Insert or update (PostgreSQL) ```typescript -// Insert path: no existing row has label 'brand-new', so the create side wins. +// Insert: no existing row has label 'brand-new', so the create side is used. const inserted = await db.orm.public.Tag.upsert({ create: { id: '30000000-0000-0000-0000-000000000099', label: 'brand-new' }, update: { label: 'brand-new-updated' }, conflictOn: { label: 'brand-new' }, }); -// Update path: 'typescript' already exists, so the update runs against it. +// Update: a row with label 'typescript' already exists, so the update runs against it. const updated = await db.orm.public.Tag.upsert({ create: { id: '30000000-0000-0000-0000-000000000098', label: 'typescript' }, update: { label: 'typescript-renamed' }, @@ -1341,7 +1400,7 @@ const user = await db.orm.users.where({ email: 'newperson@example.com' }).upsert }, update: { bio: 'set on upsert' }, }); -// on insert, update fields win over create fields on overlap +// on insert, an `update` value replaces the `create` value for the same field ``` ##### Field-operations callback on the update side (MongoDB) @@ -1363,7 +1422,7 @@ const post = await db.orm.posts }); ``` -For Prisma ORM 7 users, `upsert()` replaces the `where` conflict target with `conflictOn` (PostgreSQL): +For Prisma ORM 7 users, `upsert()` replaces the `where` argument with `conflictOn` (PostgreSQL): ```diff - const tag = await prisma.tag.upsert({ @@ -1373,14 +1432,14 @@ For Prisma ORM 7 users, `upsert()` replaces the `where` conflict target with `co - }); + const tag = await db.orm.public.Tag.upsert({ + update: { label: 'typescript-renamed' }, -+ create: { id, label: 'typescript' }, ++ create: { label: 'typescript' }, + conflictOn: { label: 'typescript' }, + }); ``` ## Grouped aggregates -PostgreSQL supports aggregation over a result set, both flat (`aggregate()`) and grouped (`groupBy().aggregate()`), with `having()` to filter groups. MongoDB has no equivalent: `aggregate()` and `groupBy()` do not exist on the Mongo collection and calling them throws `TypeError`. +PostgreSQL supports aggregation over a result set, both flat (`aggregate()`) and grouped (`groupBy().aggregate()`), with `having()` to filter groups. MongoDB has neither method: `aggregate()` and `groupBy()` do not exist on a MongoDB collection, and calling them throws a `TypeError`. Use the pipeline builder for MongoDB aggregation instead, 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: @@ -1402,6 +1461,7 @@ model Customer { model Order { id Uuid @id @default(uuid()) customerId Uuid + channel String amount Int quantity Int placedAt DateTime @default(now()) @@ -1421,16 +1481,17 @@ Compute aggregates over the current result set. #### Remarks - PostgreSQL only. Calling `aggregate()` on a Mongo collection throws `TypeError`. -- `count()` needs no field argument and always resolves to a number (`0` over an empty set). +- `count()` counts rows. It takes no argument and always resolves to a number (`0` over an empty set). +- `count(field)` counts the rows where that field is not null. - `sum()` and `avg()` resolve to `null`, not `0`, over an empty result set. This is standard SQL: aggregating zero rows with `SUM`/`AVG` yields `NULL`. Their TypeScript return type is `number | null` for this reason. #### Options | Name | Type | Required | Description | |---|---|---|---| -| `selector` | Callback `(agg) => ({ alias: agg.fn(...) })` | Yes | The aggregates to compute, keyed by output alias. | +| `selector` | Callback `(agg) => ({ alias: agg.sum('amount') })` | Yes | The aggregates to compute, keyed by output alias. | -The selector's `agg` exposes `count()`, `sum(field)`, `avg(field)`, `min(field)`, `max(field)`. +The selector's `agg` gives you `count()`, `count(field)`, `sum(field)`, `avg(field)`, `min(field)`, and `max(field)`. #### Return type @@ -1447,10 +1508,19 @@ const stats = await db.orm.public.Order.aggregate((agg) => ({ total: agg.count() // { total: 10 } ``` +##### Count the rows where a field is set (PostgreSQL) + +```typescript +const stats = await db.orm.public.Order.aggregate((agg) => ({ withQuantity: agg.count('quantity') })); +// { withQuantity: 8 } +``` + ##### Sum and average (PostgreSQL) ```typescript -const stats = await db.orm.public.Order.where({ customerId: acmeId }).aggregate((agg) => ({ +const stats = await db.orm.public.Order.where({ + customerId: '20000000-0000-0000-0000-000000000001', +}).aggregate((agg) => ({ totalAmount: agg.sum('amount'), avgAmount: agg.avg('amount'), })); @@ -1492,13 +1562,17 @@ Group rows by one or more fields, then aggregate per group. #### Remarks - PostgreSQL only. Calling `groupBy()` on a Mongo collection throws `TypeError`. -- `groupBy(...fields)` returns a `GroupedCollection`. Call `.aggregate(...)` on it to produce one row per distinct group key, carrying both the key field(s) and the aggregate aliases. +- Pass field names as separate arguments: `groupBy('customerId', 'channel')`. +- `groupBy(...)` returns a `GroupedCollection`. Call `.aggregate(...)` on it to get one row per group, carrying both the grouping fields and the aggregate aliases. +- A `where(...)` before `groupBy(...)` filters the rows that get grouped. +- `orderBy(...)` on a grouped collection sorts the groups by a grouping field. You cannot sort them by an aggregate value here. For that, use the SQL query builder, described in [Advanced queries](/orm/fundamentals/advanced-queries). +- `limit(n)` and `offset(n)` page the groups, and both require an `orderBy(...)` before them. Without one they are a TypeScript error. #### Options | Name | Type | Required | Description | |---|---|---|---| -| `...fields` | Field names (`string`) | Yes | The grouping key(s). | +| `...fields` | One or more field names (`string`) | Yes | The fields to group by. | #### Return type @@ -1518,6 +1592,33 @@ const perCustomer = await db.orm.public.Order.groupBy('customerId').aggregate((a // one row per customer, each { customerId, orderCount, totalAmount } ``` +##### Group by two fields (PostgreSQL) + +```typescript +const perCustomerAndChannel = await db.orm.public.Order.groupBy('customerId', 'channel').aggregate( + (agg) => ({ totalAmount: agg.sum('amount') }), +); +// one row per customer and channel, each { customerId, channel, totalAmount } +``` + +##### Filter rows, then group (PostgreSQL) + +```typescript +const perCustomer = await db.orm.public.Order.where((o) => o.amount.gte(100)) + .groupBy('customerId') + .having((h) => h.count().gt(1)) + .aggregate((agg) => ({ totalAmount: agg.sum('amount') })); +``` + +##### Sort and page the groups (PostgreSQL) + +```typescript +const firstTwoChannels = await db.orm.public.Order.groupBy('channel') + .orderBy((group) => group.channel.desc()) + .limit(2) + .aggregate((agg) => ({ orderCount: agg.count() })); +``` + For Prisma ORM 7 users, `groupBy()` chains into `aggregate()` instead of taking a `by` array with aggregate keys: ```diff @@ -1532,13 +1633,16 @@ Filter groups by an aggregate comparison. #### Remarks - PostgreSQL only. Chained on a `GroupedCollection` between `groupBy()` and `aggregate()`. -- The `having()` callback exposes `count()`, `sum(field)`, `avg(field)`, `min(field)`, `max(field)`, each returning comparison methods (`eq`, `neq`, `gt`, `lt`, `gte`, `lte`). It compiles to a SQL `HAVING` clause. +- The `having()` callback gives you `count()`, `sum(field)`, `avg(field)`, `min(field)`, and `max(field)`. Each of those returns an object with `eq`, `neq`, `gt`, `lt`, `gte`, and `lte`. +- You cannot refer to an alias from `aggregate()` here. Write the aggregate out again in `having()`. #### Options | Name | Type | Required | Description | |---|---|---|---| -| `predicate` | Callback `(h) => h.fn(field).cmp(value)` | Yes | The group-level condition. | +| `predicate` | Callback `(h) => h.().()` | Yes | The group-level condition. | + +The angle-bracket names above are placeholders. A real call is `h.sum('amount').gt(1000)`. #### Return type @@ -1566,20 +1670,20 @@ const groupsWithAtLeastFive = await db.orm.public.Order.groupBy('customerId') ## Filter conditions and operators -Filters build the predicate inside `where()` (and relation refinements). PostgreSQL and MongoDB use different filter surfaces. +Filters build the condition inside `where()`, and inside relation filters. PostgreSQL and MongoDB have different filter APIs. - **PostgreSQL** uses column-level comparison methods (`u.email.eq(...)`) inside a callback, plus standalone combinator functions imported from `@prisma/orm-postgres/orm-client`. - **MongoDB** uses static factories on `MongoFieldFilter` imported from `@prisma/orm-mongo/query-ast/execution`, plus `.and()`/`.not()` instance methods and the standalone `MongoOrExpr.of(...)`. ### Scalar comparisons (PostgreSQL) -Column-level comparison methods on a field accessor. +Column-level comparison methods on a field. #### Remarks -- PostgreSQL. Which methods a field has depends on its type: `eq`/`neq`/`in`/`notIn` on any field that can be compared for equality; `gt`/`lt`/`gte`/`lte` on any field that can be ordered; `like` on text fields; `isNull`/`isNotNull` on every field. -- `like()` matches a SQL pattern case-sensitively. -- `ilike()` matches case-insensitively. It is a Postgres-adapter-registered operation attached to any textual-trait field, not a core comparison method. +- PostgreSQL. Which methods a field has depends on its type: `eq`/`neq`/`in`/`notIn` on any field that can be compared for equality; `gt`/`lt`/`gte`/`lte` on any field that can be ordered; `like` and `ilike` on text fields; `isNull`/`isNotNull` on every field. +- `like()` matches a SQL pattern case-sensitively. `ilike()` matches the same pattern case-insensitively. +- `in([])` matches no rows. `notIn([])` matches every row. #### Options @@ -1628,9 +1732,10 @@ Combine or negate predicates. #### Remarks -- On PostgreSQL, `and`, `or`, `not`, and `all` are **standalone functions** imported from `@prisma/orm-postgres/orm-client`. They build AST nodes directly and are independent of any model accessor. -- On MongoDB, `.and(other)` and `.not()` are **instance methods** on any `MongoFieldFilter`. There is **no `.or()` instance method** and no `$nor` support. To OR filters, construct `MongoOrExpr.of([...])`. -- On PostgreSQL, `all()` takes no arguments and returns a constant-true predicate, useful wherever a predicate is structurally required. +- On PostgreSQL, `and`, `or`, `not`, and `all` are **standalone functions** imported from `@prisma/orm-postgres/orm-client`. You import them; they are not methods on a field. +- On PostgreSQL, `all()` takes no arguments and returns a condition that is always true. Use it where the code has to pass a condition but you do not want to filter anything, such as a branch of your own helper. This `all()` is not the `.all()` you call at the end of a chain to run the query. +- On MongoDB, `.and(other)` and `.not()` are **instance methods** on every filter expression, including `MongoFieldFilter`, `MongoOrExpr`, and `MongoAndExpr`. There is **no `.or()` instance method**. To OR filters, build `MongoOrExpr.of([...])`. +- On MongoDB, an AND of two ORs is `MongoOrExpr.of([...]).and(MongoOrExpr.of([...]))`. #### Examples @@ -1639,11 +1744,21 @@ Combine or negate predicates. ```typescript import { and, or, not } from '@prisma/orm-postgres/orm-client'; -const both = await db.orm.public.Post.where((p) => and(p.priority.eq('low'), p.userId.eq(carolId))).all(); +const both = await db.orm.public.Post.where((p) => + and(p.priority.eq('low'), p.userId.eq('00000000-0000-0000-0000-000000000003')), +).all(); const either = await db.orm.public.Post.where((p) => or(p.priority.eq('urgent'), p.priority.eq('high'))).all(); const negated = await db.orm.public.Post.where((p) => not(p.priority.eq('low'))).all(); ``` +##### `all` (PostgreSQL) + +```typescript +import { all } from '@prisma/orm-postgres/orm-client'; + +const everyPost = await db.orm.public.Post.where(() => all()).all(); +``` + ##### `and` / `or` / `not` (MongoDB) ```typescript @@ -1658,8 +1773,20 @@ const either = await db.orm.users const negated = await db.orm.users.where(MongoFieldFilter.eq('name', 'Alice').not()).all(); ``` +##### AND of two ORs (MongoDB) + +```typescript +const authorsNamedAliceOrBob = await db.orm.users + .where( + MongoOrExpr.of([MongoFieldFilter.eq('name', 'Alice'), MongoFieldFilter.eq('name', 'Bob')]).and( + MongoOrExpr.of([MongoFieldFilter.eq('role', 'author'), MongoFieldFilter.eq('role', 'editor')]), + ), + ) + .all(); +``` + :::note[No `.or()` instance method or `$nor` on MongoDB] -`MongoFieldFilter` expressions have `.and(...)` and `.not()` instance methods, but no `.or(...)`. Accessing `.or` is `undefined` and calling it throws `TypeError`. Use `MongoOrExpr.of([...])` for disjunction. There is no `$nor` combinator anywhere in the library. +MongoDB filter expressions have `.and(...)` and `.not()`, but no `.or(...)`. Use `MongoOrExpr.of([...])` instead. There is no `$nor` combinator in the library. ::: ### Relation filters (PostgreSQL) @@ -1668,10 +1795,12 @@ Filter parents by their related rows. #### Remarks -- PostgreSQL. `some()`, `every()`, and `none()` are methods on a to-many relation accessor and compile to correlated `EXISTS` / `NOT EXISTS` subqueries. -- `some()` with no predicate matches parents with at least one related row. -- `every()` is vacuously true for a parent with zero related rows. -- A to-one relation is filtered from the child side with the same `some(...)` shape (the compiled SQL is a correlated subquery regardless of cardinality). +- PostgreSQL. `some()`, `every()`, and `none()` are methods on a to-many relation, and they compile to SQL `EXISTS` subqueries. +- Each of the three takes either a callback or a plain object of equality matches: `u.posts.some((p) => p.priority.eq('urgent'))` and `u.posts.some({ priority: 'urgent' })` mean the same thing. +- `some()` with no argument matches parents with at least one related row. +- A user with no posts matches `every()`. +- `not(u.posts.some(...))` and `u.posts.none(...)` match the same rows. Prefer `none(...)`. +- A to-one relation is filtered the same way, from the side that holds the foreign key: `p.user.some(...)`. #### Examples @@ -1697,14 +1826,19 @@ A plain object of equality matches, ANDed together. #### Remarks - Available for PostgreSQL and MongoDB. -- Each key must support equality. Multiple keys are combined with implicit AND. +- Multiple keys are combined with implicit AND. +- A key set to `null` becomes an IS NULL check. A key set to `undefined` is skipped. +- A field whose type cannot be compared for equality throws an error whose `code` is `ORM.FILTER_UNSUPPORTED`. Filter that field with a comparison method instead. #### Examples ##### Shorthand object ```typescript tab="PostgreSQL" -const row = await db.orm.public.Post.where({ userId: aliceId, priority: 'high' }).first(); +const row = await db.orm.public.Post.where({ + userId: '00000000-0000-0000-0000-000000000001', + priority: 'high', +}).first(); ``` ```typescript tab="MongoDB" @@ -1718,14 +1852,17 @@ Static factories that build MongoDB filter expressions. #### Remarks - MongoDB. Imported from `@prisma/orm-mongo/query-ast/execution`. -- Exactly eleven factories exist: `of`, `eq`, `neq`, `gt`, `lt`, `gte`, `lte`, `in`, `nin`, `isNull`, `isNotNull`. The inequality factory is `neq`, **not** `ne` (`MongoFieldFilter.ne` is `undefined`). -- `isNull` / `isNotNull` are sugar for equals-null / not-equals-null, not a `$exists` check. -- The factories `exists`, `regex`, `elemMatch`, `all`, and `size` do **not** exist on `MongoFieldFilter`. There is a separate `MongoExistsExpr` class for `$exists`, but no regex, `elemMatch`, `$all`, or `$size` support anywhere in the library. +- Exactly eleven factories exist: `of`, `eq`, `neq`, `gt`, `lt`, `gte`, `lte`, `in`, `nin`, `isNull`, `isNotNull`. There is no `ne`; use `neq`. +- `of(field, operator, value)` takes a raw MongoDB operator string, for any operator the other ten factories do not cover. +- `isNull` / `isNotNull` are shorthand for equals-null and not-equals-null, not a `$exists` check. +- For `$exists`, import `MongoExistsExpr` from the same module and call `MongoExistsExpr.exists(field)` or `MongoExistsExpr.notExists(field)`. +- The factories `exists`, `regex`, `elemMatch`, `all`, and `size` do **not** exist on `MongoFieldFilter`, and the library has no regex, `elemMatch`, `$all`, or `$size` support anywhere. #### Options | Method | Type | Description | |---|---|---| +| `of(field, operator, value)` | Field + MongoDB operator string + value | Any operator, written out, such as `'$gt'`. | | `eq(field, value)` / `neq(field, value)` | Field + value | Equality / inequality. | | `gt` / `lt` / `gte` / `lte` `(field, value)` | Field + ordered value | Ordered comparisons. | | `in(field, values)` / `nin(field, values)` | Field + array | Membership / exclusion. | @@ -1756,15 +1893,23 @@ const noBio = await db.orm.users.where(MongoFieldFilter.isNull('bio')).all(); const hasBio = await db.orm.users.where(MongoFieldFilter.isNotNull('bio')).all(); ``` +##### An operator written out, and `$exists` (MongoDB) + +```typescript +import { MongoExistsExpr, MongoFieldFilter } from '@prisma/orm-mongo/query-ast/execution'; + +const older = await db.orm.users.where(MongoFieldFilter.of('age', '$gt', 30)).all(); +const withBioField = await db.orm.users.where(MongoExistsExpr.exists('bio')).all(); +``` + ### Dot-notation into an embedded object (MongoDB) Filter into a field of an embedded value object using a dot path. #### Remarks -- MongoDB. Runtime behavior is verified; the type surface does not declare embedded paths, so the object-shorthand form needs a cast. -- `MongoWhereFilter` is typed against the model's own top-level field keys, so `{ 'address.city': ... }` is a compile error without a cast. At runtime there is no such restriction: the object key is used verbatim as the Mongo field path. -- For a type-clean alternative, use `MongoFieldFilter.eq('address.city', ...)`, which takes a bare `string` and needs no cast. +- MongoDB. This works at runtime, but the TypeScript types do not know nested paths, so the object form needs a cast. Prefer `MongoFieldFilter.eq`, which takes a bare `string` path and needs no cast. +- `MongoWhereFilter` is typed against the model's own top-level field names. `{ 'address.city': ... }` is therefore a compile error without a cast. #### Examples @@ -1773,21 +1918,21 @@ Filter into a field of an embedded value object using a dot path. ```typescript import { MongoFieldFilter } from '@prisma/orm-mongo/query-ast/execution'; -// Type-clean form: MongoFieldFilter takes a bare string path. const usersInSf = await db.orm.users.where(MongoFieldFilter.eq('address.city', 'San Francisco')).all(); ``` -The object-shorthand form works at runtime but requires a cast, because the embedded path is not in the model's declared field keys: +The object form works at runtime too, with a cast: ```typescript const usersInSf = await db.orm.users .where({ 'address.city': 'San Francisco' } as unknown as Record) .all(); ``` - ## Field update operations -MongoDB field operations are accessed through a field accessor inside `update()`, `updateAndCount()`, and `upsert()` callbacks (for example `t.duration.inc(5)`). Each operation targets a field and produces a Mongo update operator. PostgreSQL's `update()` has no equivalent callback form (see [`update()`](#update)). +On MongoDB, `update()`, `updateAndCount()`, and `upsert()` accept a callback in place of a data object. The callback receives one argument, written `u` or `t` below, with one property per field: `u.bio`, `t.duration`. Call an operation on that property, such as `u.bio.set('Hello')`. Each call returns an update instruction; collect them in the array you return. PostgreSQL's `update()` has no callback form (see [`update()`](#update)). + +All three methods require `.where()`. `update()` returns the updated document, or `null` when nothing matched. `updateAndCount()` returns the number of documents it changed. ### Available operations (MongoDB) @@ -1797,56 +1942,75 @@ MongoDB field operations are accessed through a field accessor inside `update()` - `set()`, `unset()`, `inc()`, and `mul()` are available and verified. - `set(value)` assigns a field. - `unset()` removes a field. - - `inc(n)` increments a numeric field. Applied to a missing field, `$inc` initializes it to the increment. + - `inc(n)` increments a numeric field. Applied to a missing field, it sets the field to `n`. - `mul(n)` multiplies a numeric field. Applied to a missing field, MongoDB sets it to `0` regardless of the multiplier. -- The field accessor is typed against the collection's **base** model, even after `variant(...)`. Accessing a variant-only field (such as `Tutorial.duration`) inside a callback is a compile error, though it works correctly at runtime because the accessor is a `Proxy` that resolves any field name. +- The callback argument is typed against the collection's base model, even after `variant(...)`. TypeScript only knows the base model's fields, so `t.duration.inc(10)` is a type error even though it runs correctly. Put `// @ts-expect-error` on that line to accept it. #### Examples +These examples use the MongoDB accessors from [Setting up the client](#setting-up-the-client). ##### `set()` (MongoDB) +Return as many operations as you want. They are applied together, in one write. + ```typescript const updated = await db.orm.users - .where({ _id: bobId }) - .update((u) => [u.bio.set('Set via field op')]); + .where({ _id: '6650f1c2a1b2c3d4e5f60002' }) + .update((u) => [u.bio.set('Set via field op'), u.name.set('Bob R.')]); ``` ##### `inc()` and `mul()` (MongoDB) +`duration` is declared on `Tutorial`, not on the base `Post` model, so each line that touches it needs `// @ts-expect-error`. + ```typescript const incremented = await db.orm.posts .variant('Tutorial') - .where({ _id: tutorialId }) + .where({ _id: '6650f1c2a1b2c3d4e5f60003' }) + // @ts-expect-error duration is a Tutorial field, not a Post field .update((t) => [t.duration.inc(10)]); const multiplied = await db.orm.posts .variant('Tutorial') - .where({ _id: tutorialId }) + .where({ _id: '6650f1c2a1b2c3d4e5f60003' }) + // @ts-expect-error duration is a Tutorial field, not a Post field .update((t) => [t.duration.mul(3)]); ``` ##### `unset()` (MongoDB) +`updateAndCount()` takes the callback in the same place. `upsert()` takes it as its `update` key, beside `create`: `upsert({ create: { ... }, update: (u) => [u.bio.unset()] })`. + ```typescript -const updated = await db.orm.users.where({ _id: aliceId }).update((u) => [u.bio.unset()]); +const updated = await db.orm.users.where({ _id: '6650f1c2a1b2c3d4e5f60001' }).update((u) => [u.bio.unset()]); +const changed = await db.orm.users.where({ _id: '6650f1c2a1b2c3d4e5f60002' }).updateAndCount((u) => [u.bio.unset()]); ``` ### Operations that do not exist (MongoDB) #### Remarks -- MongoDB. These are library gaps, not contract limitations. `min()`, `max()`, `rename()`, and `currentDate()` have **no accessor method at all**. The library never generates `$min`, `$max`, `$rename`, or `$currentDate`. +- MongoDB. `min()`, `max()`, `rename()`, and `currentDate()` have **no accessor method at all**. :::warning[No `min`, `max`, `rename`, or `currentDate` field operations] -The field accessor implements `set`, `unset`, `inc`, `mul`, `push`, `pull`, `addToSet`, and `pop` only. `min`, `max`, `rename`, and `currentDate` are absent from the API. Because a field accessor is a `Proxy`, `u.bio.rename` resolves to `undefined` and only throws `TypeError` when you attempt to call it. Do not use these operations; they are not supported. +The callback argument implements `set`, `unset`, `inc`, `mul`, `push`, `pull`, `addToSet`, and `pop` only. `min`, `max`, `rename`, and `currentDate` are absent from the API. A missing or misspelled operation is not a compile error: `u.bio.rename` is `undefined`, and you only get a `TypeError` when you call it. Do not use `min()`, `max()`, `rename()`, or `currentDate()`. ::: ### Array operations (MongoDB, unverified) #### Remarks -- MongoDB. `push()`, `pull()`, `addToSet()`, and `pop()` exist on the field accessor and compile, but they are **unverified** on the current test contract, which has no array (`many: true`) field to target. Applied to a non-array field, MongoDB rejects the write with a server-level error (`The field '...' must be an array but is of type ...`), not a library error. -- Treat these as available but exercise them against a real array field in your own schema before relying on them. +- MongoDB. `push()`, `pull()`, `addToSet()`, and `pop()` exist on the callback argument and compile. They are not tested; try them on a real array field before relying on them. +- Applied to a field that is not an array, MongoDB rejects the write with a server-level error (`The field '...' must be an array but is of type ...`), not a library error. An array field is declared with `[]` in your contract, as in `tags String[]`. + +```typescript +const updated = await db.orm.posts.where({ _id: '6650f1c2a1b2c3d4e5f60003' }).update((p) => [ + p.tags.push('mongodb'), // appends one element + p.tags.addToSet('mongodb'), // appends it only if it is not already there + p.tags.pull('draft'), // removes every element equal to the argument + p.tags.pop(1), // removes the last element; pop(-1) removes the first +]); +``` ## Result types @@ -1880,30 +2044,38 @@ Use `Shape` to declare a function's return type once and let the compiler check ### `AsyncIterableResult` -The methods `all()` / `createAll()` / `updateAll()` / `deleteAll()` return an `AsyncIterableResult`, imported (if you need the type) from `@prisma/orm-postgres/components/runtime`. It has a dual interface: +The methods `all()` / `createAll()` / `updateAll()` / `deleteAll()` return an `AsyncIterableResult`. You can use it two ways: -- **Buffered**: `await` the result (or call `.toArray()`) to collect every row into an array. -- **Streaming**: use `for await ... of` to iterate rows one at a time. +- `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. + +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 -A result is consumed once per mode: +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. +- Switching from one way to the other throws an error whose `code` is `RUNTIME.ITERATOR_CONSUMED` (message: `already been consumed`). Awaiting a result and then looping it with `for await`, or the reverse, is what throws. -- Re-`await`ing (or calling `.toArray()` on) an already-buffered result is **safe**: it returns the cached array, with no re-query and no throw. -- **Switching modes** on a consumed result throws `RUNTIME.ITERATOR_CONSUMED` (message: `already been consumed`). Awaiting a result and then iterating it with `for await` (or the reverse) is the failure mode. +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)). ```typescript const result = db.orm.public.User.all(); const first = await result; -const again = await result.toArray(); // safe: same cached array +const again = await result.toArray(); // safe: same array -// but switching mode throws: -for await (const user of result) { - // throws RUNTIME.ITERATOR_CONSUMED +try { + for await (const user of result) { + } +} catch (error) { + if (error instanceof Error && 'code' in error && error.code === 'RUNTIME.ITERATOR_CONSUMED') { + // the result was already read as an array + } } ``` -This behavior is shared by the PostgreSQL and MongoDB implementations; the error message and rule are identical. +PostgreSQL and MongoDB behave the same here. The error message and the rule are identical. ### Named model types @@ -1924,6 +2096,6 @@ These need `8.0.0-rc.10` or newer. A `contract.d.ts` emitted by an earlier relea ### Aggregate result shapes -- `aggregate((agg) => ({ ... }))` resolves to a **single object** keyed by your aliases: `{ total: 10 }`. -- `groupBy(...).aggregate((agg) => ({ ... }))` resolves to an **array**, one object per group, carrying both the group key field(s) and the aggregate aliases: `[{ customerId, orderCount, totalAmount }, ...]`. +- `aggregate((agg) => ({ ... }))` resolves to a **single object** keyed by your aliases. `db.orm.public.Order.aggregate((agg) => ({ total: agg.count() }))` resolves to `{ total: 10 }`. +- `groupBy(...).aggregate((agg) => ({ ... }))` resolves to an **array**, one object per group. Each object carries the group key field or fields plus your aliases. `db.orm.public.Order.groupBy('customerId').aggregate((agg) => ({ orderCount: agg.count(), totalAmount: agg.sum('amount') }))` resolves to `[{ customerId, orderCount, totalAmount }, ...]`. - `count()` is always a `number` (`0` over an empty set). `sum()` and `avg()` are `number | null`: they resolve to `null`, not `0`, over an empty result set. diff --git a/docs/orm-docs-audit/c21-orm-client/conventions.md b/docs/orm-docs-audit/c21-orm-client/conventions.md new file mode 100644 index 00000000000..3cc56bc1c2f --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-client/conventions.md @@ -0,0 +1,24 @@ +# Shared conventions for the C21 plain-language pass (fundamentals) + +Verified facts (rc.9 source at wip/prisma-src/). 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`. The list is at /orm/reference/raw-queries. +- 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-orm-client/page-conventions.md b/docs/orm-docs-audit/c21-orm-client/page-conventions.md new file mode 100644 index 00000000000..14a95fa9bef --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-client/page-conventions.md @@ -0,0 +1,9 @@ +# Conventions for the orm-client reference page (in addition to conventions.md) + +- This is a reference page. Keep the per-method layout (Remarks, Options, Return, Examples, PostgreSQL/MongoDB tabs). Do not add tutorial prose; one sentence and a link to the fundamentals page is the most a "how do I" question gets, unless the answer is a fact about the method (an argument, a return value, what happens on no match). +- The page is edited in four slices by four agents at once. Edit only your slice file. Do not renumber or rename headings; if you must rename a heading, pin the old anchor with `[#old-anchor]`. +- Terms defined once, in part 1 (Example schema / Setting up the client), and used bare elsewhere: contract ("your contract, the `contract.prisma` file that replaced `schema.prisma`; `npx prisma contract emit` writes `contract.json` and `contract.d.ts` from it"); `db.orm.public.User` (models by model name; `public` is the PostgreSQL schema); `db.orm.users` (on MongoDB the accessor is the collection name from `@@map`, or the model name with a lowercase first letter); variant ("a model declared with `@@base`, stored with its base model; `variant("Bug")` narrows a collection to that model; the name is the model name"). Parts 2 to 4 may say "see [Example schema](#example-schema)" but must not redefine these. +- Banned on this page in addition to the list: terminal, facet, root name(s), roots map, aggregate map, emitted contract, type-only guard, consumption mode, child-owned/parent-owned (say "the side that holds the foreign key"), filter surface, relation refinement (say "relation filter"), AST node, textual-trait, adapter-registered, "unverified on the current test contract" (say "not tested; try it on a real field before relying on it", or state the fact if the source shows it), "vacuously true", materializing, `float8` as a JavaScript type (say `number`), `$set`/`$setOnInsert`/`$lookup`/`RETURNING`/`LIMIT 1` when the sentence is about what the reader gets rather than how to write a query. +- Every example's ids: declare them once at the top of the Examples block of the section (`const aliceId = "..."`) or replace with a literal. A page that claims examples are transcribed from tests must have examples that run as printed. +- Error codes: "throws an error whose `code` is `X`". Give one string, not two. +- Facts the readers asked for that you must look up in the rc.9 source before writing (do not guess; mark `Q` if not found): whether `create` requires an explicit id or nullable fields; what `conflictOn` takes and whether the value must match `create`; whether PostgreSQL `upsert` needs `where()`; whether `.eq()` on an enum takes the member name or the mapped value; whether `variant()` takes the model name or the discriminator value; whether `some()` takes an object as well as a callback; whether `MongoOrExpr` has `.and()`; `groupBy` with several fields; `orderBy`/`limit` on a grouped collection; `having` by alias; `in([])` behaviour; the full list of MongoDB field operations and the signatures of `push`/`pull`/`addToSet`/`pop`; whether nested `create`/`connect` work on MongoDB; whether `update()` returning `null` can also mean a wrong argument shape (if so, say so plainly); native vs text-backed enums (define both in one sentence each, with the contract syntax). diff --git a/docs/orm-docs-audit/c21-orm-client/part1.round1.md b/docs/orm-docs-audit/c21-orm-client/part1.round1.md new file mode 100644 index 00000000000..c206db6691b --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-client/part1.round1.md @@ -0,0 +1,86 @@ +Read once, top to bottom. Here's what stopped me. + +## Sentences I could not restate after one reading + +1. **Line 186:** "Models hang off `db.orm.public` and use the contract's **root names**, which match your Prisma Schema Language (PSL) model names" + Stopped by: "the contract's root names." I don't know what a contract is, what a root name is, or why an extra concept is needed to say "the name matches your model." Plainer: "Models hang off `db.orm.public` under their model names from the schema: `db.orm.public.User`." + +2. **Line 204:** "use the contract's **root names too, but those are the lowercase plural collection names** from the contract's `roots` map, not the PSL model names" + Stopped by: three ideas at once — the same term meaning something different, a `roots` map I've never seen, and a negation. Also it contradicts itself in feel: if "root name" is defined as "matches the PSL model name" one paragraph earlier, then here root names don't match model names. Plainer: "On MongoDB, models hang off `db.orm` under the collection name you mapped with `@@map`: `db.orm.users`." + +3. **Line 382:** "Which fields each of those functions accepts comes from the aggregate map in your emitted contract, not from a fixed rule." + Stopped by: "aggregate map," "emitted contract." I cannot look at my schema and predict what will compile. Plainer: "`sum()` and `avg()` accept numeric columns. `min()` and `max()` also accept text and date/time columns. Anything else is a compile error." + +4. **Line 383:** "`sum()` over an integer column returns `number` and raises `RUNTIME.DECODE_FAILED` outside ±(2^53 − 1); `avg()` over an integer column returns a `float8` `number`; `sumBigInt(field)` and `avgDecimal(field)` are the lossless forms." + Stopped by: four separate facts in one sentence, plus `RUNTIME.DECODE_FAILED` (an error code I have no context for — is it thrown? a rejected promise? what do I catch?) and `float8` (a Postgres type name used to describe a JavaScript value). I'd split this into a short list. + +5. **Line 334:** "passing a second argument to `include()` does **not** throw. JavaScript does not arity-check, so the extra argument is silently ignored and a plain, unrefined `$lookup` runs. This differs from calling a genuinely absent method, which throws `TypeError`." + Stopped by: this is a paragraph about JavaScript's calling convention, not about the ORM. I read it twice trying to find the instruction. The only fact I need: "On MongoDB `include()` takes no second argument; if you pass one it is ignored." + +6. **Line 478:** "On PostgreSQL, **native** enum columns sort in the enum's declaration order... A text-backed enum sorts by its stored value instead" + Stopped by: "native enum" vs "text-backed enum." The page never defines either, and both enums in the example schema carry `@@type("pg/text@1")`, so I have no example of the native kind and no way to tell which one I have. This directly changes my sort results and I cannot act on it. + +7. **Line 466 (note):** "columns stored as date/time strings or backed by `Temporal`. So `min('createdAt')` over a `DateTime` column type-checks and returns a `Temporal.Instant`." + Stopped by: `Temporal` appears with no introduction. Is that the TC39 `Temporal` global? Do I need a polyfill? What does `Temporal.Instant` support? This is a return type I have to write code against. + +8. **Line 466, last clause:** "those calls are a compile error, naming the operation that is unavailable for that input." + Stopped by: "naming the operation that is unavailable for that input" — I think it means the TypeScript error message mentions `SUM`, but I had to guess. + +9. **Line 588:** "`cursor()` is typed to require a preceding `orderBy()`. This is a **type-only** guard: at runtime there is no such check." + Stopped by: "type-only guard" is invented phrasing, and the whole point is an implementation detail. What I need: "Always call `orderBy()` before `cursor()`. Without it the cursor is ignored and you get every row." + +10. **Line 200:** "To attach domain methods to a model, register a custom `Collection` subclass when you build the client. That path uses the `orm(...)` factory instead of the client's built-in facet" + Stopped by: "domain methods," "facet," "the `orm(...)` factory" — three unexplained things and no code. "Facet" is used twice on the page (line 182 too) and never defined. + +## Words and phrases I had to guess + +- **"contract"** / **`contract.json`** / **`./contract.d`** — I guessed this is Prisma 8's replacement for the generated client, produced by something like `prisma generate`. The page never says. This is the single biggest gap: it appears in the first code block and is never explained or linked. +- **"facet"** (line 182, 200) — guessed: a namespace on the client object, i.e. `db.orm` vs presumably `db.something-else`. +- **"root names"** / **"`roots` map"** — guessed: the keys the client uses to expose each model. +- **"extension pack"** (line 198) — guessed: an npm package adding database-specific types. +- **"read method (#read-terminals)"** (line 222) — "terminals" guessed as "methods that end the chain and execute." +- **"reducer"** (line 341, in the `refineFn` type) — guessed: one of `count`/`sum`/`avg`/`min`/`max`. +- **"polymorphic (PostgreSQL) or discriminated (MongoDB) model"** (line 677) — guessed these both mean the `@@base` / `@@discriminator` thing in the schema. Neither is defined, and `@@base`, `@@discriminator`, `types { ... }`, `@@type("pg/text@1")`, and the `Uuid` type all appear in the example schema with no explanation or link. +- **"native"** vs **"text-backed"** enum — guessed text-backed means the `@@type("pg/text@1")` annotation, native means its absence. Not stated. +- **"multi-table inheritance (a base table plus a variant table)"** (line 682) — guessed, but then the sentence says it "affects some mutations" and points at two sections not on this page, so I still don't know what it affects. + +## Places I asked "so what do I actually type?" and got nothing + +- **Where do `contract.json` and `contract.d` come from?** No command, no link. I cannot run the very first example. +- **The pgvector example (line 198)** describes the option in prose — "pass the pack when you create the client," "needs `extensions: [pgvector]`" — but shows no code. Every other setup step gets a code block; this one doesn't, and it's the one with an unfamiliar import path. +- **What do I pass to `.eq()` for an enum?** Line 254 uses `u.kind.eq('admin')` (a member name of `user_type`) and line 271 uses `p.priority.eq('urgent')` (the *mapped value* of the `Urgent` member). Both enums are declared the same way. I cannot tell which form is correct or why they differ. +- **`variant('Bug')` and `variant('Tutorial')`** both use the capitalized PSL model name, even though every other MongoDB accessor on the page is lowercase plural. Is the variant name the model name, or the discriminator string (`"bug"`, `"tutorial"`)? The schema shows both, the page doesn't say which one `variant()` takes. +- **`combine()`** (line 394, 454): "Object of sub-views and aggregates." In the example, `posts.combine({ recent: posts.orderBy(...), total: posts.count() })` references `posts` from inside its own callback. I don't know whether that's required, whether the keys are free-form, or what shapes are legal values. +- **Every id in every example is undefined:** `aliceId`, `carolId`, `acmeId`, `postId`, `emptyCustomerId`. None is ever assigned. I can't copy any example and run it. +- **`select()` on MongoDB** — Remarks say the return type isn't narrowed. So what do I write to get a typed result? Cast it myself? Not said. +- **`where()` callback fields on PostgreSQL** — what operators exist besides `.eq()`? The page says "column-level operators" and defers to "[Filter conditions and operators]," a section not on this page. Same for `MongoFieldFilter` (`.eq`, `.gte` shown; the rest deferred). +- **`Customer` / `Order`** are used in three examples (lines 431–448) and defined nowhere in this file, only forward-referenced to "Grouped aggregates." +- **`countBigInt()`** is named in the Remarks (line 383) but has no row in the Options table and no example. +- **`PostTag`** exists in the example schema with explicit `postId`/`tagId`, while `Post.tags Tag[]` and `Tag.posts Post[]` look implicit. I can't tell whether the join model is required or vestigial, and it's never used in an example. + +## Places explaining how it works inside when I only wanted what to do + +- Line 334, the JavaScript arity paragraph. Pure internals. +- Lines 588 and 651, "type-only guard, not a runtime check." I only need "always call `orderBy()` first." +- Line 382, "comes from the aggregate map in your emitted contract, not from a fixed rule." Tells me about the code generator, not about what compiles. +- Line 332, "`include(relationName)` adds a `$lookup`." I don't need the Mongo aggregation stage name to load a relation. +- Line 682, "variants use multi-table inheritance (a base table plus a variant table)." Schema internals in a method reference. +- Line 383, `float8` and `RUNTIME.DECODE_FAILED`. The engine's type name and error code, where I wanted "use `sumBigInt()` for large sums." +- Line 297, "narrows the projected fields at runtime, but does not strip fields from the returned row type at compile time" — the fact is important, but it's phrased as a compiler/runtime split rather than "on MongoDB the result type still lists every field, even though only the selected ones have values." + +## Could I look up a method and use it correctly after one reading? + +For the plain chainers — `where`, `orderBy`, `limit`, `offset`, `distinct` — yes, once I already have a working client. The Remarks/Options/Return/Examples layout is easy to scan and the Postgres/Mongo tabs are genuinely useful. + +But I could not get to a working client at all, because the page opens with `contract.json` and `contract.d` and never says where they come from. That blocks everything. + +Beyond that, still unknown after one reading: +- What a contract is, and therefore what "root names," "the `roots` map," and "the aggregate map" mean — three of the page's load-carrying facts rest on a term it never defines. +- Whether my enums are native or text-backed, so I can't predict `orderBy` results. +- Whether `.eq()` takes an enum member name or its mapped string. +- Whether `variant()` takes the model name or the discriminator value. +- What operators exist on either database — both lists are deferred off-page. +- What `Temporal.Instant` is or whether I need a polyfill before using `min('createdAt')`. +- How to write a `combine()` shape from scratch, rather than modifying the one example. + +One more thing: the page promises at line 17 that "every example is transcribed from an executable test suite that runs against a live database," but no example is runnable as printed, because none of the id variables exist. That claim raised my expectations and then didn't hold. diff --git a/docs/orm-docs-audit/c21-orm-client/part2.round1.md b/docs/orm-docs-audit/c21-orm-client/part2.round1.md new file mode 100644 index 00000000000..e6c3c9c0dcb --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-client/part2.round1.md @@ -0,0 +1,101 @@ +Read it once, top to bottom, as a Prisma 7 user looking something up. Report below. + +## Sentences I could not restate after one reading + +**1.** "On **PostgreSQL** only `update()` and `delete()` are typed to require a prior `where()`, and even there it is a **type-only** guard with no runtime check. If you bypass the type system, `update()` and `delete()` do not throw and do not mass-mutate; they narrow to a single row by identity (a `SELECT ... LIMIT 1` over the current, possibly empty, filters, then act on that one row)." + +What stopped me: four ideas jammed together, and "bypass the type system" is never explained (do I mean `as any`? plain JavaScript? a dynamic call?). "narrow to a single row by identity" is an invented phrase — identity of what? And "possibly empty filters" contradicts "narrow to a single row." Plainest wording I'd have understood: "On PostgreSQL, TypeScript will refuse to compile `update()` or `delete()` without `where()`, but nothing checks at runtime. If you call them from JavaScript with no filter, they pick one arbitrary row and change or delete that row." + +**2.** "**Switching consumption mode** on a consumed result throws `RUNTIME.ITERATOR_CONSUMED` (`already been consumed`)." + +"Consumption mode" is not defined anywhere on this part of the page. I *guess* it means: you awaited it, now you can't `for await` it, or vice versa. The page should just say that. Also two different strings are given — `RUNTIME.ITERATOR_CONSUMED` and `already been consumed` — and I don't know which one I'd see in a log or which one to match on. + +**3.** "On PostgreSQL, `create()` supports nested `create()` on a child-owned relation and nested `connect()` on a parent-owned relation, all within one transaction." + +"Child-owned relation" and "parent-owned relation" appear here for the first time with no definition. I tried to reverse-engineer them from the two examples: `User.create({ posts: ... })` is labeled child-owned, `Post.create({ user: ... })` is labeled parent-owned. So I *think* it means "which table holds the foreign key," but I'm guessing, and I can't tell from a schema I'm looking at which of my own relations is which. Plainest wording: "You can nest `create()` on a one-to-many side, and `connect()` on the side that holds the foreign key." + +**4.** "These nested mutators are type-checked but their runtime behavior on MongoDB is unverified on the current test contract." + +"Unverified on the current test contract" tells me about your test suite, not about my code. Does it work on MongoDB or not? Should I use it? The sentence leaves me with no decision. Say either "not supported on MongoDB" or "supported on MongoDB but untested — we recommend not relying on it." + +**5.** "On PostgreSQL, `update()` accepts a data object only. It has **no** field-operations callback overload: passing a function is silently a no-op (it resolves to `null`), not an error. A bare function has no enumerable own properties, so no column is targeted." + +The last sentence explains your implementation to justify a bug. I don't need to know about enumerable own properties. Also this contradicts the doc line above it: `update()` "Returns `null` when no row matches" — so if I get `null` back I cannot tell whether nothing matched or I passed the wrong shape. + +**6.** "On MongoDB, `updateAll()` is **not atomic**. It (1) reads the matching `_id`s, (2) runs an update against the original filter, then (3) re-reads by those captured `_id`s. A concurrent write between these steps could change which documents match or their values; the result reflects the `_id` set from step 1, not one atomic snapshot." + +I can follow the mechanics, but I cannot restate what it means for me. What is the bad outcome? Can a document be updated and not returned? Returned but not updated? Returned with someone else's newer values? And what should I do instead — a transaction? The page doesn't say. + +**7.** "The base and variant rows live in separate tables, so a single `RETURNING`-less `INSERT` can't populate both." + +Internals. I wanted to know "use `createAll()` instead", which the error message already says. + +**8.** "On insert, `update` fields apply via `$set` and the remaining create-only fields via `$setOnInsert`, so `update` values win over `create` values on any overlapping field." + +The `$set`/`$setOnInsert` detail is MongoDB-driver internals. The only part I needed is the clause after "so". Plainest wording: "If a field appears in both `create` and `update`, the `update` value is used, even on the insert path." + +**9.** "Registering custom collections requires building the client with the `orm(...)` factory, not the `postgres()` client's built-in `.orm` facet. The `postgres()` client has no `collections` option, so its facet always resolves models to the base `Collection`." + +"Facet" is undefined. "Resolves models to the base `Collection`" is implementation language. I think it means: "`db.orm` from `postgres()` can't use your subclasses. Build the client with `orm()` instead." That's all I needed. + +## Words and phrases I had to guess at + +- **"terminals"** — in the anchors `#read-terminals` and `#mutation-terminals`. Guessed: methods that end the chain and actually hit the database. The visible headings say "Read methods"/"Write methods", so the anchors leak a word the page otherwise avoided. +- **`AsyncIterableResult`** — guessed: a thing that is both a promise and an async iterator. The link goes to a section not in what I read, so I can't confirm. +- **`.toArray()`** — appears once, in a Remarks parenthetical for `all()`. Never listed in any return-type table, never shown in an example. Guessed it's an alternative to `await`. +- **"already-buffered"** — guessed: you already awaited it, so the rows are in memory. +- **`variant('Bug')`, `variant('Tutorial')`** — appears in six code samples and is never explained in this part. Guessed: filters to a subclass/subtype of the model. +- **"multi-table-inheritance variant" / "MTI"** — guessed: a subtype stored in its own table with a shared base table. The abbreviation MTI is used in the quoted error message without ever being spelled out next to it. +- **"discriminated variant"** — guessed: same idea, but stored as one document with a type field. +- **"materializing"** — in "Insert rows without materializing them". Guessed: without reading the inserted rows back. +- **"field-operations callback"** — guessed from the examples (`t.duration.inc(5)`). No list of available operations here; `inc`, `set`, `mul` appear in samples and that's all I know exists. +- **"conflict target"** — guessed: the unique index Postgres uses to decide insert vs update. +- **"contract"** — `Contract`, `contract.json`, `contract.d`, "test contract". Guessed: the generated schema types. +- **"facet"** — guessed: the `.orm` property on the client. +- **"atomic"** used three different ways: "atomic single-document update", "is a single `UPDATE ... RETURNING` statement, and is atomic", "not one atomic snapshot". I can't tell if these mean the same guarantee. + +## "So what do I actually type?" — places the page doesn't say + +1. **Do I have to supply `id` myself?** `Tag.create({ label: 'typescript-2' })` has no id. `User.create({ id: '00000000-...-099', ... })` has one. `Tag.upsert({ create: { id: '30000000-...', label } })` has one, and the ORM 7 migration diff quietly adds `id` to the new version (`create: { id, label: 'typescript' }`) with no note. I don't know when ids are generated for me. +2. **Do I have to pass every nullable field?** Every MongoDB create sample spells out `bio: null` and `address: null`. Can I omit them? The page never says. +3. **`conflictOn: { label: 'brand-new' }`** — why does the conflict target carry a *value*? In SQL, `ON CONFLICT` names columns. If it's just the column name, why not `conflictOn: ['label']`? If the value matters, what does it do? And does it have to match the `create` value? Both samples make it match, without saying whether that's required. +4. **`first()` on PostgreSQL with both `where()` and an inline filter** — do they AND together, or does the inline one replace the earlier filter? Never stated. +5. **PostgreSQL `upsert()` and `where()`** — the section warning says "Always call `where()` before ... or `upsert`", but the `upsert()` Remarks say "Requires a prior `where()` on **MongoDB**", and the PostgreSQL example calls `upsert()` with no `where()` at all. Three statements, and I can't tell what to type on PostgreSQL. +6. **Comparing a MongoDB `_id` after upsert** — "Compare it with `String(...)`." Show the line. Is it `String(user._id) === String(otherId)`? I'd like to copy it, not reconstruct it. +7. **What to do about non-atomic `updateAll()` on MongoDB.** No alternative is offered. +8. **How to get a count without writing.** The read methods are `all()`, `first()`, `aggregate()`, `groupBy()`. There's no `count()` or `exists()` in the list. Did they go away? For a `findMany`+`length` user, that's an unanswered question. +9. **`bugs()` returns what?** In the subclass example, `this.variant('Bug')` is returned and then `.all()` is called on it. The return type is never named, so I don't know what I can chain onto my own domain methods. +10. **`deleteAll()` returns the deleted rows as a stream.** If I `for await` over it, are rows deleted as I iterate or all up front? Matters if I abandon the loop halfway. +11. **The elided error strings** — `... Use createAll() instead.` and `upsert() is not supported for MTI variant "" ...`. If I want to catch and match these, the `...` makes that impossible. +12. **`orm({ runtime, context, collections }).public`** — the `.public` at the end appears without comment, and `client.context` is never explained. + +## Places the page explains internals when I only wanted instructions + +- `SELECT ... LIMIT 1` in the warning block. +- "PostgreSQL's `RETURNING`-backed `create()`". +- "A bare function has no enumerable own properties, so no column is targeted." +- The three-step read/update/re-read breakdown of MongoDB `updateAll()`. +- "$set" / "$setOnInsert" in `upsert()`. +- "backed by `findOneAndUpdate`" / "backed by `findOneAndDelete`" (harmless, but it's implementation, and it is where the atomicity claim hides). +- "a single `RETURNING`-less `INSERT` can't populate both". +- "its facet always resolves models to the base `Collection`". +- "The callback groups operators into one `findOneAndUpdate`". + +## Could I look up a method and use it correctly after one reading? + +For the simple reads, yes. `all()` and `first()` are clear, the ORM 7 diffs are the most useful thing on the page, and I could write those calls immediately. + +For the writes, no. Specifically, after one reading I still would not know: + +- Whether I need to pass `id` on create, ever. +- What `conflictOn` actually takes, or whether PostgreSQL `upsert()` needs `where()` — the page says both. +- What `variant()` is, despite it appearing in six samples I'd be copying. +- Whether nested `create`/`connect` works on MongoDB. +- What the full set of field-update operations is (I've seen `inc`, `set`, `mul`, and have no idea if that's all). +- Whether `null` from a PostgreSQL `update()` means "no match" or "you passed the wrong argument shape". +- What the MongoDB `updateAll()` non-atomicity costs me in practice, or what to do instead. +- Whether a count-only read exists. + +The single biggest problem is the warning block at the top of the write section. It is the most important paragraph on the page — it's about accidentally wiping a table — and it is the hardest one to read. It packs seven method names, two databases, a type-versus-runtime distinction, and a SQL implementation detail into one block, and I would have to read it three times to act on it. The one-line version I needed is: "Always call `where()` before an update or delete. On MongoDB you get an error if you forget. On PostgreSQL you do not, and `updateAll`/`deleteAll` will hit every row." + +Path read: `/private/tmp/claude-502/-Users-will-Projects-prisma-web--claude-worktrees-prisma-orm-8-docs-audit-5bdfd9/7636eb55-44f8-408f-b81e-481bd0140ea6/scratchpad/c21/oc/part2.mdx` diff --git a/docs/orm-docs-audit/c21-orm-client/part3.round1.md b/docs/orm-docs-audit/c21-orm-client/part3.round1.md new file mode 100644 index 00000000000..2020a74e98d --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-client/part3.round1.md @@ -0,0 +1,82 @@ +Read once, top to bottom, as a Prisma 7 user looking something up. + +## Sentences I could not restate after one reading + +1. **"It is a Postgres-adapter-registered operation attached to any textual-trait field, not a core comparison method."** (line 219) + Stopped by "Postgres-adapter-registered operation", "textual-trait field", and "core comparison method" — three invented-sounding terms in one sentence, none defined anywhere on this part of the page. I cannot tell whether this changes anything I type. Plainest version I'd have understood: "`ilike()` is available on text fields when you use the PostgreSQL adapter." And then say whether that's the same condition as `like()` or a different one, because right now `like` is described as being "on text fields" and `ilike` on "textual-trait field" — two different phrases for what I have to assume is the same set. + +2. **"They build AST nodes directly and are independent of any model accessor."** (line 268) + "AST nodes" is compiler vocabulary I do not need to read a filter. "Independent of any model accessor" — I cannot resolve "model accessor"; the term is not introduced. What I think this is trying to tell me is: "You import them; they are not methods on a field." That's already said by "standalone functions imported from...", so the sentence adds nothing I can act on. + +3. **"A to-one relation is filtered from the child side with the same `some(...)` shape (the compiled SQL is a correlated subquery regardless of cardinality)."** (line 311) + Too many ideas, and the parenthesis is about SQL generation, not about me. "From the child side" took a second read — I think it means: when the model I'm querying has one related parent (Post → User), I still use `some()`. Plainest version: "Use `some()` for to-one relations too — `post.user.some(...)`." Also: the example that follows passes an **object** `some({ email: 'alice@example.com' })`, while every other `some()` example passes a **callback**. The page never says both forms work. That is the single thing I most wanted stated plainly. + +4. **"`every()` is vacuously true for a parent with zero related rows."** (line 310) + I know what "vacuously true" means, but the sentence makes me translate. Plainest: "A user with no posts matches `every()`." + +5. **"On PostgreSQL, `all()` takes no arguments and returns a constant-true predicate, useful wherever a predicate is structurally required."** (line 270) + "Structurally required" is doing a lot of work and I can't cash it out. Required where? The page gives no example of `all()` at all. Worse, `.all()` is used on nearly every line of this page as the method that runs the query and returns an array — so the same name means two unrelated things and the page doesn't acknowledge the collision. + +6. **"PostgreSQL and MongoDB use different filter surfaces."** (line 206) + "Filter surfaces" — guessed it means "different APIs for filtering". Say that. + +7. **"Filters build the predicate inside `where()` (and relation refinements)."** (line 206) + "Relation refinements" is not defined on this part of the page and I can't map it to anything below — the relation section calls them "relation filters". If they're the same thing, use one name. + +8. **"MongoDB. Runtime behavior is verified; the type surface does not declare embedded paths, so the object-shorthand form needs a cast."** (line 402) + "Runtime behavior is verified" reads like a note from your test suite to yourselves. "Type surface" — guessed it means the TypeScript types. Plainest: "This works at runtime, but the TypeScript types don't know about nested paths, so the object form needs a cast." + +9. **"each returning comparison methods (`eq`, `neq`, ...)"** (line 172) + "Returning comparison methods" — an object with those methods, I assume, not literally returning methods. The example makes it clear; the sentence doesn't. + +## Words/phrases I had to guess + +- **`db.orm.public.Order`** — guessed `public` is the Postgres schema name. Never stated in this part. +- **`db.orm.users`** (lowercase, no `public`) for MongoDB — guessed the Mongo path is `db.orm.` with no schema level, and that the casing difference (`users` vs `User`) is intentional. Pure guess. +- **"model accessor"**, **"field accessor"** — guessed both mean the `u` in `where((u) => ...)` / the `db.orm.public.User` object. They may not mean the same thing. +- **"textual-trait field"** — guessed "a string column". +- **`GroupedCollection`** — guessed it's a lazy builder, not a result, because you must call `.aggregate()`. +- **`h.fn(field).cmp(value)`** in the `having()` Options table — guessed `fn` and `cmp` are placeholders, not real names. If a reader copies that line it does not compile. +- **`MongoFieldFilter.of`** — listed as one of the eleven factories and then never mentioned again. I have no guess. What does it do and what do I pass it? +- **`agg`, `h`** callback params — fine, but nothing tells me their types if I want to extract a helper function. +- **"Each key must support equality"** (shorthand object) — guessed "don't put a relation or a JSON column in there". Which fields fail? Not said. + +## "So what do I actually type?" — unanswered + +- **`MongoFieldFilter.of(...)`** — named as existing, zero signature, zero example. +- **`MongoExistsExpr`** — "There is a separate `MongoExistsExpr` class for `$exists`" (line 360). No import path, no method names, no example. I now know it exists and still can't use it. +- **`all()` (the combinator)** — no example anywhere. No signature beyond "takes no arguments". +- **`groupBy()` with more than one field** — the Options row says `...fields` / "The grouping key(s)", every example passes one. Is it `groupBy('a', 'b')` or `groupBy(['a','b'])`? The spread notation implies the former, but after coming from v7's `by: ['customerId']` I wanted it shown once. +- **Filtering rows before grouping** — `aggregate()` has a `.where(...).aggregate(...)` example. `groupBy()` never does. Can I write `.where(...).groupBy(...).having(...).aggregate(...)`? Presumably, but the page doesn't show the full chain. +- **Ordering/limiting grouped results** — "top 5 customers by spend" is the reason I'd open this section. Nothing about `orderBy` or `limit` on a `GroupedCollection`. +- **`having()` referring to an aggregate alias** — in both examples the same `sum('amount')` is written twice, once in `having` and once in `aggregate`. Can I reference the alias instead? Not said, so I'd assume no and keep duplicating. +- **Counting a specific column** — `count()` "needs no field argument". In v7 I used `_count: { bio: true }` to count non-nulls. Can I? The page implies not but never says. +- **What to use on MongoDB instead of `aggregate()`/`groupBy()`** — "calling them throws `TypeError`" and that's the end. No "drop to the driver", no link. +- **Combining Mongo combinators** — can `MongoOrExpr.of([...])` contain another `MongoOrExpr`? Can I call `.and()` on a `MongoOrExpr`, or only on a `MongoFieldFilter`? The remark says `.and()`/`.not()` are "instance methods on any `MongoFieldFilter`", which reads like `MongoOrExpr` does *not* have them. An AND-of-ORs is the most ordinary filter there is and I can't tell how to write it. +- **`not()` on Postgres combined with relation filters** — `not(p.posts.some(...))` vs `none(...)`: both plausible, neither confirmed. +- **The Mongo dot-notation object form mixing keys** — `{ 'address.city': 'SF' } as unknown as Record`. If I cast the whole object, do my normal typed keys still get checked? Obviously not. So why would anyone use this form? The page shows it without saying "prefer `MongoFieldFilter.eq`" as a recommendation, even though its own remark implies exactly that. +- **`in()` with an empty array** — matches nothing, or throws? Unstated, and it's the classic bug. +- **`like()` escaping** — `%` and `_` in user input. Nothing. + +## Where the page explains internals when I wanted instructions + +- "They build **AST nodes** directly" (line 268). +- "It compiles to a SQL `HAVING` clause" (line 172) — harmless but not actionable. +- "compile to **correlated `EXISTS` / `NOT EXISTS` subqueries**" and "the compiled SQL is a correlated subquery **regardless of cardinality**" (lines 308, 311). The second one is pure implementation trivia inside a sentence that was supposed to tell me what to type. +- "It is a **Postgres-adapter-registered operation**" (line 219). +- "At runtime there is no such restriction: **the object key is used verbatim as the Mongo field path**" (line 403). The "verbatim" mechanism is fine as a one-liner, but it's explaining why the cast works rather than telling me which form to use. +- "**`MongoFieldFilter.ne` is `undefined`**" and "**Accessing `.or` is `undefined`** and calling it throws `TypeError`" (lines 358, 299). Telling me the property is `undefined` is describing your object's shape. "There is no `ne` — it's `neq`" is the whole message. +- "There is **no `$nor` combinator anywhere in the library**" (line 299) — repeated in the remark and the note box, and it's a statement about your codebase's contents. + +## Could I look up a method and use it correctly after one reading? + +Partly. The Postgres scalar comparisons and the flat `aggregate()` I could use immediately — the tables are good, the examples are complete and runnable, and the v7 diffs are the most useful thing on the page. + +What I still couldn't do: + +- Write any grouped query more complex than the two examples: no multi-field grouping, no filter-then-group, no sorting the groups, no reusing an alias in `having`. +- Write an AND-of-ORs on MongoDB. I don't know if `MongoOrExpr` has `.and()`. +- Use `MongoFieldFilter.of` or `MongoExistsExpr` at all, despite both being named. +- Use the combinator `all()`, and I'd probably misread `.all()` at the end of a chain as the same function the first time I hit it in someone else's code. +- Decide confidently whether `some()` takes a callback or an object, because the to-one example silently switches forms. +- Know what `public` and the lowercase Mongo collection names in `db.orm....` are. That may be in the omitted sections, but every code sample on this part depends on it. diff --git a/docs/orm-docs-audit/c21-orm-client/part4.round1.md b/docs/orm-docs-audit/c21-orm-client/part4.round1.md new file mode 100644 index 00000000000..9292df72760 --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-client/part4.round1.md @@ -0,0 +1,65 @@ +Read once, top to bottom, as a Prisma 7 user who has never seen ORM 8. + +## Sentences I could not restate after one reading + +**1.** Line 20: *"Each operation targets a field and produces a Mongo update operator."* +Stopped by: this tells me what happens inside the library, not what I get back. Does `u.bio.set('x')` return something I have to do anything with? I think the answer is "no, just put it in the array" — but the sentence made me stop and wonder. Plainer: delete it, or say "each call returns an update instruction; collect them in the array you return from the callback." + +**2.** Line 32: *"The field accessor is typed against the collection's **base** model, even after `variant(...)`. Accessing a variant-only field (such as `Tutorial.duration`) inside a callback is a compile error, though it works correctly at runtime because the accessor is a `Proxy` that resolves any field name."* +Three things at once, and the conclusion is missing. It tells me my code will not compile and also that it works. So what do I actually do — `@ts-expect-error`? `as any`? Give up? The page immediately shows me the exact broken case as a worked example (`t.duration.inc(10)`, lines 47-55) without a single word saying "this example does not type-check." I would have copied it, hit a red squiggle, and assumed I'd made a mistake. Plainer: "TypeScript only knows the base model's fields, so `t.duration.inc(10)` is a type error even though it runs correctly. Work around it with X." + +**3.** Line 68: *"These are library gaps, not contract limitations."* +Stopped by "contract." I do not know what a contract is here. I cannot tell what the distinction buys me as a reader — either way the method does not exist. Plainer: delete the sentence. + +**4.** Line 78: *"they are **unverified** on the current test contract, which has no array (`many: true`) field to target."* +Stopped by "current test contract." This is a fact about Prisma's internal test suite, not about my code. It also makes "unverified" sound like a property of the API rather than a property of your testing. Plainer: "We have not tested these. Try them against a real array field before relying on them." + +**5.** Line 95: *"Awaiting a result and then iterating it with `for await` (or the reverse) is the failure mode."* +"is the failure mode" is a phrase I had to re-read. It means "that is the thing that throws." Say that. + +## Words and phrases I had to guess + +- **"field accessor"** — guessed: the `u` / `t` object handed to the `update()` callback, and `u.bio` is the per-field thing. The page never defines it before using it four times, and defines it only obliquely via the Proxy remark. +- **"variant('Tutorial')"** — guessed: some subtype/discriminator of the `posts` collection. Possibly explained in an omitted section, but line 32 leans on it heavily. +- **"contract"** / **"test contract"** — guessed: your internal test schema. Still not sure. +- **"dual interface"** (line 85) — guessed: "you can use it two ways." +- **"`RUNTIME.ITERATOR_CONSUMED`"** — guessed: an error code string on a thrown error object. I do not know what property it is on, or what class the error is. +- **"`many: true`"** (line 78) — guessed: the schema syntax for an array field. Appears here with no other mention. +- **"base model"** (line 32) — guessed: the parent of a variant. + +## "So what do I actually type?" — unanswered + +- **`updateAndCount()` and `upsert()` callbacks.** Line 20 says field operations work in all three. Every example is `update()`. `upsert()` in particular — where does the callback go relative to the create branch? Not shown. +- **Why the callback returns an array.** Every example returns a one-element array, `[u.bio.unset()]`. The page never says I can put several operations in it, never shows two, and never says whether order matters. I inferred the point of the array from its shape alone. +- **What `update()` returns.** Every example writes `const updated = await ...` and then never uses `updated`. Is it the row? A count? The example names imply a row; nothing says so. +- **The workaround for the variant type error** (see above). This is the single biggest gap on the page. +- **The import for MongoDB users.** Line 85 gives `@prisma/orm-postgres/components/runtime`; line 108 says the behavior is shared by both. If I am on MongoDB, do I import `AsyncIterableResult` from the Postgres package? From a Mongo package? Not said. +- **How to catch `RUNTIME.ITERATOR_CONSUMED`.** I get an error code and a message substring. I do not get: what to `catch`, whether there is a typed error class, or whether I am meant to match on `.code`. +- **Two different client shapes, unreconciled.** Field-op examples use `db.orm.users` and `db.orm.posts`. The result-type example uses `db.orm.public.User.all()`. Same page, different accessors, different casing, no word about why. I would not know which one to type. +- **`aggregate((agg) => ({ ... }))`.** Line 112-113 tells me the result shape but never shows one real aggregate expression, so I do not know how `total`, `orderCount`, `totalAmount` were produced. I cannot write the call from this section. +- **`push`, `pull`, `addToSet`, `pop`.** Listed twice as existing, never shown once. I do not know their arguments — does `pull` take a value or a filter? Does `pop` take `1`/`-1` like Mongo, or nothing? +- **Line 71: *"Do not use these operations; they are not supported."*** The preceding sentence is about `rename`. For a second I read "these" as the eight operations in the same sentence's first clause. + +## Places explaining the internals when I only wanted to know what to do + +- Line 32 and line 71: the `Proxy` explanation, twice. I do not care that it is a Proxy. I care that (a) the type error is yours not mine, and (b) a typo'd operation fails at call time with `TypeError` instead of at compile time. Line 71 does at least land on the consequence; line 32 does not. +- Line 20: *"produces a Mongo update operator."* +- Line 68: *"The library never generates `$min`, `$max`, `$rename`, or `$currentDate`."* — I already know they do not exist from the previous sentence. +- Line 78: *"which has no array (`many: true`) field to target"* — your test suite's shape. +- Line 94: *"it returns the cached array, with no re-query and no throw."* — "no re-query" is implementation. "Safe, returns the same array" is what I need. + +## Could I look up a method and use it correctly after one reading? + +For `set`, `unset`, `inc`, `mul` on a non-variant field: yes. Those four are clear, and the missing-field behavior for `inc` and `mul` is genuinely useful and well stated. + +For everything else, no. Specifically I would still not know: + +- What to type to update a variant-only field without a compile error — and the page's own example is the broken case, unmarked. +- Which client shape is real: `db.orm.users` or `db.orm.public.User`. +- The signatures of `push`, `pull`, `addToSet`, `pop`. +- How to call `aggregate()` at all — only what comes back. +- Where a MongoDB user imports `AsyncIterableResult` from. +- How to catch the consumed-iterator error in code. +- Whether I can return more than one operation from the callback. + +The `AsyncIterableResult` section is the strongest part of the page: the rule is stated, the safe case and the throwing case are both shown in one snippet, and the `count`/`sum`/`avg` null behavior on line 114 is exactly the kind of fact I would come to a reference for. From c129ca04806b99cb086277604e55b92c74a4e417 Mon Sep 17 00:00:00 2001 From: willbot Date: Sat, 12 Sep 2026 09:04:56 +0200 Subject: [PATCH 03/11] docs: the report-extraction script the handover brief refers to Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- docs/orm-docs-audit/c21-orm-client/extract.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 docs/orm-docs-audit/c21-orm-client/extract.py diff --git a/docs/orm-docs-audit/c21-orm-client/extract.py b/docs/orm-docs-audit/c21-orm-client/extract.py new file mode 100644 index 00000000000..d91320e717e --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-client/extract.py @@ -0,0 +1,9 @@ +import sys,json +out=[] +for line in open(sys.argv[1]): + try: o=json.loads(line) + except: continue + if o.get("type")!="assistant": continue + for b in o.get("message",{}).get("content",[]): + if isinstance(b,dict) and b.get("type")=="text": out=[b["text"]] +print(out[0] if out else "") From 6ec608b9bd80b39d41617ccbbfb84427c79a8179 Mon Sep 17 00:00:00 2001 From: willbot Date: Sat, 12 Sep 2026 09:26:50 +0200 Subject: [PATCH 04/11] docs(orm8): orm-client reference, fact re-check of round one against rc.9 Four checkers read every claim in the page against prisma/orm at 8.0.0-rc.9 (f889eeb89e). Corrections: the page no longer claims its examples are copied from a test suite (no fixture in the tree has this schema); TypeScript refuses all six update and delete methods without where() on PostgreSQL; the multi-table variant messages name Bug on Task; MongoDB update input covers base-model fields only, and the upsert _id comes back as a hex string; MongoFieldFilter.of passes any operator through, so regex and elemMatch are reachable; a misspelled field operation is a compile error; the four array operations are tested; aggregates outside include() throw ORM.INCLUDE_INVALID; min and max are also null over an empty set; shorthand null on MongoDB also matches a missing field; ORM.FILTER_UNSUPPORTED is PostgreSQL only; sum and avg accept Time, not timetz; Temporal note now gives the polyfill import; cursor() throws ORM.CURSOR_VALUE_MISSING. Reports are in docs/orm-docs-audit/c21-orm-client/factcheck1.part*.md, to be removed before merge. Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- .../content/docs/orm/reference/orm-client.mdx | 119 +++++++++--------- .../c21-orm-client/factcheck1.partA.md | 9 ++ .../c21-orm-client/factcheck1.partB.md | 9 ++ .../c21-orm-client/factcheck1.partC.md | 7 ++ .../c21-orm-client/factcheck1.partD.md | 7 ++ 5 files changed, 92 insertions(+), 59 deletions(-) create mode 100644 docs/orm-docs-audit/c21-orm-client/factcheck1.partA.md create mode 100644 docs/orm-docs-audit/c21-orm-client/factcheck1.partB.md create mode 100644 docs/orm-docs-audit/c21-orm-client/factcheck1.partC.md create mode 100644 docs/orm-docs-audit/c21-orm-client/factcheck1.partD.md diff --git a/apps/docs/content/docs/orm/reference/orm-client.mdx b/apps/docs/content/docs/orm/reference/orm-client.mdx index ef9b1bdd373..45357ee4bcb 100644 --- a/apps/docs/content/docs/orm/reference/orm-client.mdx +++ b/apps/docs/content/docs/orm/reference/orm-client.mdx @@ -14,7 +14,7 @@ For task-oriented walkthroughs, see the Fundamentals guides: [Reading data](/orm ## Example schema -All examples on this page run against the schema below. Every example is copied from a test suite that runs against a live database. The grouped-aggregate examples use a separate `Customer` / `Order` schema, shown under [Grouped aggregates](#grouped-aggregates). +All examples on this page run against the schema below. The grouped-aggregate examples use a separate `Customer` / `Order` schema, shown under [Grouped aggregates](#grouped-aggregates). You write this schema in your contract, the `contract.prisma` file that replaced `schema.prisma`. After every change to it, run `npx prisma contract emit`. That command writes `contract.json` and `contract.d.ts`, the two files the examples below import. @@ -182,6 +182,7 @@ model Tutorial { Some parts of the schema the examples rely on: - `@@map("user")` sets the table or collection name. Without it, the table name is the model name with a lowercase first letter. +- On MongoDB, `id ObjectId @id @map("_id")` renames the field to `_id` everywhere: you filter on `_id`, and the returned document has an `_id` key. That is why the MongoDB examples never say `id`. - `Uuid` is the type for a PostgreSQL `uuid` column. `@default(uuid())` fills the id in when you insert a row. - `@@type("pg/text@1")` on an `enum` block stores the enum as a text column, with a check constraint that allows only the declared values. This page calls that a text-backed enum. - A `native_enum` block is the other kind. It creates a real PostgreSQL enum type with `CREATE TYPE ... AS ENUM`. This page calls that a native enum. The two sort differently; see [`orderBy()`](#orderby). @@ -246,7 +247,7 @@ These methods narrow a query. Each returns a collection you can chain more metho 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). -The examples below use these ids, which stand for rows the test suite inserted first: +The examples below use these ids, which stand for rows inserted before the query runs: ```typescript const aliceId = '00000000-0000-4000-8000-000000000001'; @@ -347,7 +348,7 @@ Project a row down to a subset of scalar fields. ```typescript tab="PostgreSQL" const summaries = await db.orm.public.User.select('id', 'email').orderBy((u) => u.email.asc()).all(); -// summaries[0] is { id, email } — no displayName +// summaries[0] is { id, email }, with no displayName ``` ```typescript tab="MongoDB" @@ -363,8 +364,8 @@ Eagerly load a relation onto the returned rows. - Available for PostgreSQL and MongoDB, with differences noted below. - On PostgreSQL, `include(relationName, refineFn?)` loads both to-one and to-many relations. The optional callback receives the related rows as a collection, so you can filter, order, limit, and aggregate them. See [Refinements, aggregates, and combine](#refinements-aggregates-and-combine). - On MongoDB, `include(relationName)` loads a relation that is stored as a reference. It takes the relation name and nothing else: there is no callback. -- On MongoDB, the `_id` of a loaded related document comes back as a driver `ObjectId`, not as the hex string you get for a top-level `_id`. Wrap it in `String(...)` before you compare it. -- On MongoDB, a second argument to `include()` is ignored rather than rejected, and the relation loads unfiltered. +- On MongoDB, the `_id` of a loaded related document comes back as a driver `ObjectId`, not as the hex string a document's own id field comes back as. Wrap it in `String(...)` before you compare it. +- On MongoDB, TypeScript rejects a second argument to `include()`. If you get past the type system, the extra argument is ignored and the relation loads unfiltered. #### Options @@ -411,13 +412,13 @@ On PostgreSQL, the callback you pass to `include()` receives the related rows as #### Remarks - PostgreSQL only. On MongoDB, `include()` takes no callback, and `count`, `sum`, `avg`, `min`, `max`, and `combine` do not exist on a collection at all. Calling one throws a `TypeError`. -- On PostgreSQL, those six are only callable **inside** an `include()` callback. Called anywhere else, they throw an `Error`. -- `sum()` and `avg()` take a numeric column: an integer, a floating-point number, or a decimal. They also take an interval or a time-of-day column. They do not take a date or a timestamp, and calling them on one is a compile error that names the operation the database has no version of. +- On PostgreSQL, those six are only callable **inside** an `include()` callback. Called anywhere else, they throw an error whose `code` is `ORM.INCLUDE_INVALID`. +- `sum()` and `avg()` take a numeric column: an integer, a floating-point number, or a decimal. They also take an interval or a `Time` column. They do not take a date or a timestamp: TypeScript rejects the field name, because the fields these two accept leave those columns out. - `min()` and `max()` take more: numeric and text columns, dates, times, timestamps, intervals, IP addresses, and text arrays. They do not take a boolean, a `uuid`, binary data, a bit string, or JSON. -- `count()` takes no argument and returns a `number`. `countBigInt()` returns a `bigint` instead, for a count too large for a JavaScript `number`. +- `count()` returns a `number`. With no argument it counts rows; with a field name it counts rows where that field is not null. `countBigInt()` returns a `bigint` instead, for a count too large for a JavaScript `number`. - `sum()` over an integer column returns a `number`. Past ±(2^53 − 1) a `number` cannot hold the total, and reading it throws an error whose `code` is `RUNTIME.DECODE_FAILED`. Use `sumBigInt(field)` for totals that large; it returns a `bigint`. -- `avg()` over an integer column returns a `number`, which rounds. `avgDecimal(field)` returns the exact average instead. -- When a to-many relation has no rows, `sum()` and `avg()` come back as `null`, not `0`. +- `avg()` over an integer column returns a `number`, which rounds. `avgDecimal(field)` returns the exact average instead, as a decimal string. +- When a to-many relation has no rows, `sum()`, `avg()`, `min()`, and `max()` come back as `null`, not `0`. - `combine(shape)` takes an object whose keys you choose. Each value is either the relation with more methods chained on it, which comes back as an array of rows, or an aggregate such as `posts.count()`, which comes back as that single value. Each value must start from the collection the callback was given, which is why the example below writes `posts` inside its own callback. #### Options @@ -500,7 +501,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, and it returns a `Temporal.Instant` (the standard JavaScript date and time object; Node.js 26 has it built in, earlier versions need `temporal-polyfill`). `sum('createdAt')` and `avg('createdAt')` do not compile, because PostgreSQL has no `SUM` or `AVG` over a date or a timestamp. The TypeScript error names the operation that is missing for that column. +`min('createdAt')` over a `DateTime` column compiles, and it returns a `Temporal.Instant` (the standard JavaScript date and time object; if your runtime has no global `Temporal`, import one, for example `import 'temporal-polyfill/full/global'`). `sum('createdAt')` and `avg('createdAt')` do not compile, because PostgreSQL has no `SUM` or `AVG` over a date or a timestamp. TypeScript rejects `'createdAt'` because it is not one of the fields those two accept. ::: ### `orderBy()` @@ -623,6 +624,7 @@ Resume pagination from a known position. - PostgreSQL only. MongoDB's collection has no `cursor()`; calling it throws `TypeError`. - Always call `orderBy()` before `cursor()`. TypeScript rejects the call if you do not, but nothing checks at runtime. If you get past the type system, the cursor value is ignored and you get every row back, with no error. +- The cursor object must name every column the `orderBy()` sorts on. Leave one out and the call throws an error whose `code` is `ORM.CURSOR_VALUE_MISSING`. #### Options @@ -717,7 +719,7 @@ Narrow a model to one of the variants declared with `@@base`. - Available for PostgreSQL and MongoDB. - Pass the variant's model name, such as `'Bug'`, not the string in its `@@base` attribute. -- On PostgreSQL, a variant's own fields are in a second table alongside the base model's table. That changes how some writes behave; see [`createAndCount()`](#createandcount) and [`upsert()`](#upsert). +- On PostgreSQL, a variant that sets its own `@@map`, as `Bug` and `Feature` do, keeps its own fields in a second table alongside the base model's table. A variant without `@@map` keeps them in the base model's table. That changes how some writes behave; see [`createAndCount()`](#createandcount) and [`upsert()`](#upsert). - On MongoDB, every variant is a document in the one collection, told apart by the field named in `@@discriminator`. #### Options @@ -765,7 +767,7 @@ Resolve the query to every matching row. #### Options -`all()` takes no arguments. +`all()` takes no required arguments. #### Return type @@ -915,7 +917,7 @@ A write method changes the database. `create`, `createAll`, `createAndCount`, `u For a task-oriented walkthrough, see [Writing data](/orm/fundamentals/writing-data). 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 you get an error if you forget, with the message `updateAll() requires a .where() filter. Call .where() before .updateAll()`. On PostgreSQL you do not: `update()` and `delete()` change one arbitrary row, and `updateAll()` and `deleteAll()` change every row. TypeScript does refuse to compile `update()` and `delete()` without a `where()`, but nothing checks at run time. +Always call `where()` before an update or a delete. On MongoDB you get an error if you forget, with the message `updateAll() requires a .where() filter. Call .where() before .updateAll()`. On PostgreSQL, TypeScript refuses to compile any of these methods without a `where()`, but nothing checks at run time. Once the type check is bypassed, `update()` and `delete()` change one arbitrary row, 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. @@ -1048,7 +1050,7 @@ Insert rows and return how many were inserted, without reading them back. - Available for PostgreSQL and MongoDB. - On PostgreSQL, `createAndCount()` does not work on a multi-table inheritance variant, a variant stored in its own table. Use `createAll()` instead. -- Calling it on such a variant throws an error whose `code` is `ORM.OPERATION_UNSUPPORTED`, with the message `createAndCount() is not supported for MTI variant "Tutorial" on model "Post". Use createAll() instead.` +- Calling it on such a variant throws an error whose `code` is `ORM.OPERATION_UNSUPPORTED`, with the message `createAndCount() is not supported for MTI variant "Bug" on model "Task". Use createAll() instead.` - On MongoDB, a variant is one document in one collection, so `createAndCount()` works on a variant as usual. #### Options @@ -1093,11 +1095,12 @@ Update the matched row and return it, or `null` if none matches. #### Remarks - Available for PostgreSQL and MongoDB. Requires a prior `where()` (see the warning at the top of this section). -- On MongoDB, `update()` takes a data object or a callback that returns field operations, such as `(t) => [t.duration.inc(5)]`. See [Field update operations](#field-update-operations). -- On PostgreSQL, `update()` takes a data object only. Passing a function is a mistake that returns `null`; pass an object. +- On MongoDB, `update()` takes a data object or a callback that returns field operations, such as `(p) => [p.content.set('Rewritten')]`. See [Field update operations](#field-update-operations). +- On MongoDB, the update input covers the base model's fields only. A field declared on a variant, such as `duration` on `Tutorial`, is not in it. [Field update operations](#field-update-operations) shows the workaround. +- On PostgreSQL, `update()` takes a data object only. Passing a function changes nothing and returns `null`; pass an object. - On PostgreSQL, `update()` can also relink related rows. `connect()` points the foreign key at a different row, and `disconnect()` removes a many-to-many link without deleting either row. - On MongoDB, `update()` changes one document in a single database operation. -- Returns `null` when no row matches. On PostgreSQL it also returns `null` when you passed a function instead of an object. +- Returns `null` when no row matches. On PostgreSQL it also returns `null` when the data you pass has no fields to change, which is what happens if you pass a function by mistake. #### Options @@ -1127,9 +1130,8 @@ const updated = await db.orm.users.where({ _id: bobId }).update({ bio: 'Now with ```typescript const updated = await db.orm.posts - .variant('Tutorial') - .where({ _id: tutorialId }) - .update((t) => [t.duration.inc(5), t.content.set('Updated content')]); + .where({ _id: postId }) + .update((p) => [p.title.set('Updated title'), p.content.set('Rewritten')]); ``` ##### Nested `connect()` relinks a foreign key (PostgreSQL) @@ -1232,9 +1234,8 @@ const count = await db.orm.users.where({ role: 'author' }).updateAndCount({ role ```typescript const count = await db.orm.posts - .variant('Tutorial') - .where({ _id: tutorialId }) - .updateAndCount((t) => [t.duration.mul(2)]); + .where({ authorId: aliceId }) + .updateAndCount((p) => [p.content.set('Rewritten')]); ``` ### `delete()` @@ -1249,7 +1250,7 @@ Remove the matched row and return it, or `null` if none matches. #### Options -`delete()` takes no arguments; filter with `where()` first. +`delete()` takes no required arguments; filter with `where()` first. #### Return type @@ -1288,7 +1289,7 @@ Remove every matching row and collect the results. #### Options -`deleteAll()` takes no arguments; filter with `where()` first. +`deleteAll()` takes no required arguments; filter with `where()` first. #### Return type @@ -1319,7 +1320,7 @@ Remove every matching row and return the count. #### Options -`deleteAndCount()` takes no arguments; filter with `where()` first. +`deleteAndCount()` takes no required arguments; filter with `where()` first. #### Return type @@ -1350,8 +1351,8 @@ Insert a row if none matches, otherwise update the existing row. - `conflictOn` takes one unique column with a value, such as `conflictOn: { label: 'typescript' }`. Only the column name is read: it names the unique constraint the database checks. The value is required by the type but is not used, so it does not have to match the value in `create`. - Leave `conflictOn` out to use the primary key instead. - On MongoDB, the `update` side takes a data object or a field-operations callback. If a field appears in both `create` and `update`, the `update` value is used, even when the row is inserted. -- On PostgreSQL, `upsert()` does not work on a multi-table inheritance variant, the same restriction as [`createAndCount()`](#createandcount). It throws an error whose `code` is `ORM.OPERATION_UNSUPPORTED`, with the message `upsert() is not supported for MTI variant "Tutorial" on model "Post". Use createAll() instead.` -- On MongoDB, the `_id` on the returned row comes back as the driver's `ObjectId`, not a hex string. Compare it as text: `String(user._id) === String(bobId)`. +- On PostgreSQL, `upsert()` does not work on a multi-table inheritance variant, the same restriction as [`createAndCount()`](#createandcount). It throws an error whose `code` is `ORM.OPERATION_UNSUPPORTED`, with the message `upsert() is not supported for MTI variant "Bug" on model "Task". Use createAll() instead.` +- On MongoDB, the `_id` on the returned row is decoded the same way as on a read, so you get a hex string, not the driver's `ObjectId`. #### Options @@ -1418,7 +1419,7 @@ const post = await db.orm.posts difficulty: 'beginner', duration: 0, }, - update: (t) => [t.duration.inc(1)], + update: (t) => [t.content.set('Updated content')], }); ``` @@ -1463,7 +1464,7 @@ model Order { customerId Uuid channel String amount Int - quantity Int + quantity Int? placedAt DateTime @default(now()) customer Customer @relation(fields: [customerId], references: [id]) @@ -1483,7 +1484,7 @@ Compute aggregates over the current result set. - PostgreSQL only. Calling `aggregate()` on a Mongo collection throws `TypeError`. - `count()` counts rows. It takes no argument and always resolves to a number (`0` over an empty set). - `count(field)` counts the rows where that field is not null. -- `sum()` and `avg()` resolve to `null`, not `0`, over an empty result set. This is standard SQL: aggregating zero rows with `SUM`/`AVG` yields `NULL`. Their TypeScript return type is `number | null` for this reason. +- `sum()`, `avg()`, `min()`, and `max()` resolve to `null`, not `0`, over an empty result set. This is standard SQL: aggregating zero rows yields `NULL`. Their TypeScript return type carries `| null` for this reason, so `sum('amount')` over an `Int` column is `number | null`. #### Options @@ -1491,13 +1492,13 @@ Compute aggregates over the current result set. |---|---|---|---| | `selector` | Callback `(agg) => ({ alias: agg.sum('amount') })` | Yes | The aggregates to compute, keyed by output alias. | -The selector's `agg` gives you `count()`, `count(field)`, `sum(field)`, `avg(field)`, `min(field)`, and `max(field)`. +The selector's `agg` gives you `count()`, `count(field)`, `sum(field)`, `avg(field)`, `min(field)`, and `max(field)`. It also gives you `countBigInt`, `sumBigInt`, and `avgDecimal`, which answer with a `bigint` or an exact decimal instead of a `number`. #### Return type | Return type | Example | Description | |---|---|---| -| Object of aggregate results | `{ total: 10 }` | One object with the requested aliases. `sum`/`avg` are `number \| null`. | +| Object of aggregate results | `{ total: 10 }` | One object with the requested aliases. `sum`, `avg`, `min`, and `max` can be `null`. | #### Examples @@ -1800,7 +1801,7 @@ Filter parents by their related rows. - `some()` with no argument matches parents with at least one related row. - A user with no posts matches `every()`. - `not(u.posts.some(...))` and `u.posts.none(...)` match the same rows. Prefer `none(...)`. -- A to-one relation is filtered the same way, from the side that holds the foreign key: `p.user.some(...)`. +- A to-one relation is filtered the same way, from either side: `p.user.some(...)`. #### Examples @@ -1826,9 +1827,9 @@ A plain object of equality matches, ANDed together. #### Remarks - Available for PostgreSQL and MongoDB. -- Multiple keys are combined with implicit AND. -- A key set to `null` becomes an IS NULL check. A key set to `undefined` is skipped. -- A field whose type cannot be compared for equality throws an error whose `code` is `ORM.FILTER_UNSUPPORTED`. Filter that field with a comparison method instead. +- Multiple keys are combined with implicit AND. A key set to `undefined` is skipped. +- On PostgreSQL, a key set to `null` becomes an IS NULL check. On MongoDB it matches documents where the field is null and documents where the field is missing. +- On PostgreSQL, a field whose type cannot be compared for equality throws an error whose `code` is `ORM.FILTER_UNSUPPORTED`. Filter that field with a comparison method instead. #### Examples @@ -1856,13 +1857,13 @@ Static factories that build MongoDB filter expressions. - `of(field, operator, value)` takes a raw MongoDB operator string, for any operator the other ten factories do not cover. - `isNull` / `isNotNull` are shorthand for equals-null and not-equals-null, not a `$exists` check. - For `$exists`, import `MongoExistsExpr` from the same module and call `MongoExistsExpr.exists(field)` or `MongoExistsExpr.notExists(field)`. -- The factories `exists`, `regex`, `elemMatch`, `all`, and `size` do **not** exist on `MongoFieldFilter`, and the library has no regex, `elemMatch`, `$all`, or `$size` support anywhere. +- The factories `exists`, `regex`, `elemMatch`, `all`, and `size` do **not** exist on `MongoFieldFilter`. Write those operators through `of`, which passes the operator string to MongoDB unchanged. #### Options | Method | Type | Description | |---|---|---| -| `of(field, operator, value)` | Field + MongoDB operator string + value | Any operator, written out, such as `'$gt'`. | +| `of(field, operator, value)` | Field + MongoDB operator string + value | Any operator, written out, such as `'$regex'`. | | `eq(field, value)` / `neq(field, value)` | Field + value | Equality / inequality. | | `gt` / `lt` / `gte` / `lte` `(field, value)` | Field + ordered value | Ordered comparisons. | | `in(field, values)` / `nin(field, values)` | Field + array | Membership / exclusion. | @@ -1898,7 +1899,7 @@ const hasBio = await db.orm.users.where(MongoFieldFilter.isNotNull('bio')).all() ```typescript import { MongoExistsExpr, MongoFieldFilter } from '@prisma/orm-mongo/query-ast/execution'; -const older = await db.orm.users.where(MongoFieldFilter.of('age', '$gt', 30)).all(); +const matching = await db.orm.posts.where(MongoFieldFilter.of('title', '$regex', '^Hello')).all(); const withBioField = await db.orm.users.where(MongoExistsExpr.exists('bio')).all(); ``` @@ -1930,19 +1931,19 @@ const usersInSf = await db.orm.users ``` ## Field update operations -On MongoDB, `update()`, `updateAndCount()`, and `upsert()` accept a callback in place of a data object. The callback receives one argument, written `u` or `t` below, with one property per field: `u.bio`, `t.duration`. Call an operation on that property, such as `u.bio.set('Hello')`. Each call returns an update instruction; collect them in the array you return. PostgreSQL's `update()` has no callback form (see [`update()`](#update)). +On MongoDB, `update()`, `updateAll()`, `updateAndCount()`, and `upsert()` accept a callback in place of a data object. The callback receives one argument, written `u` or `t` below, with one property per field: `u.bio`, `t.duration`. Call an operation on that property, such as `u.bio.set('Hello')`. The argument is also a function, so `u('address.city').set('San Francisco')` reaches a field inside an embedded object. Each call returns an update instruction; collect them in the array you return. PostgreSQL's `update()` has no callback form (see [`update()`](#update)). -All three methods require `.where()`. `update()` returns the updated document, or `null` when nothing matched. `updateAndCount()` returns the number of documents it changed. +All four methods require `.where()`. `update()` returns the updated document, or `null` when nothing matched. `updateAll()` returns the changed documents. `updateAndCount()` returns the number of documents it changed. ### Available operations (MongoDB) #### Remarks - MongoDB only. -- `set()`, `unset()`, `inc()`, and `mul()` are available and verified. +- `set()`, `unset()`, `inc()`, and `mul()` are available. - `set(value)` assigns a field. - `unset()` removes a field. - - `inc(n)` increments a numeric field. Applied to a missing field, it sets the field to `n`. + - `inc(n)` increments a numeric field. TypeScript offers `inc` and `mul` on numeric fields only. Applied to a missing field, `inc` sets the field to `n`. - `mul(n)` multiplies a numeric field. Applied to a missing field, MongoDB sets it to `0` regardless of the multiplier. - The callback argument is typed against the collection's base model, even after `variant(...)`. TypeScript only knows the base model's fields, so `t.duration.inc(10)` is a type error even though it runs correctly. Put `// @ts-expect-error` on that line to accept it. @@ -1979,7 +1980,7 @@ const multiplied = await db.orm.posts ##### `unset()` (MongoDB) -`updateAndCount()` takes the callback in the same place. `upsert()` takes it as its `update` key, beside `create`: `upsert({ create: { ... }, update: (u) => [u.bio.unset()] })`. +`updateAndCount()` takes the callback in the same place. `upsert()` takes it as its `update` key, beside `create`: `upsert({ create: { ... }, update: (u) => [u.bio.unset()] })`. That callback cannot use a dot path, and `upsert()` rejects one. ```typescript const updated = await db.orm.users.where({ _id: '6650f1c2a1b2c3d4e5f60001' }).update((u) => [u.bio.unset()]); @@ -1993,23 +1994,23 @@ const changed = await db.orm.users.where({ _id: '6650f1c2a1b2c3d4e5f60002' }).up - MongoDB. `min()`, `max()`, `rename()`, and `currentDate()` have **no accessor method at all**. :::warning[No `min`, `max`, `rename`, or `currentDate` field operations] -The callback argument implements `set`, `unset`, `inc`, `mul`, `push`, `pull`, `addToSet`, and `pop` only. `min`, `max`, `rename`, and `currentDate` are absent from the API. A missing or misspelled operation is not a compile error: `u.bio.rename` is `undefined`, and you only get a `TypeError` when you call it. Do not use `min()`, `max()`, `rename()`, or `currentDate()`. +The callback argument implements `set`, `unset`, `inc`, `mul`, `push`, `pull`, `addToSet`, and `pop` only. `min`, `max`, `rename`, and `currentDate` are absent from the API, and TypeScript rejects them: `rename` is not a property of `u.bio`. Do not use `min()`, `max()`, `rename()`, or `currentDate()`. ::: -### Array operations (MongoDB, unverified) +### Array operations (MongoDB) [#array-operations-mongodb-unverified] #### Remarks -- MongoDB. `push()`, `pull()`, `addToSet()`, and `pop()` exist on the callback argument and compile. They are not tested; try them on a real array field before relying on them. -- Applied to a field that is not an array, MongoDB rejects the write with a server-level error (`The field '...' must be an array but is of type ...`), not a library error. An array field is declared with `[]` in your contract, as in `tags String[]`. +- MongoDB. `push()`, `pull()`, `addToSet()`, and `pop()` exist on the callback argument. `push()` and `pull()` are tested against a live MongoDB. `addToSet()` and `pop()` are covered only by unit tests that check the operator they build. +- `push(value)` appends one element. `addToSet(value)` appends it only if it is not already there. `pull(match)` removes every element equal to the argument, or every element matching a partial object. `pop(1)` removes the last element, and `pop(-1)` removes the first. +- Applied to a field that is not an array, MongoDB rejects the write with its own error, not a Prisma ORM error. An array field is declared with `[]` in your contract, as in `tags String[]`. +- One update cannot touch the same field twice, so the lines below are four separate writes. The example schema has no array field, so they assume `tags String[]` on `User`. ```typescript -const updated = await db.orm.posts.where({ _id: '6650f1c2a1b2c3d4e5f60003' }).update((p) => [ - p.tags.push('mongodb'), // appends one element - p.tags.addToSet('mongodb'), // appends it only if it is not already there - p.tags.pull('draft'), // removes every element equal to the argument - p.tags.pop(1), // removes the last element; pop(-1) removes the first -]); +await db.orm.users.where({ _id: '6650f1c2a1b2c3d4e5f60001' }).update((u) => [u.tags.push('admin')]); +await db.orm.users.where({ _id: '6650f1c2a1b2c3d4e5f60001' }).update((u) => [u.tags.addToSet('admin')]); +await db.orm.users.where({ _id: '6650f1c2a1b2c3d4e5f60001' }).update((u) => [u.tags.pull('draft')]); +await db.orm.users.where({ _id: '6650f1c2a1b2c3d4e5f60001' }).update((u) => [u.tags.pop(1)]); ``` ## Result types @@ -2056,7 +2057,7 @@ If you need the type by name, import it from `@prisma/orm-postgres/components/ru 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. -- Switching from one way to the other throws an error whose `code` is `RUNTIME.ITERATOR_CONSUMED` (message: `already been consumed`). Awaiting a result and then looping it with `for await`, or the reverse, is what throws. +- Switching from one way to the other throws an error whose `code` is `RUNTIME.ITERATOR_CONSUMED`. The message contains `already been consumed`. Awaiting a result and then looping it with `for await`, or the reverse, is what throws. 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)). @@ -2096,6 +2097,6 @@ These need `8.0.0-rc.10` or newer. A `contract.d.ts` emitted by an earlier relea ### Aggregate result shapes -- `aggregate((agg) => ({ ... }))` resolves to a **single object** keyed by your aliases. `db.orm.public.Order.aggregate((agg) => ({ total: agg.count() }))` resolves to `{ total: 10 }`. +- `aggregate((agg) => ({ ... }))` resolves to a **single object** keyed by your aliases. `db.orm.public.Order.aggregate((agg) => ({ total: agg.count() }))` resolves to one object, such as `{ total: 10 }`. - `groupBy(...).aggregate((agg) => ({ ... }))` resolves to an **array**, one object per group. Each object carries the group key field or fields plus your aliases. `db.orm.public.Order.groupBy('customerId').aggregate((agg) => ({ orderCount: agg.count(), totalAmount: agg.sum('amount') }))` resolves to `[{ customerId, orderCount, totalAmount }, ...]`. -- `count()` is always a `number` (`0` over an empty set). `sum()` and `avg()` are `number | null`: they resolve to `null`, not `0`, over an empty result set. +- `count()` is always a `number` (`0` over an empty set). `sum()`, `avg()`, `min()`, and `max()` carry `| null`: they resolve to `null`, not `0`, over an empty result set. diff --git a/docs/orm-docs-audit/c21-orm-client/factcheck1.partA.md b/docs/orm-docs-audit/c21-orm-client/factcheck1.partA.md new file mode 100644 index 00000000000..39becf06550 --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-client/factcheck1.partA.md @@ -0,0 +1,9 @@ +# Fact re-check 1, slice A (Example schema, Setting up the client, Query-building methods) + +746 lines before and after. Corrections: removed "every example is copied from a test suite" (no fixture in rc.9 has this schema; PR #8033 says the harness was kept out of git) and "rows the test suite inserted first"; MongoDB related `_id` wording; a second argument to MongoDB `include()` is a TypeScript error first, ignored at runtime; aggregates outside `include()` throw `ORM.INCLUDE_INVALID`; `sum`/`avg` accept interval and `Time` (not `timetz`), and the TypeScript error rejects the field name rather than naming the operation; `count(field)` counts non-null values; `avgDecimal` returns a decimal string; `min`/`max` also `null` over an empty relation; `Temporal` note replaced by the source's own polyfill instruction (`temporal-polyfill/full/global`); a variant gets its own table only when it sets `@@map`; em-dash removed. + +Confirmed: every PSL construct in the schema individually (`types`, `type`, `@@type("pg/text@1")`, `native_enum`, `Uuid @default(uuid())`, `@@base`, `@@discriminator`, `PostTag` requirement, `ObjectId @id @map("_id")`, `@@index(... sort: Desc)`), `.eq()` takes the stored value, `variant()` takes the model name on both databases, `postgres(...)`/`mongo(...)` options, `extensions: [pgvector]`, `orm(...)` registration, import paths, `where`/`select`/`include`/`orderBy`/`limit`/`offset`/`cursor`/`distinct`/`distinctOn` claims, native enum ordering (code only, no test sorts an enum). + +Q: every result comment and id literal (invented, internally consistent); "write your own ranking expression" is general SQL advice. + +Not changed, for the operator: `distinctOn()` also throws `ORM.CAPABILITY_MISSING` without `postgres.distinctOn` in the contract; `cursor()` throws `ORM.CURSOR_VALUE_MISSING` when the cursor object leaves out an `orderBy` column; MongoDB `select()` projects field names, so with `@map("_id")` the field is `_id`. diff --git a/docs/orm-docs-audit/c21-orm-client/factcheck1.partB.md b/docs/orm-docs-audit/c21-orm-client/factcheck1.partB.md new file mode 100644 index 00000000000..3c618ff1465 --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-client/factcheck1.partB.md @@ -0,0 +1,9 @@ +# Fact re-check 1, slice B (Read methods, Custom Collection, Write methods) + +693 lines before, 692 after. Corrections: `all()` takes no required argument (it has an optional annotations callback); on PostgreSQL TypeScript refuses all six update/delete methods without `where()`, not just `update()`/`delete()`; the multi-table variant in the page's PostgreSQL schema is `Bug` on `Task`, so the `ORM.OPERATION_UNSUPPORTED` messages for `createAndCount()` and `upsert()` now name those; PostgreSQL `update()` returns `null` whenever the data maps to no columns (a function or an empty object); the MongoDB update input is typed on the base model only, so the `t.duration` examples were replaced with `set()` on base fields; the MongoDB upsert `_id` comes back decoded as a hex string, not an `ObjectId` (codecs.ts, CHANGELOG). + +Confirmed: no `count()` read method; `first()` inline filter ANDs with `where()` on PostgreSQL and takes no argument on MongoDB; `orm(...)` with `collections`, `client.connect()`, `client.context`; the `updateAll() requires a .where() filter...` message (code `ORM.WHERE_MISSING`); PostgreSQL create optional fields (nullable, column default, execution default); MongoDB `_id` only optional field; MongoDB create returns passed values plus `_id`; nested `create`/`connect`/`disconnect` PostgreSQL only; MongoDB `updateAll()` three steps; no `db.transaction()` on MongoDB; `deleteAll()` deletes before yielding; `deleteAndCount()` returns 0; `conflictOn` reads only the column name, value unused; PostgreSQL `upsert()` ignores `where()`; omitted `conflictOn` uses the primary key; MongoDB upsert `update` wins over `create` on insert. + +Q: the Prisma ORM 7 halves of the diff blocks (no ORM 7 client in the tree); whether the nested `create()` example type-checks without `userId` (matches the source's own doc example). + +Operator note: the MongoDB example schema has no numeric field on a base model, so `inc()`/`mul()` cannot be shown on MongoDB without `@ts-expect-error` or a schema change. diff --git a/docs/orm-docs-audit/c21-orm-client/factcheck1.partC.md b/docs/orm-docs-audit/c21-orm-client/factcheck1.partC.md new file mode 100644 index 00000000000..31cb21f85ac --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-client/factcheck1.partC.md @@ -0,0 +1,7 @@ +# Fact re-check 1, slice C (Grouped aggregates, filter conditions through shorthand filter) + +Six edits, line count unchanged (408). Corrections: `quantity Int?` in the aggregate schema; `min()`/`max()` also `null` over an empty set (Remarks and Return table); selector also has `countBigInt`, `sumBigInt`, `avgDecimal`; to-one relation filtering works from either side; shorthand `null` on MongoDB also matches a missing field; `ORM.FILTER_UNSUPPORTED` is PostgreSQL only. + +Q: the result comments in the `aggregate()`/`groupBy()` examples (`// { total: 10 }` etc.) and the Customer/Order schema itself have no seed data or fixture in the rc.9 source. The real aggregate tests run on the fixture in `test/integration/test/sql-orm-client/fixtures/contract.ts` (User/Post with `Post.views`). Page-wide decision needed on the "every example is copied from a test suite" claim. + +Confirmed: everything else in the slice, including `some()` taking an object or a callback, `MongoOrExpr.and()`, variadic `groupBy`, grouped `orderBy` by grouping field only, `limit`/`offset` typed `never` without `orderBy`, no `having` by alias, `in([])` renders FALSE and `notIn([])` TRUE. diff --git a/docs/orm-docs-audit/c21-orm-client/factcheck1.partD.md b/docs/orm-docs-audit/c21-orm-client/factcheck1.partD.md new file mode 100644 index 00000000000..8ca7f2dbeaa --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-client/factcheck1.partD.md @@ -0,0 +1,7 @@ +# Fact re-check 1, slice D (MongoFieldFilter, dot notation, field update operations, result types) + +Line count unchanged (209). Corrections: the library does pass `$regex`, `$elemMatch`, `$size` through `MongoFieldFilter.of` (and the pipeline builder has `$size`/`$regexMatch`), so the "no support anywhere" claim was wrong; `of()` example now uses `'$regex'` on a real field (`age` did not exist); `updateAll()` also takes the field-operations callback and the callback argument is callable for dot paths; `set`/`unset`/`inc`/`mul` no longer "verified" (`mul`/`unset` have unit tests only); `inc`/`mul` are typed on numeric fields only; a misspelled operation is a TypeScript error (closed object type), not `undefined` at runtime; `upsert()` rejects a dot path; the four array operations are tested (`orm/test/collection.test.ts:294-322`, `push`/`pull` also on a live database), so the body no longer says "not tested"; four operations on the same field in one `update()` conflict, so the example is split; the array example moved to `db.orm.users` assuming `tags String[]`; the `ITERATOR_CONSUMED` message wording is now "contains". + +Confirmed: eleven `MongoFieldFilter` factories, `isNull` is equals-null, `MongoExistsExpr`, dot path at runtime and the cast, eight field operations and their signatures (`push(value)`, `pull(match)`, `addToSet(value)`, `pop(1 | -1)`), callback typed against the base model after `variant()` so `t.duration.inc(10)` needs `// @ts-expect-error` (slice B's copy without it is wrong), MongoDB `update()` returns `null` only on no match (bad `_id` write throws `ORM.FIELD_IMMUTABLE`), both `components/runtime` import paths, re-await safety, `RUNTIME.ITERATOR_CONSUMED`, aggregate result shapes. + +Q: `inc`/`mul` on a missing field (MongoDB server behaviour, not in the tree); the server error text for an array operation on a non-array field (not in the tree). Heading "Array operations (MongoDB, unverified)": "unverified" is no longer true. From 2b31d5d07002009a7cd0c81c00642da6bd61ef00 Mon Sep 17 00:00:00 2001 From: willbot Date: Sat, 12 Sep 2026 09:37:46 +0200 Subject: [PATCH 05/11] docs(orm8): orm-client reference, reader-review round two applied Four fresh readers, four fixers, one per slice. Wording only, facts unchanged, except where a reader's question had a verified answer: where() before upsert() on PostgreSQL is ignored, ORM.WHERE_MISSING on every MongoDB write, and() and or() take any number of conditions, having() can be chained, GroupedCollection cannot be awaited, MongoAndExpr.of, isRuntimeError, db.close(). Page length unchanged (2,056 lines). Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- .../content/docs/orm/reference/orm-client.mdx | 315 +++++++++--------- .../c21-orm-client/part1.round2.md | 103 ++++++ .../c21-orm-client/part2.round2.md | 87 +++++ .../c21-orm-client/part3.round2.md | 85 +++++ .../c21-orm-client/part4.round2.md | 98 ++++++ 5 files changed, 530 insertions(+), 158 deletions(-) create mode 100644 docs/orm-docs-audit/c21-orm-client/part1.round2.md create mode 100644 docs/orm-docs-audit/c21-orm-client/part2.round2.md create mode 100644 docs/orm-docs-audit/c21-orm-client/part3.round2.md create mode 100644 docs/orm-docs-audit/c21-orm-client/part4.round2.md diff --git a/apps/docs/content/docs/orm/reference/orm-client.mdx b/apps/docs/content/docs/orm/reference/orm-client.mdx index 45357ee4bcb..48c253d1d0a 100644 --- a/apps/docs/content/docs/orm/reference/orm-client.mdx +++ b/apps/docs/content/docs/orm/reference/orm-client.mdx @@ -6,17 +6,15 @@ metaTitle: Prisma ORM client reference metaDescription: 'Reference for the Prisma ORM client''s query, mutation, filter, and aggregate methods.' --- -The ORM client gives you model-level methods for reading and writing data across PostgreSQL and MongoDB. This page documents every method, its availability on each database, and the behavior that differs between the two. +The ORM client gives you model-level methods for reading and writing data across PostgreSQL and MongoDB. This page documents every method, its availability on each database, and the behavior that differs between the two. Each method's Remarks say which databases it works on, how it behaves differently on one of them, and where TypeScript rejects something the database itself would accept. -Each method says which databases it works on. Where a method behaves differently on one of them, the Remarks say how. Where TypeScript rejects something the database itself would accept, the Remarks say that too. - -For task-oriented walkthroughs, see the Fundamentals guides: [Reading data](/orm/fundamentals/reading-data), [Writing data](/orm/fundamentals/writing-data), [Relations and joins](/orm/fundamentals/relations-and-joins), and [Transactions](/orm/fundamentals/transactions). +For task-oriented walkthroughs, see the Fundamentals guides: [Reading data](/orm/fundamentals/reading-data), [Writing data](/orm/fundamentals/writing-data), [Relations and joins](/orm/fundamentals/relations-and-joins), and [Transactions](/orm/fundamentals/transactions). If you are coming from Prisma ORM 7, [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7) lists the packages to install and maps `PrismaClient` and `findMany` onto the methods on this page. ## Example schema All examples on this page run against the schema below. The grouped-aggregate examples use a separate `Customer` / `Order` schema, shown under [Grouped aggregates](#grouped-aggregates). -You write this schema in your contract, the `contract.prisma` file that replaced `schema.prisma`. After every change to it, run `npx prisma contract emit`. That command writes `contract.json` and `contract.d.ts`, the two files the examples below import. +Write this schema in `contract.prisma`, the file that replaces `schema.prisma` from Prisma ORM 7. This page calls that file your contract. After every change to it, run `npx prisma contract emit`. That command writes `contract.json` and `contract.d.ts` next to `contract.prisma`, and the examples below import both. A new project keeps all three files in `src/prisma/`.
@@ -183,21 +181,21 @@ Some parts of the schema the examples rely on: - `@@map("user")` sets the table or collection name. Without it, the table name is the model name with a lowercase first letter. - On MongoDB, `id ObjectId @id @map("_id")` renames the field to `_id` everywhere: you filter on `_id`, and the returned document has an `_id` key. That is why the MongoDB examples never say `id`. -- `Uuid` is the type for a PostgreSQL `uuid` column. `@default(uuid())` fills the id in when you insert a row. -- `@@type("pg/text@1")` on an `enum` block stores the enum as a text column, with a check constraint that allows only the declared values. This page calls that a text-backed enum. -- A `native_enum` block is the other kind. It creates a real PostgreSQL enum type with `CREATE TYPE ... AS ENUM`. This page calls that a native enum. The two sort differently; see [`orderBy()`](#orderby). +- `Uuid` is a built-in type for a PostgreSQL `uuid` column. You do not import it. `@default(uuid())` fills the id in when you insert a row. +- `@@type("pg/text@1")` on an `enum` block stores the enum as a text column. The name inside is a PostgreSQL type plus a version, and the version is always `@1` today. The MongoDB form of the same attribute is `@@type("mongo/string@1")`. This page calls an enum written this way a text-backed enum. +- A `native_enum` block stores the same members as a real PostgreSQL enum type instead. Write `native_enum` where you would write `enum`, and give every member a value, as in `Urgent = "urgent"`. A member with no value is rejected. This page calls an enum written this way a native enum. The two sort differently; see [`orderBy()`](#orderby). - An enum member's stored value is the string after `=`, or the member name when there is no `=`. That string is what you pass to `.eq()`. So `Urgent = "urgent"` is matched by `.eq('urgent')`, and a bare `admin` member is matched by `.eq('admin')`. -- `@@base(Task, "bug")` makes `Bug` a variant of `Task`: a model stored together with the model it is based on. `@@discriminator(type)` names the column that says which variant a row is. `variant('Bug')` narrows a collection to that variant, and the name you pass is the model name, not the string in `@@base`. -- `PostTag` is required. A many-to-many needs the model for the join table, and a pair of list fields with no such model is rejected. See [Relations and joins](/orm/fundamentals/relations-and-joins#many-to-many). +- A variant is a model that reuses another model's fields and rows. `@@base(Task, "bug")` makes `Bug` a variant of `Task`. `@@discriminator(type)` names the column that records which variant each row is. `variant('Bug')` narrows a collection to that variant. Pass the variant's model name, not the string in `@@base`. +- `PostTag` is required. `Post.tags Tag[]` and `Tag.posts Post[]` are joined through `PostTag` because `PostTag` has a foreign key to each of the two models and its `@@id` lists exactly those two foreign key columns. A pair of list fields with no such model is rejected. See [Relations and joins](/orm/fundamentals/relations-and-joins#many-to-many). - `Embedding1536` comes from the pgvector extension. See [PostgreSQL](#postgresql) for the extra line it needs when you create the client. ## Setting up the client -Create a client with `postgres(...)` or `mongo(...)`, then read and write your models through `db.orm`. 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`. `db.orm` is where your models are. The same `db` also has a query builder, `db.sql` on PostgreSQL and `db.query` on MongoDB, and `db.raw`, which this page does not cover. 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 -Create a PostgreSQL client with `postgres(...)`. `db.orm` holds your models by model name, grouped by database schema: `db.orm.public.User`, `db.orm.public.Post`. `public` is the PostgreSQL schema your tables are in unless you put them in another one. +Create a PostgreSQL client with `postgres(...)`. `db.orm` holds your models by model name, grouped by database schema: `db.orm.public.User`, `db.orm.public.Post`. `public` is the PostgreSQL schema your tables are in unless you put them in another one. The import path `./contract.d` is written exactly like that, with no `.ts` on the end, and it resolves to the emitted `contract.d.ts`. ```typescript import postgres from '@prisma/orm-postgres/runtime'; @@ -221,7 +219,7 @@ const db = postgres({ }); ``` -To give a model your own methods on top of the built-in ones, subclass `Collection` and register the subclass with the `orm(...)` function instead of calling `postgres(...)`. [Custom `Collection` subclass](#custom-collection-subclass) has the code. +To give a model your own methods on top of the built-in ones, import `Collection` from `@prisma/orm-postgres/orm-client`, subclass it, and create the client with `orm(...)` from that same package instead of `postgres(...)`. [Custom `Collection` subclass](#custom-collection-subclass) has the code. ### MongoDB @@ -237,13 +235,11 @@ const db = mongo({ contractJson, url: process.env.MONGODB_URL, dbName: const users = await db.orm.users.all(); ``` -:::note[The two databases name models differently] -On PostgreSQL you write the model name under its schema: `db.orm.public.User`. On MongoDB you write the collection name with nothing in front of it: `db.orm.users`. The examples below follow each database's own form. -::: +The examples below follow each database's own form: `db.orm.public.User` on PostgreSQL, `db.orm.users` on MongoDB. ## Query-building methods -These methods narrow a query. Each returns a collection you can chain more methods on. You run the query with a [read method](#read-terminals) such as `all()` or `first()`. MongoDB supports fewer of them than PostgreSQL, and each method's Remarks say which databases it works on. +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(...)`. On MongoDB the accessor name is also the name of the database collection the documents are in, which is why `db.orm.users` reads the way it does. 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). @@ -267,6 +263,7 @@ Restrict a query to rows matching a filter. - On PostgreSQL, `where()` takes either a callback that calls an operator on a column (`u.email.eq(...)`) or an object of field-and-value pairs that must match exactly. - On MongoDB, `where()` takes either an object of field-and-value pairs that must match exactly or a `MongoFieldFilter`. The object form can only test for equality. For anything else, such as greater-than, use `MongoFieldFilter` (see [MongoFieldFilter](#mongofieldfilter)). - Calling `where()` more than once on the same query requires a row to match all of the filters. +- `.eq()` is one operator of many. [Filter conditions and operators](#filter-conditions-and-operators), further down this page, lists them all for both databases. #### Options @@ -322,13 +319,13 @@ For a task-oriented guide to filtering, see [Reading data](/orm/fundamentals/rea ### `select()` -Project a row down to a subset of scalar fields. +Return only the scalar fields you name. #### Remarks - Available for PostgreSQL and MongoDB. - On PostgreSQL, `select()` narrows the returned row shape at the type level: fields you didn't select are absent from the result type. -- On MongoDB, `select()` changes what the database returns but not the TypeScript type. Only the fields you named come back, while the type still lists every field on the model. +- On MongoDB, `select()` changes what the database returns but not the TypeScript type. Only the fields you named come back, while the type still lists every field on the model. Treat the fields you did not name as absent, whatever the type says. #### Options @@ -363,9 +360,8 @@ Eagerly load a relation onto the returned rows. - Available for PostgreSQL and MongoDB, with differences noted below. - On PostgreSQL, `include(relationName, refineFn?)` loads both to-one and to-many relations. The optional callback receives the related rows as a collection, so you can filter, order, limit, and aggregate them. See [Refinements, aggregates, and combine](#refinements-aggregates-and-combine). -- On MongoDB, `include(relationName)` loads a relation that is stored as a reference. It takes the relation name and nothing else: there is no callback. -- On MongoDB, the `_id` of a loaded related document comes back as a driver `ObjectId`, not as the hex string a document's own id field comes back as. Wrap it in `String(...)` before you compare it. -- On MongoDB, TypeScript rejects a second argument to `include()`. If you get past the type system, the extra argument is ignored and the relation loads unfiltered. +- On MongoDB, a document's own `_id` comes back as a hex string. The `_id` on a related document loaded by `include()` comes back as an `ObjectId` object instead. Wrap that one in `String(...)` before you compare it. +- On MongoDB, `include()` loads a relation that is stored as a reference. It takes the relation name and nothing else, and TypeScript rejects a second argument. #### Options @@ -407,19 +403,19 @@ 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. You can filter, order, and paginate them. You can also reduce them to a single value, or use `combine()` to return several results at once. +On PostgreSQL, the callback you pass to `include()` receives the related rows as a collection. The heading calls that callback a refinement. 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. #### Remarks -- PostgreSQL only. On MongoDB, `include()` takes no callback, and `count`, `sum`, `avg`, `min`, `max`, and `combine` do not exist on a collection at all. Calling one throws a `TypeError`. +- PostgreSQL only. On MongoDB, `include()` takes no callback, and `count`, `sum`, `avg`, `min`, `max`, and `combine` do not exist on a collection. - On PostgreSQL, those six are only callable **inside** an `include()` callback. Called anywhere else, they throw an error whose `code` is `ORM.INCLUDE_INVALID`. -- `sum()` and `avg()` take a numeric column: an integer, a floating-point number, or a decimal. They also take an interval or a `Time` column. They do not take a date or a timestamp: TypeScript rejects the field name, because the fields these two accept leave those columns out. +- `sum()` and `avg()` take a numeric column: an integer, a floating-point number, or a decimal. They also take an interval or a `Time` column. They do not take a date or a timestamp. TypeScript rejects a date or timestamp field name here. - `min()` and `max()` take more: numeric and text columns, dates, times, timestamps, intervals, IP addresses, and text arrays. They do not take a boolean, a `uuid`, binary data, a bit string, or JSON. - `count()` returns a `number`. With no argument it counts rows; with a field name it counts rows where that field is not null. `countBigInt()` returns a `bigint` instead, for a count too large for a JavaScript `number`. -- `sum()` over an integer column returns a `number`. Past ±(2^53 − 1) a `number` cannot hold the total, and reading it throws an error whose `code` is `RUNTIME.DECODE_FAILED`. Use `sumBigInt(field)` for totals that large; it returns a `bigint`. -- `avg()` over an integer column returns a `number`, which rounds. `avgDecimal(field)` returns the exact average instead, as a decimal string. +- `sum()` over an integer column returns a `number`. A total too large for a `number` throws an error whose `code` is `RUNTIME.DECODE_FAILED`. Use `sumBigInt(field)` for totals that large. It returns a `bigint`. +- `avg()` over an integer column returns a `number`, which rounds. `avgDecimal(field)` returns the exact average as a decimal string. Pass that string to a decimal library, or call `Number(...)` on it when rounding is fine. - When a to-many relation has no rows, `sum()`, `avg()`, `min()`, and `max()` come back as `null`, not `0`. -- `combine(shape)` takes an object whose keys you choose. Each value is either the relation with more methods chained on it, which comes back as an array of rows, or an aggregate such as `posts.count()`, which comes back as that single value. Each value must start from the collection the callback was given, which is why the example below writes `posts` inside its own callback. +- `combine(shape)` takes an object. You choose the keys. Build every value from the collection the callback was given, which is `posts` in the example below. A value that is `posts` with more methods chained on it comes back as an array of rows. A value that is an aggregate, such as `posts.count()`, comes back as that single value. #### Options @@ -501,7 +497,9 @@ 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, and it returns a `Temporal.Instant` (the standard JavaScript date and time object; if your runtime has no global `Temporal`, import one, for example `import 'temporal-polyfill/full/global'`). `sum('createdAt')` and `avg('createdAt')` do not compile, because PostgreSQL has no `SUM` or `AVG` over a date or a timestamp. TypeScript rejects `'createdAt'` because it is not one of the fields those two accept. +`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, add a polyfill: `import 'temporal-polyfill/full/global'`. + +`sum('createdAt')` and `avg('createdAt')` do not compile. TypeScript rejects `'createdAt'`, because PostgreSQL has no `SUM` or `AVG` over a date or a timestamp. ::: ### `orderBy()` @@ -513,7 +511,7 @@ Sort the result set. - Available for PostgreSQL and MongoDB, with different argument shapes. - On PostgreSQL, `orderBy()` takes a callback that returns `.asc()` or `.desc()` on one column. Pass an array of such callbacks to sort by more than one column. - On MongoDB, `orderBy()` takes an object instead: `{ field: 1 }` sorts ascending and `{ field: -1 }` sorts descending. -- On PostgreSQL, a native enum column sorts in the order its members are declared, not alphabetically. A `native_enum Priority` declared `Low`, `High`, `Urgent` sorts in that order under `.asc()`. A text-backed enum sorts by its stored text instead, so `'high'` comes before `'low'` and `'urgent'`. To sort a text-backed enum by meaning, store numbers rather than words, or write your own ranking expression. +- On PostgreSQL, a native enum column sorts in the order its members are declared, not alphabetically. A `native_enum Priority` declared `Low`, `High`, `Urgent` sorts in that order under `.asc()`. A text-backed enum sorts by its stored text instead, so `'high'` comes before `'low'` and `'urgent'`. Declare the enum as a `native_enum` block when the sort order matters. #### Options @@ -622,8 +620,8 @@ Resume pagination from a known position. #### Remarks -- PostgreSQL only. MongoDB's collection has no `cursor()`; calling it throws `TypeError`. -- Always call `orderBy()` before `cursor()`. TypeScript rejects the call if you do not, but nothing checks at runtime. If you get past the type system, the cursor value is ignored and you get every row back, with no error. +- PostgreSQL only. `cursor()` does not exist on a MongoDB collection. +- Always call `orderBy()` before `cursor()`. TypeScript rejects the call if you do not. Nothing checks this while the query runs, so a cursor on an unordered query is ignored and every row comes back with no error. - The cursor object must name every column the `orderBy()` sorts on. Leave one out and the call throws an error whose `code` is `ORM.CURSOR_VALUE_MISSING`. #### Options @@ -658,7 +656,7 @@ Remove duplicate rows, comparing only the fields you name. #### Remarks -- PostgreSQL only. MongoDB's collection has no `distinct()`; calling it throws `TypeError`. +- PostgreSQL only. `distinct()` does not exist on a MongoDB collection. #### Options @@ -678,6 +676,7 @@ Remove duplicate rows, comparing only the fields you name. ```typescript const priorities = await db.orm.public.Post.select('priority').distinct('priority').all(); +// priorities holds one row per distinct priority value, such as { priority: 'low' } ``` ### `distinctOn()` @@ -686,8 +685,8 @@ Keep the first row per key according to `orderBy()`. #### Remarks -- PostgreSQL only. MongoDB's collection has no `distinctOn()`; calling it throws `TypeError`. -- Call `orderBy()` first, so that "the first row per key" means something. As with `cursor()`, TypeScript checks this and the runtime does not. +- PostgreSQL only. `distinctOn()` does not exist on a MongoDB collection. +- Call `orderBy()` first, so that "the first row per key" means something. TypeScript rejects the call if you do not. #### Options @@ -709,6 +708,7 @@ Keep the first row per key according to `orderBy()`. const latestPerUser = await db.orm.public.Post.orderBy([(p) => p.userId.asc(), (p) => p.createdAt.desc()]) .distinctOn('userId') .all(); +// latestPerUser holds one post per userId: the newest one, because of the orderBy ``` ### `variant()` @@ -719,7 +719,7 @@ Narrow a model to one of the variants declared with `@@base`. - Available for PostgreSQL and MongoDB. - Pass the variant's model name, such as `'Bug'`, not the string in its `@@base` attribute. -- On PostgreSQL, a variant that sets its own `@@map`, as `Bug` and `Feature` do, keeps its own fields in a second table alongside the base model's table. A variant without `@@map` keeps them in the base model's table. That changes how some writes behave; see [`createAndCount()`](#createandcount) and [`upsert()`](#upsert). +- On PostgreSQL, a variant that sets its own `@@map`, as `Bug` and `Feature` do, is stored in a second table beside the base model's table. A variant without `@@map` is stored in the base model's table. That changes how some writes behave; see [`createAndCount()`](#createandcount) and [`upsert()`](#upsert). - On MongoDB, every variant is a document in the one collection, told apart by the field named in `@@discriminator`. #### Options @@ -750,7 +750,7 @@ const tutorials = await db.orm.posts.variant('Tutorial').all(); 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). -There is no `count()` method on either database. On PostgreSQL, count with `aggregate((a) => ({ n: a.count() }))`. On MongoDB, count with the pipeline builder. Both are shown under [Count records](/orm/fundamentals/reading-data#count-records). +There is no `count()` method on either database. On PostgreSQL, count with `aggregate((a) => ({ n: a.count() }))`. On MongoDB, count with `db.query`, the [pipeline builder](/orm/fundamentals/advanced-queries#mongodb-pipeline-builder). It is already on your client, so there is nothing extra to import. Both are shown under [Count records](/orm/fundamentals/reading-data#count-records). For a task-oriented walkthrough, see [Reading data](/orm/fundamentals/reading-data). @@ -763,7 +763,8 @@ Resolve the query to every matching row. - Available for PostgreSQL and MongoDB. - `all()` returns an [`AsyncIterableResult`](#asynciterableresult): you can `await` it to collect an array, or use `for await` to stream rows one at a time. - `await`ing a result you have already `await`ed is safe. You get the same array back, and the query does not run again. -- Each result can be used one way only. Awaiting it and then looping it with `for await`, or the reverse, throws an error whose `code` is `RUNTIME.ITERATOR_CONSUMED`. See [Single consumption and mode switching](#single-consumption-and-mode-switching). +- Each result can be used one way only. Awaiting it and then looping it with `for await`, or the reverse, throws an error whose `code` is `RUNTIME.ITERATOR_CONSUMED`. The rules for awaiting versus looping are under [Single consumption and mode switching](#single-consumption-and-mode-switching). +- `orderBy()` is written differently on each database. See [`orderBy()`](#orderby). #### Options @@ -818,7 +819,6 @@ Resolve the query to the first matching row, or `null` if none matches. - On PostgreSQL, `first()` accepts an inline filter: a shorthand object or a callback (`first((p) => p.priority.eq('urgent'))`). - On PostgreSQL, an inline filter is added to any earlier `where()` call. Both have to match. - On MongoDB, `first()` takes no filter argument. Filter with `where(...)` first, then call `first()`. -- Returns `null` when no row matches. #### Options @@ -866,14 +866,18 @@ For Prisma ORM 7 users, `findUnique` and `findFirst` map onto `first()`: + const alice = await db.orm.public.User.first({ email }); ``` +`first()` returns `null` on no match and does not check that only one row matched. For the throwing forms, `findUniqueOrThrow` and `findFirstOrThrow` map onto `all().firstOrThrow()`, which throws an error whose `code` is `RUNTIME.NO_ROWS`. + ### Custom `Collection` subclass -On PostgreSQL you can subclass `Collection` to attach domain methods, and register the subclass when you create the client. +On PostgreSQL you can subclass `Collection` to add your own methods to a model, and register the subclass when you create the client. #### Remarks -- PostgreSQL only. The MongoDB ORM has no equivalent collection-subclassing mechanism. -- To register a subclass, build the client with the `orm(...)` function and pass a `collections` object. The client's own `.orm` cannot do this: `postgres(...)` has no `collections` option, so `db.orm` always uses the plain `Collection`. +- PostgreSQL only. You cannot subclass a collection on MongoDB. +- `db.orm` cannot use your subclass. Build a second accessor by calling the `orm(...)` function with a `collections` object, and use that accessor for the models you subclassed. +- The accessor `orm(...)` returns has the same methods as `db.orm.public`. You keep the `postgres(...)` client as well, for `connect()` and for the `context` you pass to `orm(...)`. +- `Collection` takes two type arguments: your contract type, and the name of the model this collection is for. - A method that returns `this.variant('Bug')` gives back a collection narrowed to the `Bug` model. It is chainable, so you finish it with `all()`, `first()`, or any other method. #### Examples @@ -881,6 +885,7 @@ On PostgreSQL you can subclass `Collection` to attach domain methods, and regist ##### Register a subclass with a domain method (PostgreSQL) ```typescript +// postgres() comes from /runtime; Collection and orm() come from /orm-client. import postgres from '@prisma/orm-postgres/runtime'; import { Collection, orm } from '@prisma/orm-postgres/orm-client'; import type { Contract } from './contract.d'; @@ -898,10 +903,8 @@ class TaskCollection extends Collection { const client = postgres({ contractJson, url: process.env.DATABASE_URL }); const runtime = await client.connect(); -// orm() takes a `collections` option; the client's own `.orm` does not. const db = orm({ runtime, - // client.context carries the contract, which orm() needs to build collections. context: client.context, collections: { Task: TaskCollection }, }).public; // .public is the PostgreSQL schema @@ -917,7 +920,7 @@ A write method changes the database. `create`, `createAll`, `createAndCount`, `u For a task-oriented walkthrough, see [Writing data](/orm/fundamentals/writing-data). 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 you get an error if you forget, with the message `updateAll() requires a .where() filter. Call .where() before .updateAll()`. On PostgreSQL, TypeScript refuses to compile any of these methods without a `where()`, but nothing checks at run time. Once the type check is bypassed, `update()` and `delete()` change one arbitrary row, and `updateAll()` and `deleteAll()` change every row. +Always call `where()` before an update or a delete. On MongoDB all six methods 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 nothing checks at run time. TypeScript refuses to compile these methods without a `where()`, so the risk is code that escapes the type check, such as JavaScript or a cast to `any`. Without a filter, `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. @@ -931,7 +934,7 @@ Insert a single row and return it. - Available for PostgreSQL and MongoDB. - On PostgreSQL, leave out any field your contract gives a default, including the id. Nullable fields can be left out too. Every other field is required. - On MongoDB, `_id` is the only field you can leave out. Give every other field a value, and write `null` for a field you want empty. -- On MongoDB, `create()` returns the values you passed plus the `_id` the server assigned. It does not read the stored document back. On PostgreSQL the returned row comes from the database. +- On MongoDB, `create()` returns the values you passed plus the `_id` the server assigned, not the stored document. On PostgreSQL the returned row is read from the database. - On PostgreSQL, `create()` can create or link related rows in the same transaction. Nest `create()` on the side that does not hold the foreign key, and `connect()` on the side that does. - Nested `create()` and `connect()` are not available on MongoDB. @@ -939,9 +942,9 @@ Insert a single row and return it. | Name | Type | Required | Description | |---|---|---|---| -| `data` | Object of field values | Yes | The row to insert. | +| The row's fields | Object of field values | Yes | The row to insert. Pass the object as the only argument. There is no `data` wrapper. | -On PostgreSQL, `data` can also carry a `create` or `connect` callback for a related row, as shown below. +On PostgreSQL, that object can also carry a `create` or `connect` callback for a related row, as shown below. #### Return type @@ -972,12 +975,10 @@ const user = await db.orm.users.create({ ```typescript const author = await db.orm.public.User.create({ - id: '00000000-0000-0000-0000-000000000099', email: 'dana@example.com', displayName: 'Dana', kind: 'user', - posts: (posts) => - posts.create([{ id: '10000000-0000-0000-0000-000000000099', title: 'Dana post one' }]), + posts: (posts) => posts.create([{ title: 'Dana post one' }]), }); ``` @@ -985,7 +986,6 @@ const author = await db.orm.public.User.create({ ```typescript const post = await db.orm.public.Post.create({ - id: '10000000-0000-0000-0000-000000000098', title: 'Connected to Bob', user: (user) => user.connect({ id: bobId }), }); @@ -1011,7 +1011,7 @@ Insert multiple rows and return them. | Name | Type | Required | Description | |---|---|---|---| -| `data` | Array of row objects | Yes | The rows to insert. | +| The rows | Array of field objects | Yes | The rows to insert, passed as the only argument. There is no `data` wrapper. | #### Return type @@ -1034,14 +1034,6 @@ const created = await db.orm.users.createAll([ ]); ``` -##### Stream inserted rows (PostgreSQL) - -```typescript -for await (const tag of db.orm.public.Tag.createAll([{ label: 'gamma' }, { label: 'delta' }])) { - console.log(tag.label); -} -``` - ### `createAndCount()` Insert rows and return how many were inserted, without reading them back. @@ -1049,15 +1041,16 @@ Insert rows and return how many were inserted, without reading them back. #### Remarks - Available for PostgreSQL and MongoDB. -- On PostgreSQL, `createAndCount()` does not work on a multi-table inheritance variant, a variant stored in its own table. Use `createAll()` instead. -- Calling it on such a variant throws an error whose `code` is `ORM.OPERATION_UNSUPPORTED`, with the message `createAndCount() is not supported for MTI variant "Bug" on model "Task". Use createAll() instead.` +- On PostgreSQL, `createAndCount()` does not work on a variant that is stored in its own table. Use `createAll()` instead. +- A variant is stored in its own table when its `@@map` names a different table than its base model does. `Bug` in the [example schema](#example-schema) is one: it maps to `bug` while `Task` maps to `task`. +- Calling it on such a variant throws an error whose `code` is `ORM.OPERATION_UNSUPPORTED`, with the message `createAndCount() is not supported for MTI variant "Bug" on model "Task". Use createAll() instead.` The message calls it an MTI variant, short for multi-table inheritance. - On MongoDB, a variant is one document in one collection, so `createAndCount()` works on a variant as usual. #### Options | Name | Type | Required | Description | |---|---|---|---| -| `data` | Array of row objects | Yes | The rows to insert. | +| The rows | Array of field objects | Yes | The rows to insert, passed as the only argument. There is no `data` wrapper. | #### Return type @@ -1095,18 +1088,19 @@ Update the matched row and return it, or `null` if none matches. #### Remarks - Available for PostgreSQL and MongoDB. Requires a prior `where()` (see the warning at the top of this section). -- On MongoDB, `update()` takes a data object or a callback that returns field operations, such as `(p) => [p.content.set('Rewritten')]`. See [Field update operations](#field-update-operations). -- On MongoDB, the update input covers the base model's fields only. A field declared on a variant, such as `duration` on `Tutorial`, is not in it. [Field update operations](#field-update-operations) shows the workaround. -- On PostgreSQL, `update()` takes a data object only. Passing a function changes nothing and returns `null`; pass an object. +- On MongoDB, `update()` takes an object of field values, or a callback that returns field operations, such as `(p) => [p.content.set('Rewritten')]`. See [Field update operations](#field-update-operations). +- On MongoDB, the update input covers the base model's fields only. A field declared on a variant, such as `duration` on `Tutorial`, is a TypeScript error even though it runs correctly. Put `// @ts-expect-error` on that line, as shown under [Field update operations](#field-update-operations). +- On PostgreSQL, `update()` takes an object of field values only. Passing a function changes nothing and returns `null`. - On PostgreSQL, `update()` can also relink related rows. `connect()` points the foreign key at a different row, and `disconnect()` removes a many-to-many link without deleting either row. -- On MongoDB, `update()` changes one document in a single database operation. -- Returns `null` when no row matches. On PostgreSQL it also returns `null` when the data you pass has no fields to change, which is what happens if you pass a function by mistake. +- Returns `null` when no row matches. On PostgreSQL it also returns `null` when what you pass has no fields to change. #### Options | Name | Type | Required | Description | |---|---|---|---| -| `data` | Data object, or (MongoDB) a field-operations callback | Yes | The changes to apply. On PostgreSQL, relation mutators (`connect`, `disconnect`) may be included. | +| The changes | Object of field values, or on MongoDB a field-operations callback | Yes | What to set on the matched row, passed as the only argument. There is no `data` wrapper. | + +On PostgreSQL that object can also carry `connect` or `disconnect` for a related row, as shown below. #### Return type @@ -1145,13 +1139,14 @@ const relinked = await db.orm.public.Post.where({ id: postId }).update({ ##### Nested `disconnect()` unlinks a many-to-many row (PostgreSQL) ```typescript +// select() and include() shape the row you get back, nothing else. const updated = await db.orm.public.Post.where({ id: postId }) .select('id', 'title') .include('tags', (tag) => tag.select('id', 'label').orderBy((t) => t.label.asc())) .update({ tags: (tag) => tag.disconnect([{ id: tagId }]), }); -// only the join-table row is removed; the Tag row itself still exists +// only the join-table row is removed. The Tag row itself still exists ``` For Prisma ORM 7 users, `update()` moves the filter into `where()` and drops the `data` wrapper: @@ -1167,16 +1162,17 @@ Update every matching row and collect the results. #### Remarks -- Available for PostgreSQL and MongoDB. Call `where()` first: MongoDB enforces this at runtime; PostgreSQL does not (see the section warning above: without a filter these affect every row). +- Available for PostgreSQL and MongoDB. Call `where()` first. Without a filter this changes every row. See the warning at the top of this section. - On PostgreSQL, `updateAll()` runs as one statement, so every matching row changes together. -- On MongoDB, `updateAll()` is not one operation. It collects the matching ids, updates, then reads those documents back. A document someone else changes in between can come back with their values, and a document that starts matching only after the first step is updated but not returned. -- Prisma ORM has no `db.transaction()` on MongoDB, so there is no way to avoid this from the client. If it matters, run the writes yourself in a MongoDB driver session. See [Transactions on MongoDB](/orm/fundamentals/transactions#transactions-on-mongodb). +- On MongoDB, `updateAll()` is three database operations. First it collects the ids of the documents that match your filter. Then it updates every document that matches that filter, not only the ids it collected. Then it reads back the documents whose ids it collected. +- Two things follow from that. A document that starts matching only after the first step is updated but not returned. A document that someone else changes after the update comes back with their values. +- Prisma ORM has no `db.transaction()` on MongoDB, so you cannot avoid this from the client. To control it, share one `MongoClient` with the MongoDB driver and group the writes in a driver session, shown under [Transactions on MongoDB](/orm/fundamentals/transactions#transactions-on-mongodb). #### Options | Name | Type | Required | Description | |---|---|---|---| -| `data` | Data object, or (MongoDB) a field-operations callback | Yes | The changes to apply to every matched row. | +| The changes | Object of field values, or on MongoDB a field-operations callback | Yes | What to set on every matched row, passed as the only argument. There is no `data` wrapper. | #### Return type @@ -1203,14 +1199,13 @@ Update every matching row and return the count. #### Remarks -- Available for PostgreSQL and MongoDB. Call `where()` first: MongoDB enforces this at runtime; PostgreSQL does not (see the section warning above: without a filter these affect every row). -- On MongoDB, `updateAndCount()` accepts a data object or a field-operations callback. +- Available for PostgreSQL and MongoDB. Call `where()` first. Without a filter this changes every row. See the warning at the top of this section. #### Options | Name | Type | Required | Description | |---|---|---|---| -| `data` | Data object, or (MongoDB) a field-operations callback | Yes | The changes to apply to every matched row. | +| The changes | Object of field values, or on MongoDB a field-operations callback | Yes | What to set on every matched row, passed as the only argument. There is no `data` wrapper. | #### Return type @@ -1244,13 +1239,11 @@ Remove the matched row and return it, or `null` if none matches. #### Remarks -- Available for PostgreSQL and MongoDB. Requires a prior `where()` (see the section warning above). -- On MongoDB, `delete()` removes one document in a single database operation. -- Returns `null` when no row matches. +- Available for PostgreSQL and MongoDB. Requires a prior `where()` (see the section warning above). Returns `null` when no row matches. #### Options -`delete()` takes no required arguments; filter with `where()` first. +`delete()` takes no required arguments. Filter with `where()` first. #### Return type @@ -1284,12 +1277,12 @@ Remove every matching row and collect the results. #### Remarks -- Available for PostgreSQL and MongoDB. Call `where()` first: MongoDB enforces this at run time; PostgreSQL does not (see the section warning above: without a filter this deletes every row). -- Every matching row is deleted before the first row reaches you. Stopping the loop early still leaves nothing behind. +- Available for PostgreSQL and MongoDB. Call `where()` first. Without a filter this deletes every row. See the warning at the top of this section. +- Every matching row is deleted before the first row reaches you. Stopping the loop early does not save any of them. #### Options -`deleteAll()` takes no required arguments; filter with `where()` first. +`deleteAll()` takes no required arguments. Filter with `where()` first. #### Return type @@ -1315,12 +1308,12 @@ Remove every matching row and return the count. #### Remarks -- Available for PostgreSQL and MongoDB. Call `where()` first: MongoDB enforces this at runtime; PostgreSQL does not (see the section warning above: without a filter these affect every row). +- Available for PostgreSQL and MongoDB. Call `where()` first. Without a filter this deletes every row. See the warning at the top of this section. - Returns `0` when no row matches. #### Options -`deleteAndCount()` takes no required arguments; filter with `where()` first. +`deleteAndCount()` takes no required arguments. Filter with `where()` first. #### Return type @@ -1346,21 +1339,26 @@ Insert a row if none matches, otherwise update the existing row. #### Remarks -- On MongoDB, call `where()` first; the filter decides which document is updated. -- On PostgreSQL, `where()` is not used. `conflictOn` decides which row counts as already existing. -- `conflictOn` takes one unique column with a value, such as `conflictOn: { label: 'typescript' }`. Only the column name is read: it names the unique constraint the database checks. The value is required by the type but is not used, so it does not have to match the value in `create`. +- On MongoDB, call `where()` first. The filter decides which document is updated. +- On PostgreSQL, `upsert()` never reads the filters. A `where()` before it is ignored, without an error. `conflictOn` decides which row counts as already existing. +- `conflictOn` takes one unique column with a value, such as `conflictOn: { label: 'typescript' }`. Only the column name is used. The value is required by the type and is then ignored, so pass any value of the right type. - Leave `conflictOn` out to use the primary key instead. -- On MongoDB, the `update` side takes a data object or a field-operations callback. If a field appears in both `create` and `update`, the `update` value is used, even when the row is inserted. -- On PostgreSQL, `upsert()` does not work on a multi-table inheritance variant, the same restriction as [`createAndCount()`](#createandcount). It throws an error whose `code` is `ORM.OPERATION_UNSUPPORTED`, with the message `upsert() is not supported for MTI variant "Bug" on model "Task". Use createAll() instead.` -- On MongoDB, the `_id` on the returned row is decoded the same way as on a read, so you get a hex string, not the driver's `ObjectId`. +- When a row is inserted on PostgreSQL, the `create` side is used as you wrote it. +- When a row is inserted on MongoDB, a field that appears in both `create` and `update` takes the `update` value. +- On MongoDB, the `update` side takes an object of field values or a field-operations callback. +- On PostgreSQL, `upsert()` does not work on a variant stored in its own table, the same restriction as [`createAndCount()`](#createandcount). It throws an error whose `code` is `ORM.OPERATION_UNSUPPORTED`, with the message `upsert() is not supported for MTI variant "Bug" on model "Task". Use createAll() instead.` +- That advice is the library's own. `createAll()` inserts rows and has no update half, so use it when the rows are new. +- On MongoDB, the `_id` on the returned row is a hex string, not the driver's `ObjectId`. #### Options | Name | Type | Required | Description | |---|---|---|---| -| `create` | Data object | Yes | The row to insert if none matches. | -| `update` | Data object, or (MongoDB) a field-operations callback | Yes | The changes to apply if a row matches. | -| `conflictOn` | Object with one unique column and a value (PostgreSQL) | No | Names the unique constraint that decides insert or update. Omit to use the primary key. | +| `create` | Object of field values | Yes | The row to insert if none matches. | +| `update` | Object of field values, or on MongoDB a field-operations callback | Yes | The changes to apply if a row matches. | +| `conflictOn` | Object with one unique column and a value, PostgreSQL only | No | The column that decides insert or update. Omit to use the primary key. | + +Only the column name in `conflictOn` is used. The value it is paired with is ignored. #### Return type @@ -1373,18 +1371,19 @@ Insert a row if none matches, otherwise update the existing row. ##### Insert or update (PostgreSQL) ```typescript -// Insert: no existing row has label 'brand-new', so the create side is used. +// Insert: no existing row has label 'brand-new', so the row is inserted +// exactly as the create side describes it. const inserted = await db.orm.public.Tag.upsert({ - create: { id: '30000000-0000-0000-0000-000000000099', label: 'brand-new' }, + create: { label: 'brand-new' }, update: { label: 'brand-new-updated' }, - conflictOn: { label: 'brand-new' }, + conflictOn: { label: 'ignored' }, // only the column name `label` is used }); // Update: a row with label 'typescript' already exists, so the update runs against it. const updated = await db.orm.public.Tag.upsert({ - create: { id: '30000000-0000-0000-0000-000000000098', label: 'typescript' }, + create: { label: 'typescript' }, update: { label: 'typescript-renamed' }, - conflictOn: { label: 'typescript' }, + conflictOn: { label: 'ignored' }, // the same value works here, because it is ignored }); ``` @@ -1401,7 +1400,7 @@ const user = await db.orm.users.where({ email: 'newperson@example.com' }).upsert }, update: { bio: 'set on upsert' }, }); -// on insert, an `update` value replaces the `create` value for the same field +// on insert, bio is 'set on upsert': the `update` value wins on MongoDB ``` ##### Field-operations callback on the update side (MongoDB) @@ -1440,7 +1439,7 @@ For Prisma ORM 7 users, `upsert()` replaces the `where` argument with `conflictO ## Grouped aggregates -PostgreSQL supports aggregation over a result set, both flat (`aggregate()`) and grouped (`groupBy().aggregate()`), with `having()` to filter groups. MongoDB has neither method: `aggregate()` and `groupBy()` do not exist on a MongoDB collection, and calling them throws a `TypeError`. Use the pipeline builder for MongoDB aggregation instead, described in [MongoDB: Pipeline builder](/orm/fundamentals/advanced-queries#mongodb-pipeline-builder). +PostgreSQL supports aggregation over a result set, both flat (`aggregate()`) and grouped (`groupBy().aggregate()`), with `having()` to filter groups. Neither method exists on a MongoDB collection. To aggregate on MongoDB, use the pipeline builder, which is this library's API for MongoDB's own aggregation pipeline. It 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: @@ -1481,24 +1480,24 @@ Compute aggregates over the current result set. #### Remarks -- PostgreSQL only. Calling `aggregate()` on a Mongo collection throws `TypeError`. +- PostgreSQL only. Not available on MongoDB. - `count()` counts rows. It takes no argument and always resolves to a number (`0` over an empty set). - `count(field)` counts the rows where that field is not null. -- `sum()`, `avg()`, `min()`, and `max()` resolve to `null`, not `0`, over an empty result set. This is standard SQL: aggregating zero rows yields `NULL`. Their TypeScript return type carries `| null` for this reason, so `sum('amount')` over an `Int` column is `number | null`. +- `sum()`, `avg()`, `min()`, and `max()` resolve to `null`, not `0`, over an empty result set. Handle the `null`. `sum('amount')` over an `Int` column is typed `number | null`. #### Options | Name | Type | Required | Description | |---|---|---|---| -| `selector` | Callback `(agg) => ({ alias: agg.sum('amount') })` | Yes | The aggregates to compute, keyed by output alias. | +| `selector` | Callback `(agg) => ({ total: agg.sum('amount') })` | Yes | The aggregates to compute. Each key you return becomes a key of the result object. | -The selector's `agg` gives you `count()`, `count(field)`, `sum(field)`, `avg(field)`, `min(field)`, and `max(field)`. It also gives you `countBigInt`, `sumBigInt`, and `avgDecimal`, which answer with a `bigint` or an exact decimal instead of a `number`. +The callback's `agg` argument gives you `count()`, `count(field)`, `sum(field)`, `avg(field)`, `min(field)`, and `max(field)`. It also gives you three exact variants, for numbers too large or too precise to hold in a JavaScript `number`. `agg.countBigInt()` takes no argument and returns a `bigint`. `agg.sumBigInt('amount')` returns a `bigint`. `agg.avgDecimal('amount')` returns the exact average as a string, such as `'10.3333333333333333'`. You import nothing to use these. #### Return type | Return type | Example | Description | |---|---|---| -| Object of aggregate results | `{ total: 10 }` | One object with the requested aliases. `sum`, `avg`, `min`, and `max` can be `null`. | +| Object | `{ total: 10 }` | One key per key you returned from the callback. `sum`, `avg`, `min`, and `max` can be `null`. | #### Examples @@ -1562,11 +1561,11 @@ Group rows by one or more fields, then aggregate per group. #### Remarks -- PostgreSQL only. Calling `groupBy()` on a Mongo collection throws `TypeError`. +- PostgreSQL only. Not available on MongoDB. - Pass field names as separate arguments: `groupBy('customerId', 'channel')`. -- `groupBy(...)` returns a `GroupedCollection`. Call `.aggregate(...)` on it to get one row per group, carrying both the grouping fields and the aggregate aliases. -- A `where(...)` before `groupBy(...)` filters the rows that get grouped. -- `orderBy(...)` on a grouped collection sorts the groups by a grouping field. You cannot sort them by an aggregate value here. For that, use the SQL query builder, described in [Advanced queries](/orm/fundamentals/advanced-queries). +- `groupBy(...)` returns a `GroupedCollection`. You cannot `await` it. Call `.aggregate(...)` on it, which runs the query and gives you one row per group. Each row holds the grouping fields, typed as they are on your model, plus the keys you returned from the aggregate callback. +- Chain order: `where(...)` comes before `groupBy(...)` and filters the rows that get grouped. After `groupBy(...)` you can call `having(...)`, `orderBy(...)`, `limit(...)`, and `offset(...)` in any order. `aggregate(...)` comes last. +- `orderBy(...)` on a grouped collection sorts the groups by a field you grouped on. You cannot sort groups by an aggregate value. To do that, write the query with the SQL query builder (`db.sql`), described in [Advanced queries](/orm/fundamentals/advanced-queries). - `limit(n)` and `offset(n)` page the groups, and both require an `orderBy(...)` before them. Without one they are a TypeScript error. #### Options @@ -1579,7 +1578,7 @@ Group rows by one or more fields, then aggregate per group. | Return type | Example | Description | |---|---|---| -| `GroupedCollection` | `db.orm.public.Order.groupBy('customerId')` | A grouped collection, resolved via `aggregate()`. | +| `GroupedCollection` | `db.orm.public.Order.groupBy('customerId')` | Not a promise. Call `.aggregate(...)` on it to run the query. | #### Examples @@ -1607,7 +1606,6 @@ const perCustomerAndChannel = await db.orm.public.Order.groupBy('customerId', 'c ```typescript const perCustomer = await db.orm.public.Order.where((o) => o.amount.gte(100)) .groupBy('customerId') - .having((h) => h.count().gt(1)) .aggregate((agg) => ({ totalAmount: agg.sum('amount') })); ``` @@ -1615,7 +1613,7 @@ const perCustomer = await db.orm.public.Order.where((o) => o.amount.gte(100)) ```typescript const firstTwoChannels = await db.orm.public.Order.groupBy('channel') - .orderBy((group) => group.channel.desc()) + .orderBy((group) => group.channel.desc()) // desc() sorts descending, asc() ascending .limit(2) .aggregate((agg) => ({ orderCount: agg.count() })); ``` @@ -1633,23 +1631,22 @@ Filter groups by an aggregate comparison. #### Remarks -- PostgreSQL only. Chained on a `GroupedCollection` between `groupBy()` and `aggregate()`. -- The `having()` callback gives you `count()`, `sum(field)`, `avg(field)`, `min(field)`, and `max(field)`. Each of those returns an object with `eq`, `neq`, `gt`, `lt`, `gte`, and `lte`. -- You cannot refer to an alias from `aggregate()` here. Write the aggregate out again in `having()`. +- PostgreSQL only. Not available on MongoDB. Call it on the result of `groupBy()`, before `aggregate()`. +- The `having()` callback gives you `count()`, `count(field)`, `sum(field)`, `avg(field)`, `min(field)`, and `max(field)`. Call a comparison on the result: `h.sum('amount').gt(1000)`. The comparisons are `eq`, `neq`, `gt`, `lt`, `gte`, and `lte`. +- For more than one condition, call `.having(...)` again. The conditions are combined with AND. +- You cannot refer to a key you returned from `aggregate()` here. Write the aggregate out again in `having()`. #### Options | Name | Type | Required | Description | |---|---|---|---| -| `predicate` | Callback `(h) => h.().()` | Yes | The group-level condition. | - -The angle-bracket names above are placeholders. A real call is `h.sum('amount').gt(1000)`. +| `predicate` | Callback `(h) => h.sum('amount').gt(1000)` | Yes | The condition each group must match. | #### Return type | Return type | Example | Description | |---|---|---| -| `GroupedCollection` | `db.orm.public.Order.groupBy('customerId').having(...)` | A grouped collection filtered by the aggregate predicate. | +| `GroupedCollection` | `db.orm.public.Order.groupBy('customerId').having(...)` | The same grouped collection with this condition added. Call `.aggregate(...)` to run the query. | #### Examples @@ -1671,10 +1668,10 @@ const groupsWithAtLeastFive = await db.orm.public.Order.groupBy('customerId') ## Filter conditions and operators -Filters build the condition inside `where()`, and inside relation filters. PostgreSQL and MongoDB have different filter APIs. +A condition says which rows match. You write one inside `where()`, and inside a relation filter such as `u.posts.some(...)`. PostgreSQL and MongoDB write conditions differently. -- **PostgreSQL** uses column-level comparison methods (`u.email.eq(...)`) inside a callback, plus standalone combinator functions imported from `@prisma/orm-postgres/orm-client`. -- **MongoDB** uses static factories on `MongoFieldFilter` imported from `@prisma/orm-mongo/query-ast/execution`, plus `.and()`/`.not()` instance methods and the standalone `MongoOrExpr.of(...)`. +- **PostgreSQL** compares a field with a method on that field, such as `u.email.eq(...)`, inside a callback. To combine conditions, import `and`, `or`, `not`, and `all` from `@prisma/orm-postgres/orm-client`. +- **MongoDB** builds each condition by calling a method on the `MongoFieldFilter` class itself, such as `MongoFieldFilter.eq('name', 'Alice')`. The full list is under [`MongoFieldFilter`](#mongofieldfilter). To combine conditions, use `.and()`, `.not()`, and `MongoOrExpr.of([...])`, described under [Combinators](#combinators). ### Scalar comparisons (PostgreSQL) @@ -1682,7 +1679,7 @@ Column-level comparison methods on a field. #### Remarks -- PostgreSQL. Which methods a field has depends on its type: `eq`/`neq`/`in`/`notIn` on any field that can be compared for equality; `gt`/`lt`/`gte`/`lte` on any field that can be ordered; `like` and `ilike` on text fields; `isNull`/`isNotNull` on every field. +- PostgreSQL. Which methods a field has depends on its type, and your editor's autocomplete is the answer: if it does not offer a method on that field, the type does not support it. `isNull` and `isNotNull` are on every field. `eq`, `neq`, `in`, and `notIn` need a type that can be compared for equality. `gt`, `lt`, `gte`, and `lte` need a type that can be ordered. `like` and `ilike` need a text type. - `like()` matches a SQL pattern case-sensitively. `ilike()` matches the same pattern case-insensitively. - `in([])` matches no rows. `notIn([])` matches every row. @@ -1724,18 +1721,20 @@ const caseInsensitive = await db.orm.public.User.where((u) => u.email.ilike('%@E ```typescript const lowOrHigh = await db.orm.public.Post.where((p) => p.priority.in(['low', 'high'])).all(); const notLowOrHigh = await db.orm.public.Post.where((p) => p.priority.notIn(['low', 'high'])).all(); -const withoutEmbedding = await db.orm.public.Post.where((p) => p.embedding.isNull()).all(); +const withoutDescription = await db.orm.public.Task.where((t) => t.description.isNull()).all(); // description is String? ``` ### Combinators -Combine or negate predicates. +Combine or negate conditions. #### Remarks - On PostgreSQL, `and`, `or`, `not`, and `all` are **standalone functions** imported from `@prisma/orm-postgres/orm-client`. You import them; they are not methods on a field. -- On PostgreSQL, `all()` takes no arguments and returns a condition that is always true. Use it where the code has to pass a condition but you do not want to filter anything, such as a branch of your own helper. This `all()` is not the `.all()` you call at the end of a chain to run the query. -- On MongoDB, `.and(other)` and `.not()` are **instance methods** on every filter expression, including `MongoFieldFilter`, `MongoOrExpr`, and `MongoAndExpr`. There is **no `.or()` instance method**. To OR filters, build `MongoOrExpr.of([...])`. +- On PostgreSQL, `and(...)` and `or(...)` take any number of conditions, so `and(a, b, c)` is fine. `not(...)` takes one. +- On PostgreSQL, `all()` takes no arguments and returns a condition that matches every row. This `all()` is not the `.all()` you call at the end of a chain to run the query. +- On MongoDB, `.and(other)` and `.not()` are **methods on a condition you already built**, such as `MongoFieldFilter.eq('name', 'Alice').and(...)`. There is **no `.or()` method**. To OR conditions, build `MongoOrExpr.of([...])`. +- On MongoDB, `.and(other)` takes exactly one condition. For three, chain it: `a.and(b).and(c)`. The result is a `MongoAndExpr`, which you can also build directly with `MongoAndExpr.of([a, b, c])`. - On MongoDB, an AND of two ORs is `MongoOrExpr.of([...]).and(MongoOrExpr.of([...]))`. #### Examples @@ -1796,12 +1795,12 @@ Filter parents by their related rows. #### Remarks -- PostgreSQL. `some()`, `every()`, and `none()` are methods on a to-many relation, and they compile to SQL `EXISTS` subqueries. +- PostgreSQL. `some()`, `every()`, and `none()` are methods on a to-many relation, which is a relation that holds many related rows, such as `User.posts`. - Each of the three takes either a callback or a plain object of equality matches: `u.posts.some((p) => p.priority.eq('urgent'))` and `u.posts.some({ priority: 'urgent' })` mean the same thing. - `some()` with no argument matches parents with at least one related row. - A user with no posts matches `every()`. - `not(u.posts.some(...))` and `u.posts.none(...)` match the same rows. Prefer `none(...)`. -- A to-one relation is filtered the same way, from either side: `p.user.some(...)`. +- `some()` is also the method for a to-one relation, such as `Post.user`, which holds a single related row. There it reads as "the related row matches". It works from both sides of a relation, so `u.posts.some(...)` and `p.user.some(...)` are both valid. #### Examples @@ -1848,16 +1847,15 @@ const alice = await db.orm.users.where({ name: 'Alice', role: 'author' }).first( ### `MongoFieldFilter` -Static factories that build MongoDB filter expressions. +Helpers that build one MongoDB filter condition. Use them for anything other than equality on a top-level field, which `.where({ name: 'Alice' })` already covers. To join two conditions with AND or OR, see [Combinators](#combinators). #### Remarks -- MongoDB. Imported from `@prisma/orm-mongo/query-ast/execution`. -- Exactly eleven factories exist: `of`, `eq`, `neq`, `gt`, `lt`, `gte`, `lte`, `in`, `nin`, `isNull`, `isNotNull`. There is no `ne`; use `neq`. -- `of(field, operator, value)` takes a raw MongoDB operator string, for any operator the other ten factories do not cover. -- `isNull` / `isNotNull` are shorthand for equals-null and not-equals-null, not a `$exists` check. -- For `$exists`, import `MongoExistsExpr` from the same module and call `MongoExistsExpr.exists(field)` or `MongoExistsExpr.notExists(field)`. -- The factories `exists`, `regex`, `elemMatch`, `all`, and `size` do **not** exist on `MongoFieldFilter`. Write those operators through `of`, which passes the operator string to MongoDB unchanged. +- MongoDB. Import from `@prisma/orm-mongo/query-ast/execution`. `MongoExistsExpr` is in the same module. +- The factories are `of`, `eq`, `neq`, `gt`, `lt`, `gte`, `lte`, `in`, `nin`, `isNull`, and `isNotNull`. There is no `ne`; use `neq`. +- `of(field, operator, value)` takes the MongoDB operator written out as a quoted string, such as `'$regex'`. Use it for operators the named factories do not cover, such as `MongoFieldFilter.of('tags', '$size', 3)`. It passes the operator string to MongoDB unchanged. +- `isNull(field)` matches documents where the field is `null` and documents where the field is missing. `isNotNull(field)` is its opposite. To test only whether a field is present, use `MongoExistsExpr.exists(field)` or `MongoExistsExpr.notExists(field)`. Those two are all `MongoExistsExpr` offers. +- There is no `regex`, `elemMatch`, `all`, or `size` factory. Write those operators through `of`. #### Options @@ -1865,9 +1863,9 @@ Static factories that build MongoDB filter expressions. |---|---|---| | `of(field, operator, value)` | Field + MongoDB operator string + value | Any operator, written out, such as `'$regex'`. | | `eq(field, value)` / `neq(field, value)` | Field + value | Equality / inequality. | -| `gt` / `lt` / `gte` / `lte` `(field, value)` | Field + ordered value | Ordered comparisons. | +| `gt(field, value)` / `lt(field, value)` / `gte(field, value)` / `lte(field, value)` | Field + a value that can be compared, such as a number, a date, or a string | Greater than, less than, greater or equal, less or equal. | | `in(field, values)` / `nin(field, values)` | Field + array | Membership / exclusion. | -| `isNull(field)` / `isNotNull(field)` | Field | Null / not-null (equals-null semantics). | +| `isNull(field)` / `isNotNull(field)` | Field | Matches a `null` value and a missing field / neither of those. | #### Examples @@ -1909,8 +1907,8 @@ Filter into a field of an embedded value object using a dot path. #### Remarks -- MongoDB. This works at runtime, but the TypeScript types do not know nested paths, so the object form needs a cast. Prefer `MongoFieldFilter.eq`, which takes a bare `string` path and needs no cast. -- `MongoWhereFilter` is typed against the model's own top-level field names. `{ 'address.city': ... }` is therefore a compile error without a cast. +- MongoDB. Use `MongoFieldFilter.eq('address.city', 'San Francisco')`. It takes the path as a plain `string` and needs no cast. +- The plain object form, `.where({ 'address.city': 'San Francisco' })`, does the same thing when it runs, but it does not compile without a cast. The object you pass to `.where()` is typed against the model's own top-level field names, and a dotted key is not one of them. #### Examples @@ -1922,7 +1920,7 @@ import { MongoFieldFilter } from '@prisma/orm-mongo/query-ast/execution'; const usersInSf = await db.orm.users.where(MongoFieldFilter.eq('address.city', 'San Francisco')).all(); ``` -The object form works at runtime too, with a cast: +The object form works at runtime too, with a cast. The cast goes through `unknown` because TypeScript refuses a direct cast between two types that do not overlap: ```typescript const usersInSf = await db.orm.users @@ -1931,21 +1929,20 @@ const usersInSf = await db.orm.users ``` ## Field update operations -On MongoDB, `update()`, `updateAll()`, `updateAndCount()`, and `upsert()` accept a callback in place of a data object. The callback receives one argument, written `u` or `t` below, with one property per field: `u.bio`, `t.duration`. Call an operation on that property, such as `u.bio.set('Hello')`. The argument is also a function, so `u('address.city').set('San Francisco')` reaches a field inside an embedded object. Each call returns an update instruction; collect them in the array you return. PostgreSQL's `update()` has no callback form (see [`update()`](#update)). +On MongoDB, `update()`, `updateAll()`, `updateAndCount()`, and `upsert()` accept a callback in place of a data object. The callback receives one argument. The examples below call it `u` or `t`, and the name is yours to pick. For a top-level field, read it as a property and call an operation on it: `u.bio.set('Hello')`. For a field inside an embedded object, call the argument as a function with the path: `u('address.city').set('San Francisco')`. Return an array of the calls you made. PostgreSQL has no callback form: pass an object to [`update()`](#update) instead. -All four methods require `.where()`. `update()` returns the updated document, or `null` when nothing matched. `updateAll()` returns the changed documents. `updateAndCount()` returns the number of documents it changed. +All four methods require `.where()`. Without one they throw an error whose `code` is `ORM.WHERE_MISSING`. `update()` returns the updated document, or `null` when nothing matched. `updateAll()` returns an [`AsyncIterableResult`](#asynciterableresult) of the changed documents. `updateAndCount()` returns the number of documents it changed. ### Available operations (MongoDB) #### Remarks -- MongoDB only. -- `set()`, `unset()`, `inc()`, and `mul()` are available. +- MongoDB only. `set()`, `unset()`, `inc()`, and `mul()` are available. - `set(value)` assigns a field. - `unset()` removes a field. - `inc(n)` increments a numeric field. TypeScript offers `inc` and `mul` on numeric fields only. Applied to a missing field, `inc` sets the field to `n`. - `mul(n)` multiplies a numeric field. Applied to a missing field, MongoDB sets it to `0` regardless of the multiplier. -- The callback argument is typed against the collection's base model, even after `variant(...)`. TypeScript only knows the base model's fields, so `t.duration.inc(10)` is a type error even though it runs correctly. Put `// @ts-expect-error` on that line to accept it. +- After `variant(...)`, TypeScript still knows only the base model's fields, so `t.duration.inc(10)` is a type error even though it runs correctly. Put `// @ts-expect-error` on the line directly above the `.update(...)` line, as the example below shows. If the type error ever goes away, TypeScript reports the unused comment instead. #### Examples @@ -1980,7 +1977,7 @@ const multiplied = await db.orm.posts ##### `unset()` (MongoDB) -`updateAndCount()` takes the callback in the same place. `upsert()` takes it as its `update` key, beside `create`: `upsert({ create: { ... }, update: (u) => [u.bio.unset()] })`. That callback cannot use a dot path, and `upsert()` rejects one. +`updateAndCount()` takes the callback in the same place. `upsert()` takes it as its `update` key, beside `create`: `upsert({ create: { ... }, update: (u) => [u.bio.unset()] })`. That callback cannot use a dot path. Given one, `upsert()` throws an error whose `code` is `ORM.OPERATION_UNSUPPORTED`. ```typescript const updated = await db.orm.users.where({ _id: '6650f1c2a1b2c3d4e5f60001' }).update((u) => [u.bio.unset()]); @@ -2001,10 +1998,10 @@ The callback argument implements `set`, `unset`, `inc`, `mul`, `push`, `pull`, ` #### Remarks -- MongoDB. `push()`, `pull()`, `addToSet()`, and `pop()` exist on the callback argument. `push()` and `pull()` are tested against a live MongoDB. `addToSet()` and `pop()` are covered only by unit tests that check the operator they build. -- `push(value)` appends one element. `addToSet(value)` appends it only if it is not already there. `pull(match)` removes every element equal to the argument, or every element matching a partial object. `pop(1)` removes the last element, and `pop(-1)` removes the first. -- Applied to a field that is not an array, MongoDB rejects the write with its own error, not a Prisma ORM error. An array field is declared with `[]` in your contract, as in `tags String[]`. -- One update cannot touch the same field twice, so the lines below are four separate writes. The example schema has no array field, so they assume `tags String[]` on `User`. +- MongoDB. `push()`, `pull()`, `addToSet()`, and `pop()` are available on the callback argument. `push(value)` appends one element. `addToSet(value)` appends it only if it is not already there. `pop(1)` removes the last element, and `pop(-1)` removes the first. +- `pull(match)` removes every element equal to the argument. If the array holds objects, pass part of an object to remove every element that matches it, as in `u.tags.pull({ kind: 'draft' })`. +- These operations need an array field, declared with `[]` in your contract, as in `tags String[]`. Applied to any other field, the write fails with an error from MongoDB rather than a Prisma ORM error code. +- The four lines below are separate calls because one update cannot apply two operations to the same field. One update can still change two different fields, such as pushing to `tags` and setting `bio`. The example schema has no array field, so these lines assume `tags String[]` on `User`. ```typescript await db.orm.users.where({ _id: '6650f1c2a1b2c3d4e5f60001' }).update((u) => [u.tags.push('admin')]); @@ -2057,11 +2054,12 @@ If you need the type by name, import it from `@prisma/orm-postgres/components/ru 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. -- Switching from one way to the other throws an error whose `code` is `RUNTIME.ITERATOR_CONSUMED`. The message contains `already been consumed`. Awaiting a result and then looping it with `for await`, or the reverse, is what throws. +- Switching from one way to the other throws an error whose `code` is `RUNTIME.ITERATOR_CONSUMED`. The message contains `already been consumed`. Awaiting a result and then looping it with `for await`, or the reverse, is what throws. There is no error class to test with `instanceof`: import `isRuntimeError` from the same module and check `error.code`. 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)). ```typescript +import { isRuntimeError } from '@prisma/orm-postgres/components/runtime'; const result = db.orm.public.User.all(); const first = await result; const again = await result.toArray(); // safe: same array @@ -2070,7 +2068,7 @@ try { for await (const user of result) { } } catch (error) { - if (error instanceof Error && 'code' in error && error.code === 'RUNTIME.ITERATOR_CONSUMED') { + if (isRuntimeError(error) && error.code === 'RUNTIME.ITERATOR_CONSUMED') { // the result was already read as an array } } @@ -2097,6 +2095,7 @@ These need `8.0.0-rc.10` or newer. A `contract.d.ts` emitted by an earlier relea ### Aggregate result shapes +- [Grouped aggregates](#grouped-aggregates) documents `aggregate()`, `groupBy()` with more than one field, and every function `agg` gives you. The bullets here cover only the shape of what you get back. - `aggregate((agg) => ({ ... }))` resolves to a **single object** keyed by your aliases. `db.orm.public.Order.aggregate((agg) => ({ total: agg.count() }))` resolves to one object, such as `{ total: 10 }`. -- `groupBy(...).aggregate((agg) => ({ ... }))` resolves to an **array**, one object per group. Each object carries the group key field or fields plus your aliases. `db.orm.public.Order.groupBy('customerId').aggregate((agg) => ({ orderCount: agg.count(), totalAmount: agg.sum('amount') }))` resolves to `[{ customerId, orderCount, totalAmount }, ...]`. -- `count()` is always a `number` (`0` over an empty set). `sum()`, `avg()`, `min()`, and `max()` carry `| null`: they resolve to `null`, not `0`, over an empty result set. +- `groupBy(...).aggregate((agg) => ({ ... }))` resolves to an **array**, one object per group. Each object carries the fields you grouped by plus your aliases. `db.orm.public.Order.groupBy('customerId').aggregate((agg) => ({ orderCount: agg.count(), totalAmount: agg.sum('amount') }))` resolves to `[{ customerId, orderCount, totalAmount }, ...]`. +- `count()` is always a `number` (`0` over an empty set). `sum()`, `avg()`, `min()`, and `max()` resolve to `null`, not `0`, over an empty result set, so their TypeScript type includes `null`. diff --git a/docs/orm-docs-audit/c21-orm-client/part1.round2.md b/docs/orm-docs-audit/c21-orm-client/part1.round2.md new file mode 100644 index 00000000000..6abed9bfbad --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-client/part1.round2.md @@ -0,0 +1,103 @@ +Read once, cold, as a Prisma ORM 7 user. Findings below. + +--- + +## Sentences I could not restate after one reading + +**Line 19 — "You write this schema in your contract, the `contract.prisma` file that replaced `schema.prisma`."** +Stopped by "contract". The sentence uses the word three ways in one line: a thing you write in, a file name, and later a `.json` and a `.d.ts`. I don't know whether "contract" is a file, a concept, or a directory. Plainer: "Write your schema in `contract.prisma`. This file replaces `schema.prisma` from ORM 7." + +**Line 19–20 — "run `npx prisma contract emit`. That command writes `contract.json` and `contract.d.ts`, the two files the examples below import."** +Writes them *where*? Next to `contract.prisma`? In my source folder? The examples import `'./contract.json'`, so apparently next to my code, but the page never says. + +**Line 187 — "`@@type("pg/text@1")` on an `enum` block stores the enum as a text column, with a check constraint that allows only the declared values."** +I cannot decode `pg/text@1`. Is `@1` a version? Are there other values (`pg/text@2`)? The MongoDB schema has `mongo/string@1`, which makes me think it's a registry of names I've never seen and the page doesn't list. + +**Line 188 — "A `native_enum` block is the other kind."** +"The other kind" of what — enum declaration syntax? And `native_enum` appears nowhere in the example schema, so I can't see what one looks like. This is the first of two places where the page names a thing and never shows it. + +**Line 190 — "`@@base(Task, "bug")` makes `Bug` a variant of `Task`: a model stored together with the model it is based on."** +Too many new ideas at once: `@@base`, "variant", "@@discriminator", and `variant('Bug')` all land in one bullet. I got roughly "subclassing", but I could not restate what is stored where until I read line 722, 500 lines later, which says a variant with `@@map` uses a second table. Those two facts belong together. + +**Line 367 — "the `_id` of a loaded related document comes back as a driver `ObjectId`, not as the hex string a document's own id field comes back as."** +Three clauses and two different `_id`s. Plainer: "A document's own `_id` comes back as a hex string. An `_id` on a related document loaded by `include()` comes back as an `ObjectId` object instead." + +**Line 416 — "They do not take a date or a timestamp: TypeScript rejects the field name, because the fields these two accept leave those columns out."** +The clause after "because" restates the clause before it and adds nothing I can use. Plainer: "TypeScript rejects a date or timestamp field name here." + +**Line 422 — "Each value must start from the collection the callback was given, which is why the example below writes `posts` inside its own callback."** +I could not restate this. "Start from the collection the callback was given" is abstract, and "writes `posts` inside its own callback" describes the example rather than the rule. Looking at the code, `posts.combine({ recent: posts.orderBy(...), total: posts.count() })` — `posts` referring to itself inside its own argument is genuinely confusing, and the prose made it worse, not better. + +**Line 504 — the whole `Temporal` note.** +One sentence carries: the return type, what `Temporal.Instant` is, a polyfill import, a global check, plus two methods that don't compile and why. Four separate facts. I stopped at "if your runtime has no global `Temporal`" — how do I know whether mine does? + +**Line 516 — "To sort a text-backed enum by meaning, store numbers rather than words, or write your own ranking expression."** +"Store numbers rather than words" — in the enum's `= "..."` values? Then `.eq('urgent')` becomes `.eq('3')`? The page told me on line 189 that the `=` string is what I pass to `.eq()`, so this advice silently breaks every other example. And "write your own ranking expression" is given no syntax at all. + +**Line 626 — "TypeScript rejects the call if you do not, but nothing checks at runtime. If you get past the type system, the cursor value is ignored and you get every row back, with no error."** +I understood the words but not why I am being told. I am not planning to get past the type system. The same construction appears three times (lines 368, 626, 690), and each time it reads as a note to the maintainers. + +--- + +## Words and phrases I had to guess at + +- **"collection"** — used constantly from line 246 on, never defined. I guessed "the chainable query object", but on MongoDB "collection" also means the actual Mongo collection (line 228, 723), so I was guessing which one was meant every time. +- **"contract"** — guessed: the new name for the schema plus its generated output. +- **"terminals"**, in the link `#read-terminals` — guessed: methods that end a chain and run the query. The visible text says "read method", so the anchor uses a word the page never does. +- **"`db.orm`"** — guessed that `orm` is a namespace on the client and that `db` has other namespaces. Never said. +- **"Refinements"** (line 408 heading) — guessed: the callback passed to `include()`. +- **"variant"** — guessed: subclass. Mostly right. +- **"text-backed enum"** / **"native enum"** — I worked these out from lines 187–188, but only because they are defined there. They are then used hundreds of lines later with no reminder. +- **"Project a row down to"** (line 325) — guessed "select fewer fields". +- **"`Uuid`"** — guessed it replaces `String @db.Uuid`. The page says it's "the type for a PostgreSQL `uuid` column", which does not tell me it's a new built-in scalar rather than something I import. +- **"`./contract.d`"** (lines 204, 232) — I assumed this is a typo for `./contract`. I have never imported from a path ending in `.d` and I would have spent time on this. +- **"`@prisma/orm-mongo/query-ast/execution`"** (line 312) — "query-ast/execution" reads like an internal module path that leaked into a public import. I would not trust it to be stable. + +--- + +## Places I asked "so what do I actually type?" + +1. **Installing anything.** The page imports `@prisma/orm-postgres/runtime`, `@prisma/orm-mongo/runtime`, and `@prisma/orm-extension-pgvector/runtime`, and runs `npx prisma contract emit`. There is no `npm install` line anywhere. As a v7 user I have `@prisma/client` installed; I don't know what to remove or add. +2. **Where `contract.prisma` lives and where the emitted files land.** Covered above. +3. **What happened to my v7 code.** I have two years of `prisma.user.findMany({ where: ... })`. The page never says `findMany` is now `.where(...).all()`, or that `PrismaClient` is now `postgres(...)`. One mapping table, or even one sentence, would have oriented me. Line 13 sends me to "Fundamentals guides" instead. +4. **Closing the client.** No `$disconnect` equivalent, no statement about whether `db` is a singleton I create once per process. +5. **A `native_enum` block.** Named on line 188, never shown. +6. **Any Postgres operator other than `.eq()`.** Line 267 says `where()` takes "a callback that calls an operator on a column". The only operator shown in this entire part is `.eq()`. Line 248 points at a section I don't have. +7. **Any MongoFieldFilter operator list.** I get `.eq` and `.gte`. Line 268 says "For anything else, such as greater-than, use `MongoFieldFilter`" — so what else is there? +8. **How the `PostTag` model connects.** Line 191 says "`PostTag` is required. A many-to-many needs the model for the join table, and a pair of list fields with no such model is rejected." But in the schema, `Post` has `tags Tag[]` and `Tag` has `posts Post[]`, and neither mentions `PostTag`. Nothing I can see links them. If I were writing my own many-to-many, I would not know what to type. +9. **`Customer` and `Order`.** Lines 466, 469, 474, 483 run examples against models the page explicitly says are shown somewhere else. I cannot run the `sum()`/`avg()` examples as written. Line 17 warns me, which does not help. +10. **What to do about MongoDB `select()`.** Line 331: "changes what the database returns but not the TypeScript type." So my type says `bio` exists and at runtime it does not. What am I supposed to do — cast? Pick fields by hand? The page states the problem and stops. +11. **`avgDecimal(field)` returns "the exact average ... as a decimal string."** Do I `Number(...)` it? Feed it to a decimal library? Which one? +12. **Custom `Collection` subclass** (line 224): "subclass `Collection` and register the subclass with the `orm(...)` function instead of calling `postgres(...)`." So I give up `postgres(...)` — do I still pass `url` and `contractJson`? Where does `Collection` come from? Deferred to a section I don't have, but the deferral is placed in the setup section where I'm actively trying to create a client. +13. **`distinct()` vs `distinctOn()`.** Both examples produce a value with no comment showing what came back. Every aggregate example has a `// result is ...` comment; these two don't, and they're the two I'd most need it for. + +--- + +## Places explaining internals when I only wanted to know what to do + +- Line 187: "with a check constraint that allows only the declared values." +- Line 188: "It creates a real PostgreSQL enum type with `CREATE TYPE ... AS ENUM`." +- Line 414, 625, 661, 689: "Calling one throws a `TypeError`." Told four times. I am not going to call a method that doesn't exist; telling me the JavaScript error class is for someone debugging the runtime. +- Line 416: "because the fields these two accept leave those columns out." +- Line 419: "Past ±(2^53 − 1) a `number` cannot hold the total." I know what a float is. The actionable half is "use `sumBigInt`". +- Lines 368, 626, 690: the three "if you get past the type system" notes. +- Line 722: "keeps its own fields in a second table alongside the base model's table." Storage layout, when the thing I wanted was "call `variant('Bug')`". +- Line 312: `query-ast/execution` in a public import path. + +--- + +## Could I do what the page is for, after one reading? + +Partly. I could chain `where`, `select`, `orderBy`, `limit`, `offset`, `distinct`, and `variant` on Postgres and produce a query that looks right, because the examples are concrete and the Postgres and MongoDB forms are clearly separated. The per-method structure (Remarks / Options / Return type / Examples) is easy to skim. + +What I would still not know: + +- **How to get to the point where any of this runs.** No install step, no file locations, no statement of what replaces my existing `PrismaClient` setup. I would be stuck before the first example. +- **Whether `./contract.d` is literally what I type.** +- **Any filter operator beyond `.eq()` on Postgres.** That is the single most common thing I do, and this part of the page cannot answer it. +- **How to declare a many-to-many**, because `PostTag` is asserted to be required and is not wired to anything visible. +- **What `combine()` actually requires.** I could copy the example. I could not write a different one. +- **What a `native_enum` block looks like**, despite the page using the native-vs-text distinction to explain sort order. +- **Whether "collection" means the query builder or the Mongo collection** at any given mention. + +The page assumes I already know what a "contract" and a "collection" are. It defines the schema attributes carefully on lines 184–192 and then never gives the same treatment to the two words it leans on hardest. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-client/part2.round2.md b/docs/orm-docs-audit/c21-orm-client/part2.round2.md new file mode 100644 index 00000000000..0c782f52d80 --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-client/part2.round2.md @@ -0,0 +1,87 @@ +# Reader review: `wip/c21/r2/part2.mdx` + +I read it once, top to bottom, as a Prisma ORM 7 user. + +## Sentences I could not restate after one reading + +**1.** `"On MongoDB, count with the pipeline builder."` (line 5) +Stopped by: "the pipeline builder" is a proper noun I have never seen. It has no link and no definition anywhere on this part. The link that follows goes to a task page for both databases, so I cannot even tell whether I need an import. +Plainer: say what it is, or link the words "pipeline builder" itself. + +**2.** `"To register a subclass, build the client with the `orm(...)` function and pass a `collections` object. The client's own `.orm` cannot do this: `postgres(...)` has no `collections` option, so `db.orm` always uses the plain `Collection`."` (line 128) +Stopped by: three ideas in one bullet, and it depends on my knowing that the `db.orm` I have used on every previous example came out of `postgres(...)`. Part 1 never told me that. +Plainer: "`db.orm` cannot use a subclass. Build a second accessor with `orm({ runtime, context, collections })` and use that instead." + +**3.** `"Once the type check is bypassed, `update()` and `delete()` change one arbitrary row, and `updateAll()` and `deleteAll()` change every row."` (line 172) +Stopped by: "Once the type check is bypassed" — bypassed how? By me writing `as any`? By calling from JavaScript? The page never says, so I cannot tell whether this warning applies to me at all. "One arbitrary row" is frightening and unexplained: arbitrary by what rule? + +**4.** `"It collects the matching ids, updates, then reads those documents back. A document someone else changes in between can come back with their values, and a document that starts matching only after the first step is updated but not returned."` (line 424) +Stopped by: the last clause contradicts the first. If step one collects ids, a document that only starts matching *later* is not in that id list, so how does it get updated? Either the update runs by filter rather than by id, or the sentence is wrong. I could not resolve it. + +**5.** `"Only the column name is read: it names the unique constraint the database checks. The value is required by the type but is not used, so it does not have to match the value in `create`."` (line 603) +Stopped by: it tells me the value is ignored, then both examples pass a value that carefully matches. So what do I type? The text and the examples pull in opposite directions. + +**6.** `"Every matching row is deleted before the first row reaches you. Stopping the loop early still leaves nothing behind."` (line 540) +Stopped by: "leaves nothing behind" is ambiguous on first read — nothing deleted, or nothing remaining? I had to read it twice to land on "no matching rows survive". + +**7.** `"On MongoDB, the `_id` on the returned row is decoded the same way as on a read, so you get a hex string, not the driver's `ObjectId`."` (line 607) +Stopped by: "decoded" describes machinery. The fact I need is "`_id` comes back as a string". + +## Words and phrases I had to guess + +- **"shorthand object"** (lines 70, 79). Guessed: a plain object of field/value pairs meaning equality. Never defined. +- **"domain methods"** (line 123). Guessed: methods I write myself. +- **"multi-table inheritance variant, a variant stored in its own table"** (line 304). Guessed: some variants live in the base table and some get their own. Part 1 told me variants are "stored with its base model", so this is the opposite and nothing tells me how to declare or recognise which kind I have. "MTI" then appears only inside a quoted error message. +- **"field operations"** (lines 350, 361). Guessed from the example `p.content.set(...)` that these are per-field setter calls. +- **"Single consumption and mode switching"** (line 18). Guessed: awaiting versus `for await`. +- **`AsyncIterableResult`** (lines 16, 260). Guessed correctly from the bullet, but the type itself is never defined in this part. +- **`Collection`** (line 141). Guessed the second parameter is the model name. Two unexplained type parameters. +- **`@prisma/orm-postgres/runtime` versus `@prisma/orm-postgres/orm-client`** (lines 136-137). Guessed which exports come from which only by reading the import lines. + +## Places I asked "so what do I actually type?" + +1. **Counting on MongoDB** (line 5). No snippet, no import, no method name. +2. **Updating a variant-only field on MongoDB.** Line 351 says `duration` on `Tutorial` "is not in it" and that "[Field update operations] shows the workaround" — but never says what the workaround is. That is the one thing I came to this bullet for. +3. **Safe bulk update on MongoDB** (line 425): `"run the writes yourself in a MongoDB driver session"`. No code, and no hint how to get a driver session out of this ORM. The link is to a page that may or may not answer it. +4. **The `data` option name.** Every Options table says the argument is named `data` (lines 194, 266, 312, 361, 431, 465). Every example passes the fields directly, and line 246 says the migration "drops the `data` wrapper". On the tables alone I would have typed `create({ data: { ... } })` and got a type error. +5. **Do I pass `id` on PostgreSQL creates or not?** Line 184 says leave out fields with a default "including the id". Then lines 226, 230, 239, 630, 637 all pass hand-written UUIDs. I cannot tell whether the id has a default, whether passing it is allowed, or whether I am expected to generate UUIDs myself. +6. **`conflictOn` value.** Per point 5 above: any value? The matching value? I would have to test. +7. **Calling `where()` before `upsert()` on PostgreSQL** (line 602): `"where() is not used"`. Does it throw, or is it silently ignored? I do not know whether my existing chain is safe. +8. **The `select`/`include` chain before `.update()`** (lines 400-405). It appears with no comment. I cannot tell whether it shapes the returned row, restricts what is written, or is decoration I can delete. +9. **Which value wins on insert in an upsert.** Line 605 says the `update` value wins "even when the row is inserted" — but that bullet is labelled MongoDB, and the PostgreSQL example at line 628 comments `"the create side is used"`. Opposite behaviours, and Postgres's is never stated as a rule. +10. **Two different `orderBy` syntaxes** appear side by side at lines 45 and 51 (`(u) => u.email.asc()` versus `{ createdAt: 1 }`) with no note that they differ by database. +11. **Coming from ORM 7, what replaces `findUniqueOrThrow` / `findFirstOrThrow` / `count()` / `deleteMany` return shape?** Line 114 maps `findUnique` and `findFirst` onto `first()` but says nothing about the throwing variants, and does not say whether `first()` still enforces uniqueness. +12. **Which `db` am I holding?** Line 154 builds `const db = orm({...}).public` and then calls `db.Task.bugs()`. Everywhere else it is `db.orm.public.User`. Nothing says the two are the same kind of thing or whether I now keep two clients. + +## Places that explain the machinery when I only wanted the instruction + +- Line 128: `"The client's own `.orm` cannot do this: `postgres(...)` has no `collections` option"` — internal reason for a restriction. +- Line 158 comment: `"client.context carries the contract, which orm() needs to build collections."` — I only need to know I must pass it. +- Line 186: `"It does not read the stored document back."` +- Line 354: `"On MongoDB, `update()` changes one document in a single database operation."` Floating fact with no consequence stated here. +- Line 355 with line 352: `"it also returns `null` when the data you pass has no fields to change, which is what happens if you pass a function by mistake."` This is a defect explained twice, in mechanism terms. The instruction is one line: on PostgreSQL, pass an object. +- Line 424: the three-step description of `updateAll()`. +- Line 603: `"it names the unique constraint the database checks"`. +- Line 607: `"is decoded the same way as on a read"`. + +## Two things that read as errors + +- Line 606: the `upsert()` failure message says `"Use createAll() instead."` `createAll()` does not upsert. Either the message is wrong or the page is quoting it wrong. +- Line 172 quotes a MongoDB error that names `updateAll()` specifically, but the warning covers `update`, `delete`, `updateAll`, and `deleteAll`. I cannot tell whether the runtime check exists for all four. + +## Could I do what the page is for after one reading? + +Partly. + +I could confidently write: `all()`, `first()`, `create()`, `createAll()`, `createAndCount()`, `update()`, `delete()` and their `-All`/`-AndCount` forms on PostgreSQL, plus the plain MongoDB equivalents. The ORM 7 diff blocks are the most useful thing on the page and did most of that work. + +I would still not know: + +- How to count on MongoDB. +- How to update a field that only exists on a variant, on MongoDB. +- Whether my variant is the "multi-table inheritance" kind, so whether `createAndCount()` and `upsert()` work at all. +- Whether to pass `id` on PostgreSQL creates. +- What value to put in `conflictOn`, and which side wins on insert in a PostgreSQL upsert. +- How to register a `Collection` subclass without copying the example blind — specifically what `context` is, whether I now have two clients, and why my existing `db.orm` cannot be used. +- How to do a safe bulk update on MongoDB. +- What `AsyncIterableResult` is, beyond "await it or loop it". \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-client/part3.round2.md b/docs/orm-docs-audit/c21-orm-client/part3.round2.md new file mode 100644 index 00000000000..2dd578344f6 --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-client/part3.round2.md @@ -0,0 +1,85 @@ +Read once, top to bottom, as a Prisma ORM 7 user. + +## Sentences I could not restate after one reading + +**Line 55** — "It also gives you `countBigInt`, `sumBigInt`, and `avgDecimal`, which answer with a `bigint` or an exact decimal instead of a `number`." +Stopped by: "answer with" (I think you mean "return"), and "an exact decimal" — is that a JS `Decimal` class? Whose? Do I have to import it? Also three names mapped onto two return types, so I cannot tell which is which. Plainer: "`agg.countBigInt()` returns a `bigint`. `agg.sumBigInt('amount')` returns a `bigint`. `agg.avgDecimal('amount')` returns a `Decimal` (imported from X)." + +**Line 129** — "`orderBy(...)` on a grouped collection sorts the groups by a grouping field. You cannot sort them by an aggregate value here. For that, use the SQL query builder, described in [Advanced queries]." +Stopped by: "here" — here meaning in `orderBy`, or in grouped queries generally? And "the SQL query builder" is a thing I have never heard of; it is named as if I already know it. Plainer: "You can only sort groups by a field you grouped on. To sort by a computed total, drop down to raw SQL (link)." + +**Line 234** — "Filters build the condition inside `where()`, and inside relation filters." +Stopped by: this defines filters using the word "filters". It also implies two contexts without showing the second yet. + +**Line 237** — "MongoDB uses static factories on `MongoFieldFilter` imported from `@prisma/orm-mongo/query-ast/execution`, plus `.and()`/`.not()` instance methods and the standalone `MongoOrExpr.of(...)`." +Stopped by: too many ideas in one sentence (import path, static factories, instance methods, a third helper), plus "static factories" is library-author vocabulary. The import path `query-ast/execution` reads like an internal module I was not supposed to see. + +**Line 245** — "`eq`/`neq`/`in`/`notIn` on any field that can be compared for equality; `gt`/`lt`/`gte`/`lte` on any field that can be ordered" +Stopped by: I cannot resolve "can be compared for equality" or "can be ordered" to actual Prisma types. Is `Json` orderable? Is `Bytes` equality-comparable? I have to guess and find out at compile time. Plainer: give the list of types, or say "if the method is missing from autocomplete, the type does not support it." + +**Line 364** — "A to-one relation is filtered the same way, from either side: `p.user.some(...)`." +Stopped by: two things. `some` means "at least one of many" everywhere else on the page, so using it for a single related row is surprising and the sentence does not explain why. And "from either side" — either side of what? The relation? So `u.posts.some()` and `p.user.some()` are both fine, which I already assumed. This sentence is doing work I could not follow. + +**Line 197** — "Each of those returns an object with `eq`, `neq`, `gt`, `lt`, `gte`, and `lte`." +Stopped by: "returns an object with" describes the library's internal shape when I only need "then call a comparison on it: `h.sum('amount').gt(1000)`". + +## Words and phrases I had to guess + +- **"pipeline builder"** (line 3) — guessed: some other API that exposes MongoDB's native aggregation pipeline. The page never shows a single line of it, so this is a pure guess. +- **"static factories"** (line 237) — guessed: methods you call on the class itself, `MongoFieldFilter.eq(...)`, not on an instance. +- **"selector"** (line 53) — guessed: the callback that names which aggregates to compute. +- **"output alias"** (line 53) — guessed: the object key becomes the property name in the result. +- **`GroupedCollection`** (line 127) — guessed: an unresolved, chainable thing that is not a promise yet. The page never says it is not awaitable, so I guessed that too. +- **`Uuid`** (schema, lines 13, 23) — guessed: a new scalar type replacing `String @db.Uuid`. +- **"exact decimal"** (line 55) — guessed: arbitrary-precision decimal, not a float. +- **`MongoAndExpr`** (line 298) — named once, never constructed, never shown. Guessed it exists but I would not know how to make one. +- **"the SQL query builder"** (line 129) — guessed: raw-SQL escape hatch. +- **`.desc()`** (line 178) — guessed: descending sort. Never defined on this page. +- **"to-many relation"** (line 359) — I know this from ORM 7, but the page uses it before defining it. + +## Where I asked "so what do I actually type?" and got no answer + +1. **`countBigInt` / `sumBigInt` / `avgDecimal`** (line 55). No example, no signature. Does `countBigInt` take a field? Do I need to import a `Decimal` type to hold the result? Nothing. +2. **MongoDB aggregation.** Line 3 tells me `aggregate()` and `groupBy()` throw a `TypeError` and sends me away. The whole Mongo half of "how do I count my documents" is a link. One three-line example would have answered it. +3. **MongoDB comparison methods.** PostgreSQL gets a full table (lines 251–257): `gt`, `like`, `in`, `isNull`, all of it. MongoDB gets exactly one method, `MongoFieldFilter.eq`, used in every example. How do I write "amount greater than 100" on Mongo? How do I write "name in this list"? The page does not say the methods exist or do not exist. +4. **`having()` with more than one condition.** Every example has exactly one comparison. Can I AND two? Does `and(...)` from `@prisma/orm-postgres/orm-client` work inside `having`? No answer. +5. **`having((h) => h.count(...))` with a field.** `aggregate` has both `count()` and `count('quantity')`, but line 197 lists only `count()` for `having`. Is `h.count('quantity')` legal? I cannot tell whether the omission is deliberate. +6. **Chain order.** Line 130 says `limit`/`offset` require `orderBy` first. But the example at line 177 goes `groupBy → orderBy → limit → aggregate`, while line 168 goes `where → groupBy → having → aggregate`. Where does `having` go relative to `orderBy`? Where does `limit` go relative to `having`? I would guess and recompile. +7. **Is `and()` variadic?** Line 309 shows `and(a, b)`. I need `and(a, b, c)` constantly. The page shows two arguments and says nothing. +8. **Chaining three Mongo filters.** `.and(other)` takes one argument. For three conditions do I write `a.and(b).and(c)`? Probably, but the page never shows it. +9. **Awaiting a `GroupedCollection`.** Line 142 says "resolved via `aggregate()`". So is `await ...groupBy('customerId')` an error, or does it return the groups without aggregates? Not said. +10. **`p.embedding.isNull()`** (line 287). `embedding` is not in either schema on this page. I cannot see its type, so I cannot tell what `isNull` means for it. +11. **Grouping-field types in the result.** Line 153 says each row is `{ customerId, orderCount, totalAmount }`. Are the grouping fields typed, or is this `Record`? Matters immediately in TypeScript. + +## Where the page explains internals when I only wanted to know what to do + +- **Line 47** — "This is standard SQL: aggregating zero rows yields `NULL`. Their TypeScript return type carries `| null` for this reason." The rule ("`sum` can be `null`; handle it") is enough. The SQL-standard justification and the reasoning about why the type is what it is are backstory. +- **Line 359** — "they compile to SQL `EXISTS` subqueries." I do not need to know the generated SQL to filter users by their posts. +- **Lines 44, 125, 196** — "Calling `aggregate()` on a Mongo collection throws `TypeError`", repeated three times. Telling me the exception class is telling me how the library is built. "Not available on MongoDB" is the fact I need, and once is enough. +- **Line 237** — the import path `@prisma/orm-mongo/query-ast/execution`. "query-ast" is the library's internal architecture surfacing in something I have to type. +- **Line 197** — "returns an object with `eq`, `neq`, ..." as described above. +- **Lines 59–61, 138–142, 208–212** — the "Return type" tables. "Object of aggregate results" and "A grouped collection, resolved via `aggregate()`" restate the example rather than telling me anything I can act on. + +## Two structural problems + +**The `having()` example appears before `having()` is documented.** The heading at line 165 says "Filter rows, then group (PostgreSQL)", but the code at line 168 contains `.having((h) => h.count().gt(1))`. I hit an undefined method under a heading that does not mention it, and the explanation is 25 lines further down. Either drop `having` from that example or move the section. + +**The `all` name collision.** Line 297 flags it honestly: "This `all()` is not the `.all()` you call at the end of a chain to run the query." I still had to read line 320, `where(() => all()).all()`, twice. The note does not rescue the design; it documents that the design will confuse me. + +## Could I do what the page is for, after one reading? + +**On PostgreSQL, mostly yes.** I could write `aggregate`, `groupBy`, `having`, the scalar comparisons, `and`/`or`/`not`, and `some`/`every`/`none`. The examples are concrete and I can copy them. + +**On MongoDB, no.** I would be stuck as soon as I needed anything other than equality. The page gives me `MongoFieldFilter.eq` and nothing else, gives me no aggregation at all, and points me off the page for the part I actually came for. + +What I would still not know after one reading: + +- Any MongoDB comparison other than `eq`, and whether they exist. +- How to aggregate or group on MongoDB. +- How to call `countBigInt`, `sumBigInt`, or `avgDecimal`, or what type `avgDecimal` gives me. +- The legal order of `where`, `groupBy`, `having`, `orderBy`, `limit`, `aggregate` in a chain. +- Whether `and()` takes more than two conditions. +- Whether `having()` can hold more than one condition. +- Which field types support which comparison methods. +- How to sort groups by a computed total (I know I cannot do it here; I do not know how to do it at all). +- What `MongoAndExpr` is for or how to build one. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-client/part4.round2.md b/docs/orm-docs-audit/c21-orm-client/part4.round2.md new file mode 100644 index 00000000000..611d51daf1c --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-client/part4.round2.md @@ -0,0 +1,98 @@ +Read once, top to bottom, as a Prisma ORM 7 user. Findings below. + +## Sentences I could not restate after one reading + +**Line 3** — "Static factories that build MongoDB filter expressions." +Stopped by: "static factories" and "filter expressions." I know what a static method is, but I do not know what a "filter expression" is in this product, or why I would build one instead of passing a plain object to `.where()`. The page never says what problem these solve. Plainest wording I would have understood: "Helpers for filters you cannot write as a plain object, such as comparisons, `in`, and paths into embedded objects." + +**Line 7** — "Imported from `@prisma/orm-mongo/query-ast/execution`." +Stopped by: `query-ast`. That is your internal module layout. I have to type it, so I accept it, but nothing tells me what it means or whether there are other things in it I should know about. + +**Line 10** — "`isNull` / `isNotNull` are shorthand for equals-null and not-equals-null, not a `$exists` check." +Stopped by: this assumes I already know that in MongoDB a field set to `null` and a field that is absent are different things, and that equals-null matches both. The sentence tells me the two are not the same but not what the difference does to my results. Plainer: "`isNull` matches documents where the field is `null` and documents where the field is missing. To test only whether the field is present, use `MongoExistsExpr`." + +**Line 12** — "The factories `exists`, `regex`, `elemMatch`, `all`, and `size` do **not** exist on `MongoFieldFilter`." +Stopped by: it lists `exists` as missing one line after line 11 told me to use `MongoExistsExpr.exists`. On one reading those two lines fight each other. Also I do not know what `elemMatch` is, so "does not exist" tells me nothing I can act on. + +**Line 20** — "`gt` / `lt` / `gte` / `lte` `(field, value)` | Field + ordered value | Ordered comparisons." +Stopped by: "ordered value" and "ordered comparisons" say the same vague thing twice and neither says what types are allowed. The row is also mangled: the argument list sits outside the code span for three of the four names. Plainer: "A value that can be compared, such as a number, a date, or a string." + +**Line 65** — "`MongoWhereFilter` is typed against the model's own top-level field names. `{ 'address.city': ... }` is therefore a compile error without a cast." +Stopped by: `MongoWhereFilter` appears here for the first time and is never defined. I guessed it is the type of the plain object I pass to `.where()`. Two ideas in one sentence: what the type allows, and what breaks because of it. + +**Line 86** — the whole paragraph. Too many ideas in one block: +- "The callback receives one argument, written `u` or `t` below" — I stopped to work out whether `u` and `t` mean different things. They do not; they are just variable names in different examples. Saying it this way made me look for a distinction. +- "The argument is also a function, so `u('address.city').set('San Francisco')` reaches a field inside an embedded object." — one value being both an object with properties and a callable function is surprising. The page states it and moves on. I would have understood: "Call the argument as a function when the field is inside an embedded object: `u('address.city').set(...)`. Use the property form for top-level fields: `u.bio.set(...)`." +- "Each call returns an update instruction; collect them in the array you return." — "update instruction" is your internal name for the thing. As a reader I only need "return an array of the calls you made." + +**Line 88** — "`updateAll()` returns the changed documents." +Stopped by: line 172 later says `updateAll()` returns an `AsyncIterableResult`. On first reading these look like two different answers. Line 88 should say what the result type is, or say nothing. + +**Line 100** — "The callback argument is typed against the collection's base model, even after `variant(...)`. TypeScript only knows the base model's fields, so `t.duration.inc(10)` is a type error even though it runs correctly. Put `// @ts-expect-error` on that line to accept it." +Stopped by: this is an explanation of a limitation in your type generation, told from the inside. I only wanted to know what to type. Also "on that line" is ambiguous — the error is inside the callback but the example puts the comment above `.update(`. I could not have guessed that placement from the sentence. + +**Line 135** — "That callback cannot use a dot path, and `upsert()` rejects one." +Stopped by: "rejects" — at compile time or at runtime? If at runtime, what error do I catch? The page gives an error code elsewhere (`RUNTIME.ITERATOR_CONSUMED`) so I expected one here. + +**Line 156** — "`addToSet()` and `pop()` are covered only by unit tests that check the operator they build." +Stopped by: this is about your test suite, not about my code. I do not know what I am supposed to do with it. Am I being told not to trust them? + +**Line 159** — "One update cannot touch the same field twice, so the lines below are four separate writes." +Stopped by: I read the first half as a rule I need to obey, then the second half revealed it is an excuse for how the example is formatted. Plainer: "These four lines are separate calls because a single update cannot apply two operations to the same field." + +**Line 209** — "`sum()`, `avg()`, `min()`, and `max()` carry `| null`." +Stopped by: "carry `| null`" is shorthand for "their TypeScript type includes `null`." I worked it out, but it reads like notation, not a sentence. + +## Words and phrases I had to guess + +- **"static factories"** (line 3) — guessed: methods on the class you call without creating an instance. +- **"filter expressions"** (line 3) — guessed: the object the query builder actually sends to MongoDB. +- **"query-ast"** (line 7) — guessed: abstract syntax tree, your internal query representation. +- **"raw MongoDB operator string"** (line 9) — guessed: a literal like `'$regex'`, quoted, dollar sign included. The example confirmed it, the sentence did not. +- **"ordered value"** (line 20) — guessed: a number, date, or string. +- **`MongoWhereFilter`** (line 65) — guessed: the type of the plain object form of `.where()`. +- **"update instruction"** (line 86) — guessed: whatever `u.bio.set('x')` returns; an opaque value I just put in the array. +- **"the collection's base model"** (line 100) — guessed: the model named by `@@base`, from the earlier part of the page. +- **"the group key field or fields"** (line 208) — guessed: you can pass more than one field to `groupBy`, though nothing shows that. +- **"Options"** (line 14, heading) — guessed: this is a methods table, not options. The heading is wrong. + +## Places I asked "so what do I actually type?" and got no answer + +1. **How do I combine two filters?** This is the largest gap. Every example has exactly one condition. I cannot find out how to write "name is Alice AND createdAt after X" with `MongoFieldFilter`. Do I pass an array? Call `.where()` twice? Is there an `and`/`or` factory? The page does not say, and I need this in almost every real query. +2. **Can I mix a plain object and a `MongoFieldFilter` in the same `.where()`?** Never addressed. +3. **`MongoExistsExpr`** — line 11 names two methods. Does it have others? Does it combine with `MongoFieldFilter`? No `#### Options` table for it, unlike `MongoFieldFilter`. +4. **Regex options.** `of('title', '$regex', '^Hello')` is shown. How do I pass case-insensitivity (`$options: 'i'`)? The `of` signature takes one value, so I cannot see where it goes. +5. **`pull(match)`** — line 157 says it removes "every element matching a partial object." No example of the partial-object form. I do not know what to type. +6. **What happens if I omit `.where()`?** Line 88 says all four methods "require" it. Compile error or runtime throw? Not said. +7. **PostgreSQL increment.** Line 86 tells me PostgreSQL has no callback form and links to `update()`. It never says what I do instead. If I am on PostgreSQL, I have just read a long section that does not apply and got no replacement. +8. **Two casts.** Line 81 is `as unknown as Record`. Why two? A reader who tries the single cast first will fail and not know why. +9. **`@ts-expect-error` placement.** The example shows the comment above `.update(`, but line 100 says "on that line." Which line, when the call is chained across four lines? And this comment fails the build if you ever fix the type, which the page does not warn about. +10. **Catching the consumed-iterator error.** Line 197 does `error instanceof Error && 'code' in error && error.code === ...`. Is there an exported error class I could `instanceof` instead? That is the first thing I would ask, and the page does not say. +11. **Aggregate functions.** Line 207 uses `agg.count()` and `agg.sum('amount')`. There is no list of what `agg` offers; `avg`, `min`, `max` only appear incidentally in line 209. No signature for any of them. +12. **`groupBy` with more than one field.** Line 208 implies it is possible ("field or fields") but every example passes one string. +13. **Array ops in one call.** Line 159 says one update cannot touch the same field twice. Can I push to `tags` and set `bio` in one call? Presumably yes, but after that warning I would not risk it without an example. + +## Places that explain the internals when I only wanted to know what to do + +- **Line 8** — "Exactly eleven factories exist... There is no `ne`; use `neq`." The count is an inventory note for whoever audited the API. I only need the list and the correction. +- **Line 12** — reciting five factories that do not exist, three of which (`elemMatch`, `all`, `size`) I had not heard of. +- **Line 64** — "the TypeScript types do not know nested paths, so the object form needs a cast." This is why your types are limited. The instruction is the last clause: use `MongoFieldFilter.eq`. Lead with it. +- **Line 100** — an explanation of how `variant()` interacts with generated types. What I need is the one line about `@ts-expect-error`. +- **Line 156** — your unit-test coverage. +- **Line 158** — "MongoDB rejects the write with its own error, not a Prisma ORM error." Telling me whose error it is, without showing me the error, does not help me handle it. +- **Line 152** — the heading anchor is `[#array-operations-mongodb-unverified]`. The word "unverified" is an internal status note and it will show up in the page URL. A reader who lands on that link will wonder what is unverified. + +## Could I do what the page is for, after one reading? + +Partly. I could write a single-condition MongoDB filter (`MongoFieldFilter.eq`, `gt`, `in`, `isNull`) and I could write a single-field update (`set`, `inc`, `push`). Those parts have complete examples I can copy. + +What I still would not know: + +- How to write a filter with more than one condition. This alone blocks most real work. +- What `MongoFieldFilter` is for at all, versus just passing `.where({ name: 'Alice' })`. The page never compares them, so I do not know when to reach for it. +- How to do any of this on PostgreSQL. Roughly half the page is marked "MongoDB" and the PostgreSQL path is a link. +- What `agg` supports, and how to group by more than one field. +- Whether `addToSet` and `pop` are safe to use, because line 156 raised a doubt and did not resolve it. +- What to catch when `upsert()` "rejects" a dot path, or when MongoDB rejects an array operation on a non-array field. + +One more structural note: the table under `#### Options` is a list of methods, and the same heading pattern (`Remarks` / `Options` / `Examples`) is applied to sections that are not API members at all, such as "Dot-notation into an embedded object." Reading top to bottom, I expected `Options` to mean configuration and kept being wrong. \ No newline at end of file From 3325c100c11a591116ea150199abff912c0dc3d8 Mon Sep 17 00:00:00 2001 From: willbot Date: Sat, 12 Sep 2026 10:05:31 +0200 Subject: [PATCH 06/11] docs(orm8): orm-client reference, reader-review round three applied Wording round with a no-growth budget (2,056 to 2,049 lines). Answers readers asked for, each looked up in rc.9: native_enum syntax shown, the @@base discriminator value, the many-to-many rule as an instruction, counting on MongoDB with db.query, firstOrThrow(), conflictOn on several columns, ORM.WHERE_MISSING includes upsert(), is/isNot mapped onto some()/none(), Json has no equality comparison so the shorthand object throws ORM.FILTER_UNSUPPORTED, precise-number aggregates shown, sorting groups by an aggregate via db.sql. Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- .../content/docs/orm/reference/orm-client.mdx | 362 ++++++++---------- .../c21-orm-client/part1.round3.md | 92 +++++ .../c21-orm-client/part2.round3.md | 76 ++++ .../c21-orm-client/part3.round3.md | 75 ++++ .../c21-orm-client/part4.round3.md | 86 +++++ 5 files changed, 484 insertions(+), 207 deletions(-) create mode 100644 docs/orm-docs-audit/c21-orm-client/part1.round3.md create mode 100644 docs/orm-docs-audit/c21-orm-client/part2.round3.md create mode 100644 docs/orm-docs-audit/c21-orm-client/part3.round3.md create mode 100644 docs/orm-docs-audit/c21-orm-client/part4.round3.md diff --git a/apps/docs/content/docs/orm/reference/orm-client.mdx b/apps/docs/content/docs/orm/reference/orm-client.mdx index 48c253d1d0a..30f7b90ffbc 100644 --- a/apps/docs/content/docs/orm/reference/orm-client.mdx +++ b/apps/docs/content/docs/orm/reference/orm-client.mdx @@ -8,13 +8,11 @@ metaDescription: 'Reference for the Prisma ORM client''s query, mutation, filter The ORM client gives you model-level methods for reading and writing data across PostgreSQL and MongoDB. This page documents every method, its availability on each database, and the behavior that differs between the two. Each method's Remarks say which databases it works on, how it behaves differently on one of them, and where TypeScript rejects something the database itself would accept. -For task-oriented walkthroughs, see the Fundamentals guides: [Reading data](/orm/fundamentals/reading-data), [Writing data](/orm/fundamentals/writing-data), [Relations and joins](/orm/fundamentals/relations-and-joins), and [Transactions](/orm/fundamentals/transactions). If you are coming from Prisma ORM 7, [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7) lists the packages to install and maps `PrismaClient` and `findMany` onto the methods on this page. +This page assumes a project where `prisma orm init` has already run and your tables exist: [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7) has the packages to install, `prisma orm init`, and `prisma db update`, which replaces `prisma migrate`, and [How migrations work](/orm/migrations/how-migrations-work) creates the tables. For task-oriented walkthroughs, see the Fundamentals guides: [Reading data](/orm/fundamentals/reading-data), [Writing data](/orm/fundamentals/writing-data), [Relations and joins](/orm/fundamentals/relations-and-joins), and [Transactions](/orm/fundamentals/transactions). ## Example schema -All examples on this page run against the schema below. The grouped-aggregate examples use a separate `Customer` / `Order` schema, shown under [Grouped aggregates](#grouped-aggregates). - -Write this schema in `contract.prisma`, the file that replaces `schema.prisma` from Prisma ORM 7. This page calls that file your contract. After every change to it, run `npx prisma contract emit`. That command writes `contract.json` and `contract.d.ts` next to `contract.prisma`, and the examples below import both. 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). Your schema now lives in `contract.prisma`, the contract, which replaces `schema.prisma` from Prisma ORM 7. 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/`.
@@ -182,16 +180,25 @@ Some parts of the schema the examples rely on: - `@@map("user")` sets the table or collection name. Without it, the table name is the model name with a lowercase first letter. - On MongoDB, `id ObjectId @id @map("_id")` renames the field to `_id` everywhere: you filter on `_id`, and the returned document has an `_id` key. That is why the MongoDB examples never say `id`. - `Uuid` is a built-in type for a PostgreSQL `uuid` column. You do not import it. `@default(uuid())` fills the id in when you insert a row. -- `@@type("pg/text@1")` on an `enum` block stores the enum as a text column. The name inside is a PostgreSQL type plus a version, and the version is always `@1` today. The MongoDB form of the same attribute is `@@type("mongo/string@1")`. This page calls an enum written this way a text-backed enum. -- A `native_enum` block stores the same members as a real PostgreSQL enum type instead. Write `native_enum` where you would write `enum`, and give every member a value, as in `Urgent = "urgent"`. A member with no value is rejected. This page calls an enum written this way a native enum. The two sort differently; see [`orderBy()`](#orderby). +- `@@type("pg/text@1")` on an `enum` block stores the enum as a text column. Always write it exactly like that. Other versions do not exist yet. The MongoDB form of the same attribute is `@@type("mongo/string@1")`. This page calls an enum written this way a text-backed enum. +- A `native_enum` block stores the same members as a real PostgreSQL enum type instead. Give every member a value. A member with no value is rejected. This page calls an enum written this way a native enum. The two sort differently; see [`orderBy()`](#orderby). + + ```prisma + native_enum Priority { + Low = "low" + High = "high" + Urgent = "urgent" + } + ``` + - An enum member's stored value is the string after `=`, or the member name when there is no `=`. That string is what you pass to `.eq()`. So `Urgent = "urgent"` is matched by `.eq('urgent')`, and a bare `admin` member is matched by `.eq('admin')`. -- A variant is a model that reuses another model's fields and rows. `@@base(Task, "bug")` makes `Bug` a variant of `Task`. `@@discriminator(type)` names the column that records which variant each row is. `variant('Bug')` narrows a collection to that variant. Pass the variant's model name, not the string in `@@base`. -- `PostTag` is required. `Post.tags Tag[]` and `Tag.posts Post[]` are joined through `PostTag` because `PostTag` has a foreign key to each of the two models and its `@@id` lists exactly those two foreign key columns. A pair of list fields with no such model is rejected. See [Relations and joins](/orm/fundamentals/relations-and-joins#many-to-many). -- `Embedding1536` comes from the pgvector extension. See [PostgreSQL](#postgresql) for the extra line it needs when you create the client. +- A variant is a model that reuses another model's fields and rows. `@@base(Task, "bug")` makes `Bug` a variant of `Task`. Its second argument, `"bug"`, is the value stored in the discriminator column for a `Bug` row. `@@discriminator(type)` names that column. `variant('Bug')` narrows a collection to that variant. Pass the variant's model name, not the string in `@@base`. +- `PostTag` is required. To join `Post.tags Tag[]` and `Tag.posts Post[]`, write a third model with one foreign key to each side and an `@@id` of exactly those two columns. Prisma ORM finds it for you, and neither `Post` nor `Tag` names it. A pair of list fields with no such model is rejected. See [Relations and joins](/orm/fundamentals/relations-and-joins#many-to-many). +- The `types` block gives a name to a type from an extension package, so your models can use that name. `Embedding1536` comes from the pgvector extension. See [PostgreSQL](#postgresql) for the extra line it needs when you create the client. ## Setting up the client -Create a client with `postgres(...)` or `mongo(...)`, then read and write your models through `db.orm`. `db.orm` is where your models are. The same `db` also has a query builder, `db.sql` on PostgreSQL and `db.query` on MongoDB, and `db.raw`, which this page does not cover. 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 SQL. This page covers none of those three. 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 @@ -219,7 +226,7 @@ const db = postgres({ }); ``` -To give a model your own methods on top of the built-in ones, import `Collection` from `@prisma/orm-postgres/orm-client`, subclass it, and create the client with `orm(...)` from that same package instead of `postgres(...)`. [Custom `Collection` subclass](#custom-collection-subclass) has the code. +To give a model your own methods on top of the built-in ones, import `Collection` from `@prisma/orm-postgres/orm-client`, subclass it, and register the subclass with `orm(...)` from that same package. You still create the client with `postgres(...)`, and `orm(...)` takes two things from that client. [Custom `Collection` subclass](#custom-collection-subclass) has the code. ### MongoDB @@ -239,7 +246,7 @@ The examples below follow each database's own form: `db.orm.public.User` on Post ## 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(...)`. On MongoDB the accessor name is also the name of the database collection the documents are in, which is why `db.orm.users` reads the way it does. 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. 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(...)`. On MongoDB, `users` in `db.orm.users` is also the name of the database collection. 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). @@ -250,7 +257,6 @@ const aliceId = '00000000-0000-4000-8000-000000000001'; const carolId = '00000000-0000-4000-8000-000000000003'; const postId = '00000000-0000-4000-8000-000000000010'; const acmeId = '00000000-0000-4000-8000-000000000020'; -const emptyCustomerId = '00000000-0000-4000-8000-000000000021'; ``` ### `where()` @@ -269,7 +275,7 @@ Restrict a query to rows matching a filter. | Name | Type | Required | Description | |---|---|---|---| -| `filter` | Callback `(fields) => Expression`, shorthand object, or (MongoDB) a `MongoFieldFilter` | Yes | The condition rows must satisfy. | +| `filter` | A callback that calls an operator on a column, a shorthand object, or (MongoDB) a `MongoFieldFilter` | Yes | The condition rows must satisfy. | #### Return type @@ -325,7 +331,7 @@ Return only the scalar fields you name. - Available for PostgreSQL and MongoDB. - On PostgreSQL, `select()` narrows the returned row shape at the type level: fields you didn't select are absent from the result type. -- On MongoDB, `select()` changes what the database returns but not the TypeScript type. Only the fields you named come back, while the type still lists every field on the model. Treat the fields you did not name as absent, whatever the type says. +- On MongoDB, `select()` changes what the database returns but not the TypeScript type. Only the fields you named come back. The type still lists every field on the model, so read only the fields you named from each returned document and treat the rest as absent. #### Options @@ -368,7 +374,7 @@ Eagerly load a relation onto the returned rows. | Name | Type | Required | Description | |---|---|---|---| | `relationName` | `string` | Yes | The relation to load. | -| `refineFn` | `(relation: Collection) => Collection \| aggregate` | No | PostgreSQL only. Narrows the loaded relation, or reduces it to one value. | +| `refineFn` | A callback that returns the narrowed relation or an aggregate of it | No | PostgreSQL only. Narrows the loaded relation, or reduces it to one value. | #### Return type @@ -403,19 +409,19 @@ 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. The heading calls that callback a refinement. 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. +On PostgreSQL, the callback you pass to `include()` receives the related rows as a collection. This page calls that callback a refinement. 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. #### Remarks - PostgreSQL only. On MongoDB, `include()` takes no callback, and `count`, `sum`, `avg`, `min`, `max`, and `combine` do not exist on a collection. - On PostgreSQL, those six are only callable **inside** an `include()` callback. Called anywhere else, they throw an error whose `code` is `ORM.INCLUDE_INVALID`. -- `sum()` and `avg()` take a numeric column: an integer, a floating-point number, or a decimal. They also take an interval or a `Time` column. They do not take a date or a timestamp. TypeScript rejects a date or timestamp field name here. +- `sum()` and `avg()` take a numeric column: an integer, a floating-point number, or a decimal. They also take a `Time` column. They do not take a date or a timestamp, and TypeScript rejects a date or timestamp field name here. - `min()` and `max()` take more: numeric and text columns, dates, times, timestamps, intervals, IP addresses, and text arrays. They do not take a boolean, a `uuid`, binary data, a bit string, or JSON. - `count()` returns a `number`. With no argument it counts rows; with a field name it counts rows where that field is not null. `countBigInt()` returns a `bigint` instead, for a count too large for a JavaScript `number`. - `sum()` over an integer column returns a `number`. A total too large for a `number` throws an error whose `code` is `RUNTIME.DECODE_FAILED`. Use `sumBigInt(field)` for totals that large. It returns a `bigint`. -- `avg()` over an integer column returns a `number`, which rounds. `avgDecimal(field)` returns the exact average as a decimal string. Pass that string to a decimal library, or call `Number(...)` on it when rounding is fine. +- `avg()` over an integer column returns a JavaScript floating-point `number`. `avgDecimal(field)` returns the exact average as a decimal string. Pass that string to a decimal library, or call `Number(...)` on it when a floating-point value is fine. - When a to-many relation has no rows, `sum()`, `avg()`, `min()`, and `max()` come back as `null`, not `0`. -- `combine(shape)` takes an object. You choose the keys. Build every value from the collection the callback was given, which is `posts` in the example below. A value that is `posts` with more methods chained on it comes back as an array of rows. A value that is an aggregate, such as `posts.count()`, comes back as that single value. +- `combine(shape)` takes an object. You choose the keys. Call `posts.combine({ ... })`, and inside the object write `posts` again for each value. It is the same object every time. A value that chains more methods on `posts` comes back as an array of rows. A value that is an aggregate, such as `posts.count()`, comes back as that single value. #### Options @@ -473,15 +479,6 @@ const avgCustomers = await db.orm.public.Customer.include('orders', (orders) => // avgCustomers[0].orders is 300 ``` -A customer with no orders reduces to `null`: - -```typescript -const rows = await db.orm.public.Customer.include('orders', (orders) => orders.sum('amount')) - .where({ id: emptyCustomerId }) - .all(); -// rows[0].orders is null -``` - ##### `combine()` multiple sub-views (PostgreSQL) ```typescript @@ -499,7 +496,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, add a polyfill: `import 'temporal-polyfill/full/global'`. -`sum('createdAt')` and `avg('createdAt')` do not compile. TypeScript rejects `'createdAt'`, because PostgreSQL has no `SUM` or `AVG` over a date or a timestamp. +`sum('createdAt')` and `avg('createdAt')` do not compile. PostgreSQL has no `SUM` or `AVG` over a date or a timestamp. ::: ### `orderBy()` @@ -511,7 +508,7 @@ Sort the result set. - Available for PostgreSQL and MongoDB, with different argument shapes. - On PostgreSQL, `orderBy()` takes a callback that returns `.asc()` or `.desc()` on one column. Pass an array of such callbacks to sort by more than one column. - On MongoDB, `orderBy()` takes an object instead: `{ field: 1 }` sorts ascending and `{ field: -1 }` sorts descending. -- On PostgreSQL, a native enum column sorts in the order its members are declared, not alphabetically. A `native_enum Priority` declared `Low`, `High`, `Urgent` sorts in that order under `.asc()`. A text-backed enum sorts by its stored text instead, so `'high'` comes before `'low'` and `'urgent'`. Declare the enum as a `native_enum` block when the sort order matters. +- On PostgreSQL, a native enum column sorts in the order its members are declared, not alphabetically. The example schema declares `Priority` text-backed, so it sorts by its stored text: `'high'`, then `'low'`, then `'urgent'`. Declare the same members in a `native_enum Priority` block instead and `.asc()` sorts them `Low`, `High`, `Urgent`. Do that when the sort order matters. #### Options @@ -621,8 +618,8 @@ Resume pagination from a known position. #### Remarks - PostgreSQL only. `cursor()` does not exist on a MongoDB collection. -- Always call `orderBy()` before `cursor()`. TypeScript rejects the call if you do not. Nothing checks this while the query runs, so a cursor on an unordered query is ignored and every row comes back with no error. -- The cursor object must name every column the `orderBy()` sorts on. Leave one out and the call throws an error whose `code` is `ORM.CURSOR_VALUE_MISSING`. +- Always call `orderBy()` before `cursor()`. TypeScript rejects the call if you do not. The library does not check this while the query runs, so code that skips TypeScript, such as plain JavaScript or a cast, gets no error: the cursor is ignored and every row comes back. +- The cursor object must name every column the `orderBy()` sorts on. Leave one out and the call throws an error whose `code` is `ORM.CURSOR_VALUE_MISSING`. With two sort columns, give both keys. The rows are compared as a pair, so you get the rows past the first column's value, plus the rows with that same first value and past the second. #### Options @@ -657,6 +654,7 @@ Remove duplicate rows, comparing only the fields you name. #### Remarks - PostgreSQL only. `distinct()` does not exist on a MongoDB collection. +- You do not need `select()`. `distinct('priority')` keeps one whole row per distinct `priority` value, whatever the query returns. #### Options @@ -687,6 +685,7 @@ Keep the first row per key according to `orderBy()`. - PostgreSQL only. `distinctOn()` does not exist on a MongoDB collection. - Call `orderBy()` first, so that "the first row per key" means something. TypeScript rejects the call if you do not. +- Start the `orderBy()` with the `distinctOn()` columns, as the example does. PostgreSQL rejects a query whose `ORDER BY` does not begin with the `DISTINCT ON` columns. The library does not check this, so a mismatch fails in the database. #### Options @@ -719,7 +718,7 @@ Narrow a model to one of the variants declared with `@@base`. - Available for PostgreSQL and MongoDB. - Pass the variant's model name, such as `'Bug'`, not the string in its `@@base` attribute. -- On PostgreSQL, a variant that sets its own `@@map`, as `Bug` and `Feature` do, is stored in a second table beside the base model's table. A variant without `@@map` is stored in the base model's table. That changes how some writes behave; see [`createAndCount()`](#createandcount) and [`upsert()`](#upsert). +- On PostgreSQL, a variant that sets its own `@@map`, as `Bug` and `Feature` do, gets its own table. That changes how some writes behave; see [`createAndCount()`](#createandcount) and [`upsert()`](#upsert). - On MongoDB, every variant is a document in the one collection, told apart by the field named in `@@discriminator`. #### Options @@ -750,7 +749,14 @@ const tutorials = await db.orm.posts.variant('Tutorial').all(); 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). -There is no `count()` method on either database. On PostgreSQL, count with `aggregate((a) => ({ n: a.count() }))`. On MongoDB, count with `db.query`, the [pipeline builder](/orm/fundamentals/advanced-queries#mongodb-pipeline-builder). It is already on your client, so there is nothing extra to import. Both are shown under [Count records](/orm/fundamentals/reading-data#count-records). +There is no `count()` method on either database. On PostgreSQL, count with `aggregate((a) => ({ n: a.count() }))`. On MongoDB, count with `db.query`, the [pipeline builder](/orm/fundamentals/advanced-queries#mongodb-pipeline-builder): + +```typescript +const built = db.query.from('posts').count('total').build(); +const [counted] = await (await db.runtime()).query(built); // counted.total is the count +``` + +Both are shown under [Count records](/orm/fundamentals/reading-data#count-records). For a task-oriented walkthrough, see [Reading data](/orm/fundamentals/reading-data). @@ -847,26 +853,16 @@ const urgentPost = await db.orm.public.Post.first((p) => p.priority.eq('urgent') const bob = await db.orm.users.where({ name: 'Bob' }).first(); ``` -##### No match returns `null` - -```typescript tab="PostgreSQL" -const nobody = await db.orm.public.User.first({ email: 'nobody@example.com' }); -// null -``` - -```typescript tab="MongoDB" -const nobody = await db.orm.users.where({ email: 'nobody@example.com' }).first(); -// null -``` - For Prisma ORM 7 users, `findUnique` and `findFirst` map onto `first()`: ```diff - const alice = await prisma.user.findUnique({ where: { email } }); + const alice = await db.orm.public.User.first({ email }); +- const alice = await prisma.user.findUniqueOrThrow({ where: { email } }); ++ const alice = await db.orm.public.User.where({ email }).all().firstOrThrow(); ``` -`first()` returns `null` on no match and does not check that only one row matched. For the throwing forms, `findUniqueOrThrow` and `findFirstOrThrow` map onto `all().firstOrThrow()`, which throws an error whose `code` is `RUNTIME.NO_ROWS`. +`first()` returns `null` on no match and does not check that only one row matched. `findUniqueOrThrow` and `findFirstOrThrow` both map onto `all().firstOrThrow()`, as the last line above shows. `firstOrThrow()` is a method on the result that `all()` gives you, and calling it does not count as using the result a second time. It throws an error whose `code` is `RUNTIME.NO_ROWS` when no row matched. ### Custom `Collection` subclass @@ -875,8 +871,8 @@ On PostgreSQL you can subclass `Collection` to add your own methods to a model, #### Remarks - PostgreSQL only. You cannot subclass a collection on MongoDB. -- `db.orm` cannot use your subclass. Build a second accessor by calling the `orm(...)` function with a `collections` object, and use that accessor for the models you subclassed. -- The accessor `orm(...)` returns has the same methods as `db.orm.public`. You keep the `postgres(...)` client as well, for `connect()` and for the `context` you pass to `orm(...)`. +- `db.orm` cannot use your subclass. Call the `orm(...)` function with a `collections` object to get a second accessor with the same methods as `db.orm.public`. You now have two accessors, both of them work, and your existing `db.orm.public.X` calls are unaffected. Use the one `orm(...)` returns for the models you subclassed. +- `orm(...)` takes two things from the client that `postgres(...)` created under [Setting up the client](#setting-up-the-client). `client.connect()` gives you the `runtime` argument, and `client.context` is the `context` argument. - `Collection` takes two type arguments: your contract type, and the name of the model this collection is for. - A method that returns `this.variant('Bug')` gives back a collection narrowed to the `Bug` model. It is chainable, so you finish it with `all()`, `first()`, or any other method. @@ -903,14 +899,14 @@ class TaskCollection extends Collection { const client = postgres({ contractJson, url: process.env.DATABASE_URL }); const runtime = await client.connect(); -const db = orm({ +const tasks = orm({ runtime, context: client.context, collections: { Task: TaskCollection }, }).public; // .public is the PostgreSQL schema -const bugs = await db.Task.bugs().all(); -const features = await db.Task.features().all(); +const bugs = await tasks.Task.bugs().all(); +const features = await tasks.Task.features().all(); ``` ## Write methods [#mutation-terminals] @@ -920,7 +916,7 @@ A write method changes the database. `create`, `createAll`, `createAndCount`, `u For a task-oriented walkthrough, see [Writing data](/orm/fundamentals/writing-data). 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 all six methods 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 nothing checks at run time. TypeScript refuses to compile these methods without a `where()`, so the risk is code that escapes the type check, such as JavaScript or a cast to `any`. Without a filter, `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 you only get this protection from TypeScript. Without a filter, `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. @@ -932,7 +928,7 @@ Insert a single row and return it. #### Remarks - Available for PostgreSQL and MongoDB. -- On PostgreSQL, leave out any field your contract gives a default, including the id. Nullable fields can be left out too. Every other field is required. +- On PostgreSQL, you may leave out any field your contract gives a default, including the id. Passing an explicit id is accepted. Nullable fields can be left out too. Every other field is required. - On MongoDB, `_id` is the only field you can leave out. Give every other field a value, and write `null` for a field you want empty. - On MongoDB, `create()` returns the values you passed plus the `_id` the server assigned, not the stored document. On PostgreSQL the returned row is read from the database. - On PostgreSQL, `create()` can create or link related rows in the same transaction. Nest `create()` on the side that does not hold the foreign key, and `connect()` on the side that does. @@ -944,7 +940,7 @@ Insert a single row and return it. |---|---|---|---| | The row's fields | Object of field values | Yes | The row to insert. Pass the object as the only argument. There is no `data` wrapper. | -On PostgreSQL, that object can also carry a `create` or `connect` callback for a related row, as shown below. +On PostgreSQL, that object can also carry a `create` or `connect` callback for a related row, as shown below. `posts.create([...])` and `tags.disconnect([...])` take arrays because those relations are to-many, and `user.connect({...})` takes one object because `Post.user` is to-one. #### Return type @@ -1043,7 +1039,7 @@ Insert rows and return how many were inserted, without reading them back. - Available for PostgreSQL and MongoDB. - On PostgreSQL, `createAndCount()` does not work on a variant that is stored in its own table. Use `createAll()` instead. - A variant is stored in its own table when its `@@map` names a different table than its base model does. `Bug` in the [example schema](#example-schema) is one: it maps to `bug` while `Task` maps to `task`. -- Calling it on such a variant throws an error whose `code` is `ORM.OPERATION_UNSUPPORTED`, with the message `createAndCount() is not supported for MTI variant "Bug" on model "Task". Use createAll() instead.` The message calls it an MTI variant, short for multi-table inheritance. +- Calling it on such a variant throws an error whose `code` is `ORM.OPERATION_UNSUPPORTED`, with the message `createAndCount() is not supported for MTI variant "Bug" on model "Task". Use createAll() instead.` - On MongoDB, a variant is one document in one collection, so `createAndCount()` works on a variant as usual. #### Options @@ -1089,10 +1085,17 @@ Update the matched row and return it, or `null` if none matches. - Available for PostgreSQL and MongoDB. Requires a prior `where()` (see the warning at the top of this section). - On MongoDB, `update()` takes an object of field values, or a callback that returns field operations, such as `(p) => [p.content.set('Rewritten')]`. See [Field update operations](#field-update-operations). -- On MongoDB, the update input covers the base model's fields only. A field declared on a variant, such as `duration` on `Tutorial`, is a TypeScript error even though it runs correctly. Put `// @ts-expect-error` on that line, as shown under [Field update operations](#field-update-operations). +- On MongoDB, the update input covers the base model's fields only. Putting a field declared on a variant in the update, such as `duration` on `Tutorial`, makes TypeScript complain even though the update runs correctly. Put `// @ts-expect-error` on the line directly above `.update(...)`. The same is true of `updateAll()` and `updateAndCount()`. + + ```typescript + // @ts-expect-error duration is a Tutorial field, not a Post field + .update((t) => [t.duration.inc(10)]); + ``` + - On PostgreSQL, `update()` takes an object of field values only. Passing a function changes nothing and returns `null`. - On PostgreSQL, `update()` can also relink related rows. `connect()` points the foreign key at a different row, and `disconnect()` removes a many-to-many link without deleting either row. -- Returns `null` when no row matches. On PostgreSQL it also returns `null` when what you pass has no fields to change. +- Returns `null` when no row matches. On PostgreSQL it also returns `null` when the object you pass is empty and has no keys at all. An object whose values already match the row is different: that update runs and returns the row. +- On PostgreSQL, `select()` and `include()` before `.update()` choose which fields and relations the returned row carries. They change nothing about what is written. #### Options @@ -1139,7 +1142,6 @@ const relinked = await db.orm.public.Post.where({ id: postId }).update({ ##### Nested `disconnect()` unlinks a many-to-many row (PostgreSQL) ```typescript -// select() and include() shape the row you get back, nothing else. const updated = await db.orm.public.Post.where({ id: postId }) .select('id', 'title') .include('tags', (tag) => tag.select('id', 'label').orderBy((t) => t.label.asc())) @@ -1164,8 +1166,8 @@ Update every matching row and collect the results. - Available for PostgreSQL and MongoDB. Call `where()` first. Without a filter this changes every row. See the warning at the top of this section. - On PostgreSQL, `updateAll()` runs as one statement, so every matching row changes together. -- On MongoDB, `updateAll()` is three database operations. First it collects the ids of the documents that match your filter. Then it updates every document that matches that filter, not only the ids it collected. Then it reads back the documents whose ids it collected. -- Two things follow from that. A document that starts matching only after the first step is updated but not returned. A document that someone else changes after the update comes back with their values. +- On MongoDB, `updateAll()` is not a single operation, and two things follow. A document that starts matching your filter only once the call is under way is updated but not returned to you. A document that someone else changes during the call comes back carrying their values, not yours. +- That happens because the update runs by filter, while the read-back runs by the ids collected before the update. - Prisma ORM has no `db.transaction()` on MongoDB, so you cannot avoid this from the client. To control it, share one `MongoClient` with the MongoDB driver and group the writes in a driver session, shown under [Transactions on MongoDB](/orm/fundamentals/transactions#transactions-on-mongodb). #### Options @@ -1225,14 +1227,6 @@ const count = await db.orm.public.Post.where({ userId: carolId }).updateAndCount const count = await db.orm.users.where({ role: 'author' }).updateAndCount({ role: 'admin' }); ``` -##### Field-operations callback (MongoDB) - -```typescript -const count = await db.orm.posts - .where({ authorId: aliceId }) - .updateAndCount((p) => [p.content.set('Rewritten')]); -``` - ### `delete()` Remove the matched row and return it, or `null` if none matches. @@ -1341,13 +1335,14 @@ Insert a row if none matches, otherwise update the existing row. - On MongoDB, call `where()` first. The filter decides which document is updated. - On PostgreSQL, `upsert()` never reads the filters. A `where()` before it is ignored, without an error. `conflictOn` decides which row counts as already existing. -- `conflictOn` takes one unique column with a value, such as `conflictOn: { label: 'typescript' }`. Only the column name is used. The value is required by the type and is then ignored, so pass any value of the right type. +- `conflictOn` names the unique column that decides insert or update, such as `conflictOn: { label: 'typescript' }`. Only the column name is used. The value is required by the type and is then ignored, so pass any value of the right type. +- For a unique constraint on several columns together, name every one of those columns. The set you name has to match one `@@unique` constraint. - Leave `conflictOn` out to use the primary key instead. - When a row is inserted on PostgreSQL, the `create` side is used as you wrote it. - When a row is inserted on MongoDB, a field that appears in both `create` and `update` takes the `update` value. - On MongoDB, the `update` side takes an object of field values or a field-operations callback. - On PostgreSQL, `upsert()` does not work on a variant stored in its own table, the same restriction as [`createAndCount()`](#createandcount). It throws an error whose `code` is `ORM.OPERATION_UNSUPPORTED`, with the message `upsert() is not supported for MTI variant "Bug" on model "Task". Use createAll() instead.` -- That advice is the library's own. `createAll()` inserts rows and has no update half, so use it when the rows are new. +- There is no upsert for such a variant. Read the row with `first()`, then call `create()` or `update()`. - On MongoDB, the `_id` on the returned row is a hex string, not the driver's `ObjectId`. #### Options @@ -1356,9 +1351,7 @@ Insert a row if none matches, otherwise update the existing row. |---|---|---|---| | `create` | Object of field values | Yes | The row to insert if none matches. | | `update` | Object of field values, or on MongoDB a field-operations callback | Yes | The changes to apply if a row matches. | -| `conflictOn` | Object with one unique column and a value, PostgreSQL only | No | The column that decides insert or update. Omit to use the primary key. | - -Only the column name in `conflictOn` is used. The value it is paired with is ignored. +| `conflictOn` | Object naming the unique column or columns, PostgreSQL only | No | What decides insert or update. Omit to use the primary key. | #### Return type @@ -1383,7 +1376,7 @@ const inserted = await db.orm.public.Tag.upsert({ const updated = await db.orm.public.Tag.upsert({ create: { label: 'typescript' }, update: { label: 'typescript-renamed' }, - conflictOn: { label: 'ignored' }, // the same value works here, because it is ignored + conflictOn: { label: 'ignored' }, }); ``` @@ -1439,7 +1432,7 @@ For Prisma ORM 7 users, `upsert()` replaces the `where` argument with `conflictO ## Grouped aggregates -PostgreSQL supports aggregation over a result set, both flat (`aggregate()`) and grouped (`groupBy().aggregate()`), with `having()` to filter groups. Neither method exists on a MongoDB collection. To aggregate on MongoDB, use the pipeline builder, which is this library's API for MongoDB's own aggregation pipeline. It is described in [MongoDB: Pipeline builder](/orm/fundamentals/advanced-queries#mongodb-pipeline-builder). +PostgreSQL supports aggregation over a result set. `aggregate()` treats the whole result set as one group. `groupBy().aggregate()` aggregates per group, and `having()` filters those groups. None of these methods exist on a MongoDB collection. 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). The examples below use a separate `Customer` / `Order` schema, where `Order.amount` is a numeric column: @@ -1476,14 +1469,15 @@ model Order { ### `aggregate()` -Compute aggregates over the current result set. +Compute aggregates over the current result set. `aggregate()` runs the query, so you `await` it directly instead of calling `.all()`. #### Remarks - PostgreSQL only. Not available on MongoDB. -- `count()` counts rows. It takes no argument and always resolves to a number (`0` over an empty set). +- `count()` counts rows. It takes no argument and always returns a number, `0` over an empty set. - `count(field)` counts the rows where that field is not null. -- `sum()`, `avg()`, `min()`, and `max()` resolve to `null`, not `0`, over an empty result set. Handle the `null`. `sum('amount')` over an `Int` column is typed `number | null`. +- `sum()`, `avg()`, `min()`, and `max()` return `null`, not `0`, over an empty result set. Handle the `null`. `sum('amount')` over an `Int` column is typed `number | null`. +- `sum()` and `avg()` are offered on `Decimal` and `BigInt` columns. Over a `Decimal` column they return the exact value as a string. Over a `BigInt` column `sum()` returns a `number` and throws an error whose `code` is `RUNTIME.DECODE_FAILED` if the total is past `Number.MAX_SAFE_INTEGER`. Use `sumBigInt()` on a `BigInt` column whose total can grow that large. #### Options @@ -1491,7 +1485,7 @@ Compute aggregates over the current result set. |---|---|---|---| | `selector` | Callback `(agg) => ({ total: agg.sum('amount') })` | Yes | The aggregates to compute. Each key you return becomes a key of the result object. | -The callback's `agg` argument gives you `count()`, `count(field)`, `sum(field)`, `avg(field)`, `min(field)`, and `max(field)`. It also gives you three exact variants, for numbers too large or too precise to hold in a JavaScript `number`. `agg.countBigInt()` takes no argument and returns a `bigint`. `agg.sumBigInt('amount')` returns a `bigint`. `agg.avgDecimal('amount')` returns the exact average as a string, such as `'10.3333333333333333'`. You import nothing to use these. +The callback's `agg` argument gives you `count()`, `count(field)`, `sum(field)`, `avg(field)`, `min(field)`, and `max(field)`. Three more methods avoid JavaScript's `number` precision limits. `agg.countBigInt()` takes no argument and returns a `bigint`. `agg.sumBigInt('amount')` returns a `bigint`. `agg.avgDecimal('amount')` returns the average as a string, such as `'10.3333333333333333'`. #### Return type @@ -1504,15 +1498,11 @@ The callback's `agg` argument gives you `count()`, `count(field)`, `sum(field)`, ##### Count (PostgreSQL) ```typescript -const stats = await db.orm.public.Order.aggregate((agg) => ({ total: agg.count() })); -// { total: 10 } -``` - -##### Count the rows where a field is set (PostgreSQL) - -```typescript -const stats = await db.orm.public.Order.aggregate((agg) => ({ withQuantity: agg.count('quantity') })); -// { withQuantity: 8 } +const stats = await db.orm.public.Order.aggregate((agg) => ({ + total: agg.count(), + withQuantity: agg.count('quantity'), // rows where quantity is not null +})); +// { total: 10, withQuantity: 8 } ``` ##### Sum and average (PostgreSQL) @@ -1537,6 +1527,17 @@ const stats = await db.orm.public.Order.aggregate((agg) => ({ // { cheapest: 10, priciest: 500 } ``` +##### Counts and totals past JavaScript's precision limit (PostgreSQL) + +```typescript +const exact = await db.orm.public.Order.aggregate((agg) => ({ + rows: agg.countBigInt(), + total: agg.sumBigInt('amount'), + average: agg.avgDecimal('amount'), +})); +// { rows: 10n, total: 1500n, average: '150.0000000000000000' } +``` + ##### `null` over an empty set (PostgreSQL) ```typescript @@ -1563,9 +1564,9 @@ Group rows by one or more fields, then aggregate per group. - PostgreSQL only. Not available on MongoDB. - Pass field names as separate arguments: `groupBy('customerId', 'channel')`. -- `groupBy(...)` returns a `GroupedCollection`. You cannot `await` it. Call `.aggregate(...)` on it, which runs the query and gives you one row per group. Each row holds the grouping fields, typed as they are on your model, plus the keys you returned from the aggregate callback. +- `groupBy(...)` returns a `GroupedCollection`, which you never import or name yourself. You cannot `await` it. Call `.aggregate(...)` on it, which runs the query and gives you one row per group. Each row holds the grouping fields, typed as they are on your model, plus the keys you returned from the aggregate callback. - Chain order: `where(...)` comes before `groupBy(...)` and filters the rows that get grouped. After `groupBy(...)` you can call `having(...)`, `orderBy(...)`, `limit(...)`, and `offset(...)` in any order. `aggregate(...)` comes last. -- `orderBy(...)` on a grouped collection sorts the groups by a field you grouped on. You cannot sort groups by an aggregate value. To do that, write the query with the SQL query builder (`db.sql`), described in [Advanced queries](/orm/fundamentals/advanced-queries). +- `orderBy(...)` on a grouped collection sorts the groups by a field you grouped on. You cannot sort groups by an aggregate value. To do that, write the query with the SQL query builder (`db.sql`). See the example below. - `limit(n)` and `offset(n)` page the groups, and both require an `orderBy(...)` before them. Without one they are a TypeScript error. #### Options @@ -1592,23 +1593,6 @@ const perCustomer = await db.orm.public.Order.groupBy('customerId').aggregate((a // one row per customer, each { customerId, orderCount, totalAmount } ``` -##### Group by two fields (PostgreSQL) - -```typescript -const perCustomerAndChannel = await db.orm.public.Order.groupBy('customerId', 'channel').aggregate( - (agg) => ({ totalAmount: agg.sum('amount') }), -); -// one row per customer and channel, each { customerId, channel, totalAmount } -``` - -##### Filter rows, then group (PostgreSQL) - -```typescript -const perCustomer = await db.orm.public.Order.where((o) => o.amount.gte(100)) - .groupBy('customerId') - .aggregate((agg) => ({ totalAmount: agg.sum('amount') })); -``` - ##### Sort and page the groups (PostgreSQL) ```typescript @@ -1618,6 +1602,18 @@ const firstTwoChannels = await db.orm.public.Order.groupBy('channel') .aggregate((agg) => ({ orderCount: agg.count() })); ``` +##### Sort the groups by an aggregate, with `db.sql` (PostgreSQL) + +```typescript +const topCustomers = db.sql.public.order + .select((f, fns) => ({ customerId: f.customerId, orders: fns.count() })) + .groupBy((f) => f.customerId) + .orderBy((f, fns) => fns.count(), { direction: 'desc' }) + .limit(5) + .build(); +const rows = await db.runtime().query(topCustomers); +``` + For Prisma ORM 7 users, `groupBy()` chains into `aggregate()` instead of taking a `by` array with aggregate keys: ```diff @@ -1658,20 +1654,12 @@ const bigSpenders = await db.orm.public.Order.groupBy('customerId') .aggregate((agg) => ({ totalAmount: agg.sum('amount') })); ``` -##### Filter groups by row count (PostgreSQL) - -```typescript -const groupsWithAtLeastFive = await db.orm.public.Order.groupBy('customerId') - .having((h) => h.count().gte(5)) - .aggregate((agg) => ({ orderCount: agg.count() })); -``` - ## 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(...)`. 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(...)`. Stacked `where()` calls are combined with AND, and you can use the shorthand object in one call and the callback in the next. PostgreSQL and MongoDB write conditions differently. - **PostgreSQL** compares a field with a method on that field, such as `u.email.eq(...)`, inside a callback. To combine conditions, import `and`, `or`, `not`, and `all` from `@prisma/orm-postgres/orm-client`. -- **MongoDB** builds each condition by calling a method on the `MongoFieldFilter` class itself, such as `MongoFieldFilter.eq('name', 'Alice')`. The full list is under [`MongoFieldFilter`](#mongofieldfilter). To combine conditions, use `.and()`, `.not()`, and `MongoOrExpr.of([...])`, described under [Combinators](#combinators). +- **MongoDB** has no callback form. `where()` takes either the shorthand object or a filter you build by calling a static method on the `MongoFieldFilter` class, 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). ### Scalar comparisons (PostgreSQL) @@ -1679,7 +1667,7 @@ Column-level comparison methods on a field. #### Remarks -- PostgreSQL. Which methods a field has depends on its type, and your editor's autocomplete is the answer: if it does not offer a method on that field, the type does not support it. `isNull` and `isNotNull` are on every field. `eq`, `neq`, `in`, and `notIn` need a type that can be compared for equality. `gt`, `lt`, `gte`, and `lte` need a type that can be ordered. `like` and `ilike` need a text type. +- PostgreSQL. The methods available on a field depend on the field's type. Autocomplete shows which ones apply. `isNull` and `isNotNull` are on every field. `eq`, `neq`, `in`, and `notIn` need a type that can be compared for equality. `gt`, `lt`, `gte`, and `lte` need a type that can be ordered. `like` and `ilike` need a text type. `Uuid` and `DateTime` can be compared for equality and ordered, so they have all of these except `like` and `ilike`. `Json` can do neither, so a `Json` field has only `isNull` and `isNotNull`. - `like()` matches a SQL pattern case-sensitively. `ilike()` matches the same pattern case-insensitively. - `in([])` matches no rows. `notIn([])` matches every row. @@ -1706,7 +1694,7 @@ const notAlice = await db.orm.public.User.where((u) => u.email.neq('alice@exampl ```typescript const after = await db.orm.public.Post.where((p) => p.createdAt.gt(new Date('2024-01-02T10:00:00.000Z'))).all(); -const inclusive = await db.orm.public.Post.where((p) => p.createdAt.gte(new Date('2024-01-02T10:00:00.000Z'))).all(); +// gte() is the same comparison, including the boundary value. ``` ##### Pattern matching (PostgreSQL) @@ -1734,8 +1722,7 @@ Combine or negate conditions. - On PostgreSQL, `and(...)` and `or(...)` take any number of conditions, so `and(a, b, c)` is fine. `not(...)` takes one. - On PostgreSQL, `all()` takes no arguments and returns a condition that matches every row. This `all()` is not the `.all()` you call at the end of a chain to run the query. - On MongoDB, `.and(other)` and `.not()` are **methods on a condition you already built**, such as `MongoFieldFilter.eq('name', 'Alice').and(...)`. There is **no `.or()` method**. To OR conditions, build `MongoOrExpr.of([...])`. -- On MongoDB, `.and(other)` takes exactly one condition. For three, chain it: `a.and(b).and(c)`. The result is a `MongoAndExpr`, which you can also build directly with `MongoAndExpr.of([a, b, c])`. -- On MongoDB, an AND of two ORs is `MongoOrExpr.of([...]).and(MongoOrExpr.of([...]))`. +- On MongoDB, `.and(other)` takes exactly one condition. For three, chain it: `a.and(b).and(c)`. You can also write all three at once with `MongoAndExpr.of([a, b, c])`. `MongoAndExpr` and `MongoOrExpr` come from the same module as `MongoFieldFilter`. #### Examples @@ -1743,7 +1730,6 @@ Combine or negate conditions. ```typescript import { and, or, not } from '@prisma/orm-postgres/orm-client'; - const both = await db.orm.public.Post.where((p) => and(p.priority.eq('low'), p.userId.eq('00000000-0000-0000-0000-000000000003')), ).all(); @@ -1755,7 +1741,6 @@ const negated = await db.orm.public.Post.where((p) => not(p.priority.eq('low'))) ```typescript import { all } from '@prisma/orm-postgres/orm-client'; - const everyPost = await db.orm.public.Post.where(() => all()).all(); ``` @@ -1785,8 +1770,8 @@ const authorsNamedAliceOrBob = await db.orm.users .all(); ``` -:::note[No `.or()` instance method or `$nor` on MongoDB] -MongoDB filter expressions have `.and(...)` and `.not()`, but no `.or(...)`. Use `MongoOrExpr.of([...])` instead. There is no `$nor` combinator in the library. +:::note[No `.or()` method on MongoDB] +MongoDB filter expressions have `.and(...)` and `.not()`, but no `.or(...)`. Use `MongoOrExpr.of([...])` instead. ::: ### Relation filters (PostgreSQL) @@ -1800,7 +1785,7 @@ Filter parents by their related rows. - `some()` with no argument matches parents with at least one related row. - A user with no posts matches `every()`. - `not(u.posts.some(...))` and `u.posts.none(...)` match the same rows. Prefer `none(...)`. -- `some()` is also the method for a to-one relation, such as `Post.user`, which holds a single related row. There it reads as "the related row matches". It works from both sides of a relation, so `u.posts.some(...)` and `p.user.some(...)` are both valid. +- All three are also methods on a to-one relation, such as `Post.user`, which holds a single related row. There `some()` reads as "the related row matches" and `none()` as "it does not". #### Examples @@ -1817,6 +1802,14 @@ const noUrgentPost = await db.orm.public.User.where((u) => u.posts.none((p) => p ```typescript const postsByAlice = await db.orm.public.Post.where((p) => p.user.some({ email: 'alice@example.com' })).all(); +const postsByAnyoneElse = await db.orm.public.Post.where((p) => p.user.none({ email: 'alice@example.com' })).all(); +``` + +For Prisma ORM 7 users, `some()` and `none()` on a to-one relation replace `is` and `isNot`: + +```diff +- const postsByAlice = await prisma.post.findMany({ where: { user: { is: { email: 'alice@example.com' } } } }); ++ const postsByAlice = await db.orm.public.Post.where((p) => p.user.some({ email: 'alice@example.com' })).all(); ``` ### Shorthand object filter @@ -1828,7 +1821,7 @@ A plain object of equality matches, ANDed together. - Available for PostgreSQL and MongoDB. - Multiple keys are combined with implicit AND. A key set to `undefined` is skipped. - On PostgreSQL, a key set to `null` becomes an IS NULL check. On MongoDB it matches documents where the field is null and documents where the field is missing. -- On PostgreSQL, a field whose type cannot be compared for equality throws an error whose `code` is `ORM.FILTER_UNSUPPORTED`. Filter that field with a comparison method instead. +- On PostgreSQL, the shorthand object cannot filter a field whose type cannot be compared for equality, such as `Json`. Naming such a field throws an error whose `code` is `ORM.FILTER_UNSUPPORTED`. Use the callback form, which offers whatever that type does support. On a `Json` field that is `isNull()` and `isNotNull()`. #### Examples @@ -1852,24 +1845,24 @@ Helpers that build one MongoDB filter condition. Use them for anything other tha #### Remarks - MongoDB. Import from `@prisma/orm-mongo/query-ast/execution`. `MongoExistsExpr` is in the same module. -- The factories are `of`, `eq`, `neq`, `gt`, `lt`, `gte`, `lte`, `in`, `nin`, `isNull`, and `isNotNull`. There is no `ne`; use `neq`. -- `of(field, operator, value)` takes the MongoDB operator written out as a quoted string, such as `'$regex'`. Use it for operators the named factories do not cover, such as `MongoFieldFilter.of('tags', '$size', 3)`. It passes the operator string to MongoDB unchanged. -- `isNull(field)` matches documents where the field is `null` and documents where the field is missing. `isNotNull(field)` is its opposite. To test only whether a field is present, use `MongoExistsExpr.exists(field)` or `MongoExistsExpr.notExists(field)`. Those two are all `MongoExistsExpr` offers. -- There is no `regex`, `elemMatch`, `all`, or `size` factory. Write those operators through `of`. +- The helpers are `of`, `eq`, `neq`, `gt`, `lt`, `gte`, `lte`, `in`, `nin`, `isNull`, and `isNotNull`. Each returns one filter, which you can pass straight to `.where()` or hold in a variable. There is no `ne`; use `neq`. +- `of(field, operator, value)`: pass the MongoDB operator as a string, including the `$`, for example `'$regex'`. Use it for operators the named helpers do not cover, such as `MongoFieldFilter.of('tags', '$size', 3)`. `of` does no checking of its own, so a misspelled operator such as `'$regexp'` fails at the database, not in TypeScript. +- `isNull(field)` matches documents where the field is `null` and documents where the field is missing. `isNotNull(field)` matches everything else. To test only whether a field is present, use `MongoExistsExpr.exists(field)` or `MongoExistsExpr.notExists(field)`. Those two are all `MongoExistsExpr` offers. +- There is no `regex`, `elemMatch`, `all`, or `size` helper. Write those operators through `of`. #### Options | Method | Type | Description | |---|---|---| -| `of(field, operator, value)` | Field + MongoDB operator string + value | Any operator, written out, such as `'$regex'`. | +| `of(field, operator, value)` | Field + MongoDB operator string + value | Any operator, including the `$`, such as `'$regex'`. | | `eq(field, value)` / `neq(field, value)` | Field + value | Equality / inequality. | -| `gt(field, value)` / `lt(field, value)` / `gte(field, value)` / `lte(field, value)` | Field + a value that can be compared, such as a number, a date, or a string | Greater than, less than, greater or equal, less or equal. | +| `gt(field, value)` / `lt(field, value)` / `gte(field, value)` / `lte(field, value)` | Field + a value of any type | Greater than, less than, greater or equal, less or equal. | | `in(field, values)` / `nin(field, values)` | Field + array | Membership / exclusion. | -| `isNull(field)` / `isNotNull(field)` | Field | Matches a `null` value and a missing field / neither of those. | +| `isNull(field)` / `isNotNull(field)` | Field | `isNull` matches a `null` value or a missing field. `isNotNull` matches everything else. | #### Examples -##### Comparison factories (MongoDB) +##### Comparison helpers (MongoDB) ```typescript import { MongoFieldFilter } from '@prisma/orm-mongo/query-ast/execution'; @@ -1892,7 +1885,7 @@ const noBio = await db.orm.users.where(MongoFieldFilter.isNull('bio')).all(); const hasBio = await db.orm.users.where(MongoFieldFilter.isNotNull('bio')).all(); ``` -##### An operator written out, and `$exists` (MongoDB) +##### An operator written as a string, and `$exists` (MongoDB) ```typescript import { MongoExistsExpr, MongoFieldFilter } from '@prisma/orm-mongo/query-ast/execution'; @@ -1903,12 +1896,12 @@ const withBioField = await db.orm.users.where(MongoExistsExpr.exists('bio')).all ### Dot-notation into an embedded object (MongoDB) -Filter into a field of an embedded value object using a dot path. +Filter into a field of an object stored inside the document, rather than in its own collection, using a dot path. #### Remarks -- MongoDB. Use `MongoFieldFilter.eq('address.city', 'San Francisco')`. It takes the path as a plain `string` and needs no cast. -- The plain object form, `.where({ 'address.city': 'San Francisco' })`, does the same thing when it runs, but it does not compile without a cast. The object you pass to `.where()` is typed against the model's own top-level field names, and a dotted key is not one of them. +- MongoDB. Use `MongoFieldFilter.eq('address.city', 'San Francisco')` for a dotted path. It takes the path as a plain `string` and needs no cast. +- You can also write `.where({ 'address.city': 'San Francisco' })`. It works, but TypeScript rejects it unless you add a cast, because `.where()` only accepts your model's top-level field names. #### Examples @@ -1920,7 +1913,7 @@ import { MongoFieldFilter } from '@prisma/orm-mongo/query-ast/execution'; const usersInSf = await db.orm.users.where(MongoFieldFilter.eq('address.city', 'San Francisco')).all(); ``` -The object form works at runtime too, with a cast. The cast goes through `unknown` because TypeScript refuses a direct cast between two types that do not overlap: +The object form works at runtime too, with a cast: ```typescript const usersInSf = await db.orm.users @@ -1929,20 +1922,23 @@ const usersInSf = await db.orm.users ``` ## Field update operations -On MongoDB, `update()`, `updateAll()`, `updateAndCount()`, and `upsert()` accept a callback in place of a data object. The callback receives one argument. The examples below call it `u` or `t`, and the name is yours to pick. For a top-level field, read it as a property and call an operation on it: `u.bio.set('Hello')`. For a field inside an embedded object, call the argument as a function with the path: `u('address.city').set('San Francisco')`. Return an array of the calls you made. 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, called `u` or `t` in the examples below, and returns an array of the operations you want. PostgreSQL has no callback form: pass an object to [`update()`](#update) instead. -All four methods require `.where()`. Without one they throw an error whose `code` is `ORM.WHERE_MISSING`. `update()` returns the updated document, or `null` when nothing matched. `updateAll()` returns an [`AsyncIterableResult`](#asynciterableresult) of the changed documents. `updateAndCount()` returns the number of documents it changed. +- For a top-level field, read it off the callback's argument as a property: `u.bio.set('Hello')`. +- For a field inside an embedded object, call the callback's argument as a function with the dot path: `u('address.city').set('San Francisco')`. + +All four methods require `.where()`. Without one they throw an error whose `code` is `ORM.WHERE_MISSING`. 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()` returns the number of documents it changed. ### Available operations (MongoDB) #### Remarks -- MongoDB only. `set()`, `unset()`, `inc()`, and `mul()` are available. +- MongoDB only. These are the four operations for single-value fields: `set()`, `unset()`, `inc()`, and `mul()`. The four array operations follow under [Array operations](#array-operations-mongodb-unverified). - `set(value)` assigns a field. - `unset()` removes a field. - `inc(n)` increments a numeric field. TypeScript offers `inc` and `mul` on numeric fields only. Applied to a missing field, `inc` sets the field to `n`. - - `mul(n)` multiplies a numeric field. Applied to a missing field, MongoDB sets it to `0` regardless of the multiplier. -- After `variant(...)`, TypeScript still knows only the base model's fields, so `t.duration.inc(10)` is a type error even though it runs correctly. Put `// @ts-expect-error` on the line directly above the `.update(...)` line, as the example below shows. If the type error ever goes away, TypeScript reports the unused comment instead. + - `mul(n)` multiplies a numeric field. Applied to a missing field, it sets the field to `0` whatever `n` is. +- After `variant(...)`, `t.duration.inc(10)` is a type error even though it runs correctly. Put `// @ts-expect-error` on the line directly above the `.update(...)` line, as the example below shows. If that type error ever goes away, TypeScript reports the comment as unused, and you delete the comment then. #### Examples @@ -1955,6 +1951,8 @@ Return as many operations as you want. They are applied together, in one write. const updated = await db.orm.users .where({ _id: '6650f1c2a1b2c3d4e5f60002' }) .update((u) => [u.bio.set('Set via field op'), u.name.set('Bob R.')]); +// A field inside the embedded `address` object, by dot path: +const moved = await db.orm.users.where({ _id: '6650f1c2a1b2c3d4e5f60002' }).update((u) => [u('address.city').set('San Francisco')]); ``` ##### `inc()` and `mul()` (MongoDB) @@ -1977,7 +1975,7 @@ const multiplied = await db.orm.posts ##### `unset()` (MongoDB) -`updateAndCount()` takes the callback in the same place. `upsert()` takes it as its `update` key, beside `create`: `upsert({ create: { ... }, update: (u) => [u.bio.unset()] })`. That callback cannot use a dot path. Given one, `upsert()` throws an error whose `code` is `ORM.OPERATION_UNSUPPORTED`. +`updateAndCount()` takes the callback in the same place. `upsert()` takes it as its `update` key, beside `create`, and [`upsert()`](#upsert) shows a full call. That callback cannot use a dot path. Given one, `upsert()` throws an error whose `code` is `ORM.OPERATION_UNSUPPORTED`. To change a field inside an embedded object in an `upsert()`, set the whole embedded object on its top-level field instead, as in `u.address.set({ street: '1 Market St', city: 'San Francisco', country: 'US' })`. ```typescript const updated = await db.orm.users.where({ _id: '6650f1c2a1b2c3d4e5f60001' }).update((u) => [u.bio.unset()]); @@ -1986,22 +1984,18 @@ const changed = await db.orm.users.where({ _id: '6650f1c2a1b2c3d4e5f60002' }).up ### Operations that do not exist (MongoDB) -#### Remarks - -- MongoDB. `min()`, `max()`, `rename()`, and `currentDate()` have **no accessor method at all**. - :::warning[No `min`, `max`, `rename`, or `currentDate` field operations] -The callback argument implements `set`, `unset`, `inc`, `mul`, `push`, `pull`, `addToSet`, and `pop` only. `min`, `max`, `rename`, and `currentDate` are absent from the API, and TypeScript rejects them: `rename` is not a property of `u.bio`. Do not use `min()`, `max()`, `rename()`, or `currentDate()`. +The callback's argument implements eight operations, and only these eight: `set`, `unset`, `inc`, `mul`, `push`, `pull`, `addToSet`, and `pop`. `min`, `max`, `rename`, and `currentDate` are absent from the API, and TypeScript rejects them: `rename` is not a property of `u.bio`. ::: ### Array operations (MongoDB) [#array-operations-mongodb-unverified] #### Remarks -- MongoDB. `push()`, `pull()`, `addToSet()`, and `pop()` are available on the callback argument. `push(value)` appends one element. `addToSet(value)` appends it only if it is not already there. `pop(1)` removes the last element, and `pop(-1)` removes the first. +- MongoDB. `push()`, `pull()`, `addToSet()`, and `pop()` are available on the callback's argument. `push(value)` appends one element. `addToSet(value)` appends it only if it is not already there. `pop(1)` removes the last element, and `pop(-1)` removes the first. Those are MongoDB's own `$pop` values. - `pull(match)` removes every element equal to the argument. If the array holds objects, pass part of an object to remove every element that matches it, as in `u.tags.pull({ kind: 'draft' })`. -- These operations need an array field, declared with `[]` in your contract, as in `tags String[]`. Applied to any other field, the write fails with an error from MongoDB rather than a Prisma ORM error code. -- The four lines below are separate calls because one update cannot apply two operations to the same field. One update can still change two different fields, such as pushing to `tags` and setting `bio`. The example schema has no array field, so these lines assume `tags String[]` on `User`. +- These operations need an array field, declared with `[]` in your contract, as in `tags String[]`. Applied to any other field, the write fails with an error raised by MongoDB, not by Prisma ORM. In a `catch` block, `isRuntimeError(error)` is `false` for it and there is no `error.code` to match, so read the message. +- The four lines below are separate calls because one update cannot apply two operations to the same field. One update can still change two different fields, such as pushing to `tags` and setting `bio`. The example schema has no array field, so these four lines assume `tags String[]` on `User` and will not run against the schema on this page. ```typescript await db.orm.users.where({ _id: '6650f1c2a1b2c3d4e5f60001' }).update((u) => [u.tags.push('admin')]); @@ -2012,34 +2006,6 @@ await db.orm.users.where({ _id: '6650f1c2a1b2c3d4e5f60001' }).update((u) => [u.t ## Result types -### Model and result types - -`contract.d.ts` exports a `Models` namespace with one type per model. On PostgreSQL the schema is part of the name, `Models.public_User`; on SQLite, which has no schemas, it is `Models.User`. The same file exports a `models` constant for dotted access, `typeof models.public.User`. A model type carries every scalar field and every relation, so no query returns it directly. Three types derive what queries do return: - -- `Scalars` is the model without its relations: what `all()` and `first()` return when you do not `select` or `include`. -- `Shape` is a row with chosen fields and relations. In `Spec`, `"+"` names scalars and relations to keep, `"-"` names scalars to drop, and any other key is a relation with its own nested spec. Naming only relations in `"+"` keeps every scalar. -- `ResultType` is one row of a query you already wrote. - -`Scalars` and `Shape` come from `@prisma/orm-postgres/family-contract/types` (`@prisma/orm-mongo/family-contract/types` on MongoDB); `ResultType` comes from `@prisma/orm-postgres/components/runtime`. - -```typescript -import type { Models } from "./prisma/contract"; -import type { Scalars, Shape } from "@prisma/orm-postgres/family-contract/types"; -import type { ResultType } from "@prisma/orm-postgres/components/runtime"; - -type UserRow = Scalars; -// { id: number; email: string; name: string | null } - -type UserResponse = Shape; -// { id: number; name: string | null; posts: { id: number; title: string }[] } - -const usersWithPosts = db.orm.public.User.include("posts"); -type UserWithPosts = ResultType; -// the same type as Shape -``` - -Use `Shape` to declare a function's return type once and let the compiler check the body at the `return`. Wrong field names, a relation under `"-"`, or a scalar in `"+"` next to a `"-"` are compile errors on that key. These types replace Prisma ORM 7's `Prisma.User` and `Prisma.UserGetPayload<...>`. - ### `AsyncIterableResult` The methods `all()` / `createAll()` / `updateAll()` / `deleteAll()` return an `AsyncIterableResult`. You can use it two ways: @@ -2047,19 +2013,20 @@ 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. -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. [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 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. -- Switching from one way to the other throws an error whose `code` is `RUNTIME.ITERATOR_CONSUMED`. The message contains `already been consumed`. Awaiting a result and then looping it with `for await`, or the reverse, is what throws. There is no error class to test with `instanceof`: import `isRuntimeError` from the same module and check `error.code`. +- Switching from one way to the other throws an error whose `code` is `RUNTIME.ITERATOR_CONSUMED`. The message contains `already been consumed`. Awaiting a result and then looping it with `for await`, or the reverse, is what throws. There is no error class to test with `instanceof`. Import `isRuntimeError` from `@prisma/orm-postgres/components/runtime` on PostgreSQL, or from `@prisma/orm-mongo/components/runtime` on MongoDB, and check `error.code`. -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)). +PostgreSQL and MongoDB behave the same here, with the same error message and the same rule. 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)). ```typescript import { isRuntimeError } from '@prisma/orm-postgres/components/runtime'; +// all() returns the result object straight away and `await` is what runs the query, so this example holds it in a variable. const result = db.orm.public.User.all(); const first = await result; const again = await result.toArray(); // safe: same array @@ -2074,28 +2041,9 @@ try { } ``` -PostgreSQL and MongoDB behave the same here. The error message and the rule are identical. - -### Named model types - -`contract.d.ts` exports a `Models` namespace and a `models` constant with one member per model, named `_` (`Models.public_User`, or `typeof models.public.User`; bare model names on SQLite). Two helpers from `@prisma/orm-postgres/family-contract/types` turn a model into a row type without a client in scope: `Scalars` is the row a default fetch returns, and `Shape` derives a data structure with chosen scalars and nested relations (`'-'` drops fields, `'+'` on a relation picks its fields). `ResultType` from `@prisma/orm-postgres/components/runtime` names what a query returns. - -```typescript -import type { Models } from './prisma/contract'; -import type { Scalars, Shape } from '@prisma/orm-postgres/family-contract/types'; -import type { ResultType } from '@prisma/orm-postgres/components/runtime'; - -type UserRow = Scalars; -type UserResponse = Shape; -const usersWithPosts = db.orm.public.User.include('posts'); -type UserWithPosts = ResultType; -``` - -These need `8.0.0-rc.10` or newer. A `contract.d.ts` emitted by an earlier release lacks the `Models` namespace until you run `prisma contract emit` again. On MongoDB, import `Scalars` and `Shape` from the `@prisma/orm-mongo` equivalent of the types entrypoint. - ### Aggregate result shapes +- `aggregate((agg) => ({ ... }))` resolves to a **single object** keyed by your aliases. `agg` is the callback's argument, and it carries the aggregate functions. `db.orm.public.Order.aggregate((agg) => ({ total: agg.count() }))` resolves to one object, such as `{ total: 10 }`. - [Grouped aggregates](#grouped-aggregates) documents `aggregate()`, `groupBy()` with more than one field, and every function `agg` gives you. The bullets here cover only the shape of what you get back. -- `aggregate((agg) => ({ ... }))` resolves to a **single object** keyed by your aliases. `db.orm.public.Order.aggregate((agg) => ({ total: agg.count() }))` resolves to one object, such as `{ total: 10 }`. - `groupBy(...).aggregate((agg) => ({ ... }))` resolves to an **array**, one object per group. Each object carries the fields you grouped by plus your aliases. `db.orm.public.Order.groupBy('customerId').aggregate((agg) => ({ orderCount: agg.count(), totalAmount: agg.sum('amount') }))` resolves to `[{ customerId, orderCount, totalAmount }, ...]`. - `count()` is always a `number` (`0` over an empty set). `sum()`, `avg()`, `min()`, and `max()` resolve to `null`, not `0`, over an empty result set, so their TypeScript type includes `null`. diff --git a/docs/orm-docs-audit/c21-orm-client/part1.round3.md b/docs/orm-docs-audit/c21-orm-client/part1.round3.md new file mode 100644 index 00000000000..efef933071f --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-client/part1.round3.md @@ -0,0 +1,92 @@ +I read `part1.mdx` once, top to bottom, as someone coming from Prisma ORM 7. + +## Sentences I could not restate after one reading + +**Line 17** — "Write this schema in `contract.prisma`, the file that replaces `schema.prisma` from Prisma ORM 7. This page calls that file your contract." +Stopped by: "contract" is a new word introduced by renaming, and the page never says what it *is* beyond "the new name for schema.prisma". Then `contract.json` and `contract.d.ts` appear, and I now have three "contract" things. Plainest wording I'd have understood: "Your schema now lives in `contract.prisma`. Running `npx prisma contract emit` compiles it into two generated files: `contract.json` (read at runtime) and `contract.d.ts` (read by TypeScript). You import both." + +**Line 185** — "The name inside is a PostgreSQL type plus a version, and the version is always `@1` today." +Stopped by: if the version is always `@1`, why am I typing it? The sentence tells me a fact about the tool's future, not what to do. I'd have written: "Always write `@@type(\"pg/text@1\")`. Other versions do not exist yet." + +**Line 186** — "A `native_enum` block stores the same members as a real PostgreSQL enum type instead." +Stopped by: "the same members" — the same as what? The example schema has no `native_enum` block, so I have nothing to compare against. And there is no `native_enum` code sample anywhere on the page. I would have needed three lines of actual `native_enum Priority { ... }` code. + +**Line 189** — "`Post.tags Tag[]` and `Tag.posts Post[]` are joined through `PostTag` because `PostTag` has a foreign key to each of the two models and its `@@id` lists exactly those two foreign key columns." +Stopped by: too many ideas in one sentence, and the mechanism is implicit. Nothing on `Post` or `Tag` mentions `PostTag`. So the rule is "the tool scans for a model shaped like this and wires it up silently." I had to read this three times. Plainer: "Write a third model with one foreign key to each side and an `@@id` of exactly those two columns. Prisma finds it automatically — you do not reference it from `Post` or `Tag`." + +**Line 194** — "The same `db` also has a query builder, `db.sql` on PostgreSQL and `db.query` on MongoDB, and `db.raw`, which this page does not cover." +Stopped by: I cannot tell whether "which this page does not cover" applies to `db.raw` alone or to all three. Split it into two sentences. + +**Line 242** — "On MongoDB the accessor name is also the name of the database collection the documents are in, which is why `db.orm.users` reads the way it does." +Stopped by: "reads the way it does" explains nothing. The paragraph has just told me "collection" means a query object, then immediately uses "collection" to mean a MongoDB collection. I finished the paragraph less sure what the word means than when I started. + +**Line 406** — "The heading calls that callback a refinement." +Stopped by: which heading? A sentence about the page's own headings is not a sentence about the tool. Say "This page calls that callback a refinement," or just call it one and move on. + +**Line 418** — "Build every value from the collection the callback was given, which is `posts` in the example below. A value that is `posts` with more methods chained on it comes back as an array of rows." +Stopped by: in the example, `combine` is called *on* `posts` and `posts` is also used *inside* the object. I could not work out whether the inner `posts` is the same object, a copy, or something rebuilt each time. The rule "a value that is X with more methods chained on it" is a description of syntax, not of what I should write. + +**Line 624** — "TypeScript rejects the call if you do not. Nothing checks this while the query runs, so a cursor on an unordered query is ignored and every row comes back with no error." +Stopped by: these two sentences contradict each other on first read. If TypeScript rejects it, how do I ever reach the runtime case? (I eventually guessed: plain JavaScript, or a `any` cast.) Say that. + +**Line 722** — "a variant that sets its own `@@map`, as `Bug` and `Feature` do, is stored in a second table beside the base model's table. A variant without `@@map` is stored in the base model's table." +Stopped by: too many ideas, and I cannot act on any of them here. It also forward-references two methods I do not have yet. + +## Words and phrases I had to guess + +- **"contract"** — guessed: the new name for the schema file, plus the generated files next to it. +- **"emit"** (`prisma contract emit`) — guessed: generate/compile. It is not a word I associate with running a schema step; `generate` was the ORM 7 word and the page never connects the two. +- **"variant"** — guessed: subclass / single-table inheritance. Line 188 defines it as "a model that reuses another model's fields and rows", which I only half-understood until the `Bug` example. +- **"discriminator"** — guessed from Java/ORM background. The page never says what value goes in the column; I guessed it is the string in `@@base(Task, "bug")`, i.e. `"bug"`. The page explicitly tells me *not* to pass that string to `variant()` but never says what it is for. +- **`@@base(Task, "bug")`** — guessed the second argument is the stored discriminator value. +- **"text-backed enum"** vs **"native enum"** — guessed: stored as a `text` column vs a real PostgreSQL `ENUM` type. The page does say this; my problem was that `Priority` is used as the example of both (line 186/514 describe `native_enum Priority`, but the schema declares `Priority` as a text-backed enum, and line 549 says so). Same name, two different declarations, no code for one of them. +- **`Expression`** (line 272) — guessed: whatever `u.email.eq(...)` returns. Not defined or linked. +- **`aggregate`** in `(relation: Collection) => Collection | aggregate` (line 371) — lowercase, so not a TypeScript type. Guessed it means "or one of the aggregate methods". +- **`@prisma/orm-mongo/query-ast/execution`** (line 244) — "query-ast" reads like an internal package path. Guessed AST = abstract syntax tree. I have no idea why a user-facing filter class lives behind that path, and it makes me think I am importing something private. +- **`types { Embedding1536 = pgvector.Vector(1536) }`** (line 24) — guessed: a type alias block. Never explained. Line 190 only tells me where `Embedding1536` comes from, not what the `types` block is or that I must write one. +- **"refinement"** — guessed: the callback passed to `include()`. +- **"which rounds"** (line 416) — guessed: returns an integer. To what precision? Unknown. + +## Places I asked "so what do I actually type?" and got no answer + +1. **Installing anything.** The page imports `@prisma/orm-postgres/runtime`, `@prisma/orm-mongo/runtime`, `@prisma/orm-extension-pgvector/runtime`, and `temporal-polyfill`. There is no `npm install` line anywhere. Line 11 says the migration page "lists the packages to install", but I am on the reference page with a code sample in front of me and I cannot run it. +2. **Getting from zero to `contract.prisma`.** Is there an `init` command? Does `src/prisma/` have to be created by hand? Line 17 says "A new project keeps all three files in `src/prisma/`" — is that a rule the tool enforces, a default I can change, or just a convention? +3. **Creating the tables.** `contract emit` writes `contract.json` and `contract.d.ts`. Nothing creates the `user` table. I came from `prisma migrate` and the page does not say what replaces it or link to it. +4. **tsconfig.** Two things in the first code sample need specific compiler settings and neither is mentioned: `import contractJson from './contract.json' with { type: 'json' }` (import attributes) and `from './contract.d'` (line 198 tells me to write it "exactly like that" but not what makes that resolve). If it needs `"module": "nodenext"` or `resolveJsonModule`, say so. +5. **Counting rows on a top-level query.** Line 411: `count`, `sum`, `avg`, `min`, `max` "are only callable **inside** an `include()` callback." So how do I count all users? This is the most basic thing I do every day in ORM 7 (`prisma.user.count()`) and the page tells me the method exists but I may not call it. Even a pointer ("see [Aggregates](#…)") would do. +6. **Getting correct types out of `select()` on MongoDB.** Line 328 tells me the type is wrong and to ignore it. It does not tell me what to write instead — a cast? `Pick<>`? Nothing. +7. **Writing a `native_enum`.** Described at line 186 and 514, recommended at 514 ("Declare the enum as a `native_enum` block when the sort order matters"), never shown. +8. **`interval` and `Time` columns** (line 412). Neither appears in the example schema and neither is a type I know how to declare. What do I write in `contract.prisma`? +9. **The `Customer` / `Order` schema.** Lines 462 and 465 run examples against models I cannot see, pointing at a section further down. Same for `acmeId` and `emptyCustomerId` at lines 252–253. +10. **Where the example rows come from.** Line 246: "these ids, which stand for rows inserted before the query runs." Inserted how? There is no seed snippet, so I cannot reproduce a single example on this page. +11. **Multi-column cursors.** Line 625 says the cursor object "must name every column the `orderBy()` sorts on", but the only example uses one column. With two columns, what does the object look like, and does it compare them as a tuple? +12. **`distinctOn` ordering.** Line 689 says call `orderBy()` first. In PostgreSQL, `DISTINCT ON` requires the leading `ORDER BY` columns to match the distinct columns. The example (line 708) obeys that, but the page never says it is a rule, so I would have written a broken query and not known why. +13. **`distinct()` with no `select()`.** Line 678 chains `select('priority').distinct('priority')`. Do I need the `select()`? The page does not say. +14. **The `Temporal` polyfill** (line 500). It shows `import 'temporal-polyfill/full/global'` but not the install command or where the import goes. +15. **Custom `Collection` subclass** (line 222): "create the client with `orm(...)` ... instead of `postgres(...)`". Does `orm()` take the same `{ contractJson, url }` object? Does it still connect to Postgres? Unanswered here. + +## Places that explain internals when I only wanted to know what to do + +- **Line 722** — variants with `@@map` go "in a second table beside the base model's table", without `@@map` in the base table. That is storage layout. What I want to know is which one to pick and what breaks. +- **Line 189** — the whole explanation of why `PostTag` is detected (foreign keys, `@@id` contents). I want the rule as an instruction, not as a description of the detection algorithm. +- **Line 185** — "The name inside is a PostgreSQL type plus a version, and the version is always `@1` today." Internal format of a string I am told to copy verbatim. +- **Line 502** — "TypeScript rejects `'createdAt'`, because PostgreSQL has no `SUM` or `AVG` over a date or a timestamp." The reason is fine, but it is the third sentence in a row about why the type system says no, when the actionable content is one sentence. +- **Line 244** — `@prisma/orm-mongo/query-ast/execution` exposes the tool's internal module layout in an import path I am told to type. +- **Line 328 and 363** — both are honest descriptions of bugs or leaks in the MongoDB layer (`select()` lies about types; related `_id` is an `ObjectId` instead of a string). I understand them, but both stop at describing the leak instead of telling me the workaround. Line 363 at least gives me `String(...)`; line 328 gives me nothing. + +## Could I do what the page is for, after one reading? + +Partly. I could write query chains: `where`, `select`, `orderBy`, `limit`, `offset`, `distinct`, `variant`. The method sections are consistent and the Remarks/Options/Return type/Examples shape is easy to scan. The PostgreSQL-vs-MongoDB differences are called out where they matter. + +I could not get to the point of running one. After one reading I still would not know: + +- What to install. No package names in a command anywhere on the page. +- How to create `contract.prisma` in a new project, or whether `src/prisma/` is required. +- How the tables get created. Nothing replaces `prisma migrate` here, and nothing links to it. +- What tsconfig settings the first code sample needs. +- How to count, sum, or average anything that is not inside an `include()` callback. +- How to write a `native_enum`, an `interval` column, or a `Time` column, all three of which the page tells me to use. +- What the second argument to `@@base("bug")` is for. +- How to reproduce any example, because the rows the examples query are never inserted anywhere on the page. + +The biggest single problem is the gap between line 17 ("Write this schema in `contract.prisma`... run `npx prisma contract emit`") and line 200 (a code sample importing three packages I have never installed). Everything between those two points is missing, and the page reads as if I had already done it. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-client/part2.round3.md b/docs/orm-docs-audit/c21-orm-client/part2.round3.md new file mode 100644 index 00000000000..09d481b2492 --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-client/part2.round3.md @@ -0,0 +1,76 @@ +Read once, top to bottom, as a Prisma ORM 7 user. + +## Sentences I could not restate after one reading + +**1. Line 131 — "The accessor `orm(...)` returns has the same methods as `db.orm.public`. You keep the `postgres(...)` client as well, for `connect()` and for the `context` you pass to `orm(...)`."** +What stopped me: four things I have never seen (`postgres(...)`, `connect()`, `client.context`, `runtime`) in two sentences, none of them defined. Also the first sentence's grammar ("The accessor `orm(...)` returns has") made me re-read it three times. +Plainer: "`orm({ runtime, context, collections })` returns an accessor with the same methods as `db.orm`. Keep the client you got from `postgres(...)`: you need `client.connect()` for the runtime and `client.context` for the `context` argument." + +**2. Line 130 — "`db.orm` cannot use your subclass. Build a second accessor by calling the `orm(...)` function with a `collections` object, and use that accessor for the models you subclassed."** +What stopped me: "second accessor" — second to what? And the whole rest of the page says `db.orm.public.User`, while this example makes `db` mean something different (`const db = orm({...}).public`, then `db.Task.bugs()`). Two different `db` variables on one page. I genuinely could not tell whether I now have one client or two, and whether my other queries still work. + +**3. Line 121 — "For the throwing forms, `findUniqueOrThrow` and `findFirstOrThrow` map onto `all().firstOrThrow()`, which throws an error whose `code` is `RUNTIME.NO_ROWS`."** +What stopped me: `firstOrThrow()` appears here once and nowhere else — no section, no example. And line 18 just told me a result "can be used one way only", so calling a method on `all()` looked like the thing I was warned not to do. Plainer: show the line — `await db.orm.public.User.where({ email }).all().firstOrThrow()` — and say that `firstOrThrow()` is a method on the result. + +**4. Line 419 — "On MongoDB, `updateAll()` is three database operations. First it collects the ids of the documents that match your filter. Then it updates every document that matches that filter, not only the ids it collected. Then it reads back the documents whose ids it collected."** +What stopped me: too many ideas at once, and the "not only the ids it collected" clause is a correction folded into the middle of a list. I needed line 420 to work out what line 419 meant. + +**5. Line 344 — "A field declared on a variant, such as `duration` on `Tutorial`, is a TypeScript error even though it runs correctly."** +What stopped me: "is a TypeScript error" for a *field* — I had to guess it means "putting that field in the update object makes the compiler complain, but the update works". I also could not tell if this applies to `updateAll()` and `updateAndCount()` too. + +**6. Line 347 — "On PostgreSQL it also returns `null` when what you pass has no fields to change."** +Could not resolve: does "no fields to change" mean an empty object `{}`, or an object whose values already match the row? Those are very different, and the difference decides whether a `null` return means "not found" or "nothing to do". + +**7. Line 596 — "`conflictOn` takes one unique column with a value... The value is required by the type and is then ignored, so pass any value of the right type."** +I followed it, but only after the code comments. My question afterwards: what do I do when the unique constraint is on two columns together? The page never says. + +**8. Line 602 — "That advice is the library's own. `createAll()` inserts rows and has no update half, so use it when the rows are new."** +What stopped me: the page is telling me its own error message gives bad advice, then does not tell me what to do instead. If I need upsert behaviour on a `Bug`, I finish this bullet knowing only that I cannot have it. + +## Words and phrases I had to guess + +- **"shorthand object"** (line 71, 79) — guessed: a plain `{ field: value }` object meaning equality, as opposed to the `(p) => p.x.eq(...)` callback. Never defined. +- **"terminals"** in the heading anchors `#read-terminals` and `#mutation-terminals` — guessed: internal jargon for "the method that ends the chain". The prose says "read method" and "write method", which is clear; the anchor uses a word the page never uses. +- **"child-owned relation" / "parent-owned relation"** (anchors on lines 226, 237) — guessed: which side stores the foreign key. The visible heading says it plainly; only the anchor uses the jargon. +- **"MTI variant"** (line 298) — the page does expand it, but only as "what the error message calls it". I guessed that a variant with its own `@@map` table is what "multi-table inheritance" means. +- **`@@map`** (line 297) — guessed: a schema attribute setting the table name. Not defined in this part. +- **"field operations" / "field-operations callback"** (line 343, 353) — guessed from the example `(p) => [p.content.set('Rewritten')]`: return an array of per-field set calls. The definition is promised at `#field-update-operations` and not given here. +- **`/runtime` and `/orm-client`** (line 140) — guessed: subpath imports of the package. +- **"`context`" and "`runtime`"** (line 155-162) — I could copy the code but I do not know what either is. +- **"variant stored in its own table"** — guessed it means a separate physical table, so a single INSERT cannot return a count. + +## Places I asked "so what do I actually type?" and got no answer + +1. **Counting on MongoDB** (line 5). "count with `db.query`, the pipeline builder. It is already on your client, so there is nothing extra to import." I still cannot write the line. Counting is the most ordinary thing I do; sending me to another page for it is the worst omission on the page. The PostgreSQL form is given inline — give the MongoDB one too. +2. **`firstOrThrow()`** (line 121). No example anywhere. +3. **The `@ts-expect-error` line** (line 344). "as shown under Field update operations" — I have to leave the page to see one comment-and-line snippet. +4. **Grouping MongoDB writes** (line 421). "share one `MongoClient` with the MongoDB driver and group the writes in a driver session" — no code. This is the fix for a correctness problem the page just told me I have. +5. **Building the client at all** (line 155-157). The subclass example is the only place `postgres(...)`, `connect()`, and `context` appear, and it presents them as things I already do. Every other example starts from a `db` that appeared from nowhere. +6. **Array vs object in nested relation calls.** `posts.create([{...}])` and `tag.disconnect([{...}])` take arrays; `user.connect({...})` takes an object. I guessed it follows to-many vs to-one. The page never says. +7. **Upserting a variant stored in its own table** (line 601-602). See above. +8. **Does `upsert()` count as one of the "six methods" that throw `ORM.WHERE_MISSING`?** The warning (line 175) says "all six methods" and is titled "Filter before you update or delete", but line 594 says MongoDB `upsert()` needs `where()` too. I cannot tell whether a missing `where()` on `upsert()` throws or silently upserts the wrong document. +9. **Omitting fields on PostgreSQL `create()`** (line 187). "leave out any field your contract gives a default, including the id" reads as an instruction. Must I omit them, or may I? If I pass an explicit id, does it error? + +## Places that explain the internals when I only wanted to know what to do + +- **Line 419** (the three MongoDB operations). The consequences on line 420 are what I need. The step-by-step mechanism is the implementation, and it is given first and at greater length. +- **Line 298** — telling me what the error message's wording is short for is describing the library's internal vocabulary. What I need is "you cannot use `createAndCount()` here; use `createAll()`", which line 296 already says. +- **Line 595-596, 613** — the fact that `conflictOn`'s value is ignored is a quirk I must know, but the page states it three separate times (bullet, table row, trailing sentence, plus two code comments). Once is enough. +- **Line 175** — "On PostgreSQL nothing checks at run time. TypeScript refuses to compile these methods without a `where()`, so the risk is code that escapes the type check, such as JavaScript or a cast to `any`." This is a description of how the safety check is implemented. What I need is: "on PostgreSQL you only get this protection from TypeScript." + +## Could I do what the page is for, after one reading? + +Mostly yes for plain CRUD. I could write `all()`, `first()`, `create()`, `createAll()`, `update()`, `delete()` and their `*All`/`*AndCount` forms on either database, and the ORM 7 diffs made the mapping from `findMany`/`findUnique`/`create`/`update`/`delete`/`upsert` easy. The warning about filtering before update or delete landed. The note that `deleteAll()` deletes everything before the first row reaches you is the single clearest sentence on the page. + +What I would still not know: + +- How to count rows on MongoDB. +- How to throw instead of getting `null`, beyond the name `firstOrThrow()`. +- How to construct the client at all — `postgres()`, `connect()`, `context`, `runtime` are used but never introduced, so the `Collection` subclass example is copy-paste only, and I would not know how to adapt it. +- Whether subclassing means I now have two accessors to keep straight, and what happens to my existing `db.orm.public.X` calls. +- What "field operations" are, or how to write the `@ts-expect-error` line I am told to write. +- How to upsert a variant that lives in its own table. +- Whether a missing `where()` on `upsert()` is checked. +- What `select()` and `include()` do beyond a code comment saying they "shape the row you get back". + +One concrete inconsistency worth fixing regardless of the reader question: line 601 quotes the `upsert()` error as saying `Use createAll() instead.` and line 602 concedes that advice does not fit. If the message is really that, say what the reader should do; if it is not, the quote is wrong. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-client/part3.round3.md b/docs/orm-docs-audit/c21-orm-client/part3.round3.md new file mode 100644 index 00000000000..748cdd351ca --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-client/part3.round3.md @@ -0,0 +1,75 @@ +## Sentences I could not restate after one reading + +**Line 3** — "PostgreSQL supports aggregation over a result set, both flat (`aggregate()`) and grouped (`groupBy().aggregate()`), with `having()` to filter groups. Neither method exists on a MongoDB collection." + +"Neither method" stopped me. Three methods were just named — `aggregate()`, `groupBy()`, `having()` — so "neither" (which means two) does not resolve. Plainer: "None of these methods exist on a MongoDB collection." + +**Line 3** — "use the pipeline builder, which is this library's API for MongoDB's own aggregation pipeline." + +"this library" — which library? The page has named `@prisma/orm-postgres` and `@prisma/orm-mongo`; neither is "this library." Plainer: "use the pipeline builder, Prisma's wrapper around MongoDB's aggregation pipeline." + +**Line 55** — "It also gives you three exact variants, for numbers too large or too precise to hold in a JavaScript `number`." + +Too many ideas in one sentence and "exact variants" is an invented label. I had to read the next three sentences to work out that "exact" means "does not lose precision." Plainer: "Three more methods avoid JavaScript's `number` precision limits." + +**Line 55** — "You import nothing to use these." + +I could restate it, but I could not work out why it is here. Nothing earlier suggested I would need an import for a method on a callback argument. It answers a question the page never raised. + +**Line 243** — "Which methods a field has depends on its type, and your editor's autocomplete is the answer: if it does not offer a method on that field, the type does not support it." + +Three ideas in one sentence, and the middle one tells me the page is not going to answer. Plainer: "The methods available on a field depend on the field's type. Autocomplete shows which ones apply." + +**Line 364** — "It works from both sides of a relation, so `u.posts.some(...)` and `p.user.some(...)` are both valid." + +"works from both sides" is a slick phrase, not a description. I think it means "the same method name is used whether the relation is to-many or to-one," which the previous sentence already said. I could not tell whether it was making a new point. + +## Words and phrases I had to guess + +- **"flat (`aggregate()`)"** (line 3) — guessed: aggregating over the whole result set as one group, as opposed to per group. +- **"exact variants"** (line 55) — guessed: they return `bigint` or a string instead of `number`, so no precision is lost. +- **"resolves to"** (lines 45, 47) — guessed: the promise resolves, i.e. this is the value you get after `await`. +- **"calling a method on the `MongoFieldFilter` class itself"** (line 235) — guessed: a static method, called on the class, not on an instance. +- **"a type that can be compared for equality" / "a type that can be ordered"** (line 243) — guessed at the boundary, and I still do not know it. Is `Uuid` orderable? Is `Json` equality-comparable? Is `DateTime`? The page never lists the types. +- **`GroupedCollection`, `MongoAndExpr`, `MongoOrExpr`** — guessed these are class names I mostly do not touch, except `MongoOrExpr` and `MongoAndExpr`, which I do call `.of()` on. +- **`@prisma/orm-mongo/query-ast/execution`** (line 326) — a path with "query-ast" and "execution" in it reads like an internal module. I guessed it really is the supported public import, because the page uses it in an example. + +## "So what do I actually type?" — unanswered + +1. **Sorting groups by an aggregate value.** Line 129: "To do that, write the query with the SQL query builder (`db.sql`)." This is the most common thing anyone wants from `groupBy` — top customers by total spend. The page names the escape hatch and gives no code, not even one line. +2. **The three exact methods have no example.** `countBigInt()`, `sumBigInt()`, `avgDecimal()` are described in prose at line 55 and appear in no code block, no Options table, and no Return type table. +3. **Aggregating a `BigInt` or `Decimal` column.** Line 47 only covers `sum('amount')` on `Int`. What does plain `sum()` do on a `Decimal` column — throw, or silently lose precision? If it throws, that is the one case where I need `avgDecimal`, and the page does not say so. +4. **Building `MongoAndExpr` directly.** Line 298 tells me `MongoAndExpr.of([a, b, c])` exists. The only Mongo import line on the page (line 326) imports `MongoFieldFilter` and `MongoOrExpr`. I do not know where `MongoAndExpr` comes from. +5. **The Mongo import path is not in the prose.** Line 234 tells me exactly what to import for PostgreSQL. The matching Mongo bullet at line 235 tells me none of it; I have to scroll to a code block on line 326 to find the module. +6. **Chaining two `.where()` calls.** The page tells me explicitly that repeated `.having(...)` is ANDed (line 197). It never says whether repeated `.where(...)` is. I would have to guess. +7. **Mixing the shorthand object and a callback in one `where()`.** Both forms are documented. Whether I can pass both, or combine them, is not stated. +8. **Is the callback form available on MongoDB?** Every Mongo example uses `MongoFieldFilter`. Line 235 implies the callback form is PostgreSQL-only, but never says it. +9. **`every()` and `none()` on a to-one relation.** Line 364 says `some()` works on to-one. It says nothing about the other two, so I do not know if `p.user.none(...)` compiles. +10. **The ORM 7 replacement for `is` / `isNot`.** I used `{ user: { is: {...} } }` for two years. The page gives me a migration diff for `aggregate()` and for `groupBy()`, so I expected one here. There is none, and `is`/`isNot` are never mentioned. I had to infer that `p.user.some({...})` is the replacement. +11. **`aggregate()` is itself the terminal call.** Everywhere else a query ends in `.all()` or `.first()`. Line 68 awaits `aggregate()` directly. The page never states that `aggregate()` runs the query; I worked it out from the `await` in the examples. + +## A direct contradiction + +Line 243: if a field's type cannot be compared for equality, it does not have `eq` — "if it does not offer a method on that field, the type does not support it." + +Line 392: "a field whose type cannot be compared for equality throws an error whose `code` is `ORM.FILTER_UNSUPPORTED`. Filter that field with a comparison method instead." + +If the type has no equality comparison method, which comparison method am I supposed to use instead? The advice on line 392 points at something line 243 says does not exist. One of these is wrong, or line 392 means something else and does not say what. + +## Internals where I only wanted instructions + +- Line 142, Return type table: "`GroupedCollection`". I cannot import it, name it, or type a variable with it from anything the page shows. The useful part is the two lines of prose next to it ("Not a promise. Call `.aggregate(...)`"). +- Line 298: "The result is a `MongoAndExpr`" — relevant only because `.of()` is then mentioned, but it is introduced as a fact about the return type rather than as something to do. +- Line 350: "There is no `$nor` combinator in the library." `$nor` is MongoDB's own operator name. This tells me about the library's coverage; it does not tell me what to write if I wanted NOR. + +## Could I do what the page is for, after one reading? + +For the common PostgreSQL cases, yes. I could write `aggregate()`, `groupBy(...).aggregate(...)`, `having(...)`, scalar comparisons, `and`/`or`/`not`, and relation filters from the examples without going anywhere else. The "For Prisma ORM 7 users" diffs on lines 114 and 184 did more for me than anything else on the page. + +What I would still not know: + +- How to sort groups by a computed total — the single query I would have written first. +- Which field types support which comparison methods, so I would find out by hitting a compile error or a runtime `ORM.FILTER_UNSUPPORTED`. +- What to do with a `Decimal` or `BigInt` column, or when the three exact methods are required rather than optional. +- How to write anything on MongoDB beyond copying the four example lines. I have `MongoFieldFilter` (list is in a part I do not have), `MongoOrExpr.of`, `.and()`, `.not()`, one import path, and a named class I cannot import. +- Whether my ORM 7 `is` / `isNot` relation filters map onto `some()`, and whether stacked `where()` calls behave like the stacked `having()` calls the page does document. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-client/part4.round3.md b/docs/orm-docs-audit/c21-orm-client/part4.round3.md new file mode 100644 index 00000000000..ac2ec5c9a91 --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-client/part4.round3.md @@ -0,0 +1,86 @@ +I read the file once, straight through, as a Prisma ORM 7 user. Findings below. + +# Sentences I could not restate after one reading + +**Line 9:** "`of(field, operator, value)` takes the MongoDB operator written out as a quoted string, such as `'$regex'`." +"Written out" stopped me. Written out as opposed to what? There is no other form of the operator shown anywhere, so the contrast is with something I have never seen. Plainer: "Pass the MongoDB operator as a string, including the `$`, for example `'$regex'`." + +**Line 21 (table):** "`isNull(field)` / `isNotNull(field)` | Field | Matches a `null` value and a missing field / neither of those." +"Neither of those" forced me to reconstruct the sentence twice. Plainer: "`isNull` matches a `null` value or a missing field. `isNotNull` matches everything else." + +**Line 19 (table):** "Field + a value that can be compared, such as a number, a date, or a string" +I could not tell whether this is a real type constraint I will hit, or just prose. If TypeScript rejects some types here, say which. If not, say "a number, date, or string". + +**Line 64:** "The plain object form, `.where({ 'address.city': 'San Francisco' })`, does the same thing when it runs, but it does not compile without a cast." +Two ideas jammed together — runtime behaviour and compile behaviour — before I know why either matters. Plainer: "You can also write `.where({ 'address.city': 'San Francisco' })`. It works, but TypeScript rejects it unless you add a cast, because `.where()` only accepts your model's top-level field names." + +**Line 85:** the whole opening paragraph of "Field update operations." +This is six instructions in one block: which methods take a callback, that the callback takes one argument, that the argument name is free, that top-level fields are properties, that embedded fields use a function call, that you return an array, and that PostgreSQL is different. On one reading I retained "callback, array, MongoDB only" and lost the rest. It needs to be a short paragraph plus a two-line list. + +**Line 87:** "All four methods require `.where()`." +I could not restate this because I do not believe I understood it. `upsert()` is one of the four. In Prisma 7 an upsert is keyed by a unique `where`, so maybe this is the same thing — but the page never says what `.where()` means for `upsert()`, and `upsert()` gets no example at all. + +**Line 98:** "If the type error ever goes away, TypeScript reports the unused comment instead." +I understood the words, but not why I am being told. It reads like a warning about a future breakage I cannot act on today. What do I do when that happens — delete the comment? Say that. + +**Line 147:** "The callback argument implements `set`, `unset`, `inc`, `mul`, `push`, `pull`, `addToSet`, and `pop` only." +This directly contradicts line 93, four paragraphs earlier: "`set()`, `unset()`, `inc()`, and `mul()` are available." On first reading I concluded the page disagreed with itself and had to scroll back to check. Line 93 needs to say "available" means "in this subsection", or list all eight. + +**Line 182:** "There is no error class to test with `instanceof`: import `isRuntimeError` from the same module and check `error.code`." +"The same module" is ambiguous. The previous paragraph named two modules (`@prisma/orm-postgres/components/runtime` and `@prisma/orm-mongo/components/runtime`). I had to look at the code sample to resolve it. Name the module in the sentence. + +**Line 206:** "[Grouped aggregates](#grouped-aggregates) documents `aggregate()`, `groupBy()` with more than one field, and every function `agg` gives you." +"Every function `agg` gives you" assumes I already know `agg` is the callback argument. Line 207 introduces it one line later. The order is backwards. + +# Words and phrases I had to guess + +- **"factories"** (lines 8, 11, 25). I guessed: static methods on `MongoFieldFilter` that build a filter object. The page never says what they return or whether I can hold one in a variable. +- **"It passes the operator string to MongoDB unchanged"** (line 9). I guessed: no validation, so a typo like `'$regexp'` fails at the database, not at compile time. Worth stating outright, because it changes how I debug. +- **"embedded value object"** (line 59). I guessed: a nested object stored inside the document, not a separate collection. A Prisma 7 user knows "embedded documents" from Mongo; "value object" is a different vocabulary and I was not sure it meant the same thing. +- **"the callback argument"** (lines 147, 154). I guessed this means the thing named `u` or `t`. It is called "one argument" at line 85, "the argument" at 85, "the callback argument" at 147. Three names for one thing. +- **"applied together, in one write"** (line 105). I guessed: atomic, one round trip to MongoDB. +- **"an error from MongoDB rather than a Prisma ORM error code"** (line 156). I guessed: the thrown thing has no `.code` I can match on, so `isRuntimeError` will not help. Not stated. +- **`pop(1)` / `pop(-1)`** (line 154). I guessed nothing — the page told me what each does. But I had to accept the numbers as magic; there is no hint they are MongoDB's own `$pop` values. + +# Places I asked "so what do I actually type?" + +1. **The function-call form for embedded fields in an update.** Line 85 says `u('address.city').set('San Francisco')`, and then no example in the entire section uses it. Every one of the eight code samples uses the property form. I wanted one sample showing a dotted update end to end. +2. **`upsert()`.** Line 133 gives a fragment inside prose: `upsert({ create: { ... }, update: (u) => [u.bio.unset()] })`. `create` is literally `{ ... }`. I do not know what goes in it, and there is no `.where()` on that fragment even though line 87 says all four methods require one. +3. **How to update an embedded field in an `upsert()`.** Line 133 tells me the dot path throws `ORM.OPERATION_UNSUPPORTED`. It never says what to do instead. That is the one place I would actually be stuck. +4. **What to catch when an array operation hits a non-array field.** Line 156 says the error comes "from MongoDB rather than a Prisma ORM error code". So what is in my `catch` block? No code, no error shape. +5. **`createAll()` and `deleteAll()`.** Line 170 lists them as returning `AsyncIterableResult`. Neither appears anywhere else in this part, so I do not know what they take. +6. **Does `updateAll()` exist on PostgreSQL?** Line 87 sits inside a MongoDB section but states `updateAll()` return behaviour as a general fact. Line 170 lists `updateAll()` with no database qualifier. I could not tell. +7. **`ORM.WHERE_MISSING` and `ORM.OPERATION_UNSUPPORTED`.** Both are named with no example of catching them, while `RUNTIME.ITERATOR_CONSUMED` gets a full `try/catch`. Inconsistent. +8. **Line 188:** `const result = db.orm.public.User.all();` — no `await`. Everywhere else in the file, `all()` is awaited inline. I could not tell if dropping `await` here is required for the example to work or just incidental. + +# Places explaining internals when I wanted instructions + +- **Line 64:** "The object you pass to `.where()` is typed against the model's own top-level field names, and a dotted key is not one of them." This is the type system's reasoning. I wanted: "use `MongoFieldFilter.eq()` for dotted paths." +- **Line 76:** "The cast goes through `unknown` because TypeScript refuses a direct cast between two types that do not overlap." A TypeScript lesson. The code sample already shows the cast; the explanation adds nothing I can act on. +- **Line 98:** "TypeScript still knows only the base model's fields, so `t.duration.inc(10)` is a type error even though it runs correctly." The "even though it runs correctly" part is the only bit I needed. The rest is how the type generator works. +- **Line 97:** "Applied to a missing field, MongoDB sets it to `0` regardless of the multiplier." Reasonable to state — but it is described as MongoDB's behaviour, which left me unsure whether Prisma might change it. + +# Other things that stopped me + +**Line 150:** the heading anchor is `[#array-operations-mongodb-unverified]`. The word "unverified" will be in the published URL and the copy-link button. As a reader I would take that as a signal the section is not trustworthy. This looks like an internal marker that escaped. + +**Line 144 and 147 say the same thing twice.** The Remarks bullet and the warning box both list `min`, `max`, `rename`, `currentDate`. The warning adds "Do not use `min()`, `max()`, `rename()`, or `currentDate()`" — a third repetition inside itself. + +**Line 157:** "The example schema has no array field, so these lines assume `tags String[]` on `User`." So the four code samples immediately below do not run against the schema the rest of the page uses. I would have copied them and got a failure. The note is above the code, which helps, but the samples should be marked. + +# Could I do what the page is for, after one reading? + +Partly. + +**I could:** write MongoDB filters with `MongoFieldFilter`, including `of()` for uncovered operators, and filter into an embedded object with a dot path. Update top-level fields with `set`, `unset`, `inc`, `mul`. Push and pull array elements. Consume an `AsyncIterableResult` both ways and know not to switch. + +**I would still not know:** + +- What to type for an `upsert()` — no complete example, and no answer for the embedded-field case it explicitly forbids. +- How to write the `u('address.city')` function-call form in real code, because no example uses it. +- Whether `updateAll()`, `createAll()`, and `deleteAll()` exist on PostgreSQL. +- Whether `.where()` on `upsert()` means the same thing it means on `update()`. +- What the full operation list is, because two sections give different lists without reconciling them. +- What to catch when an array operation fails on a non-array field. + +The filter half of this page is solid and I would ship code from it. The update half assumes I will read the opening paragraph three times, and it withholds the one example — `upsert()` — I would most likely need to look up. \ No newline at end of file From a3ba18816b2cd6c053195a7bdb1d41847e4e3b59 Mon Sep 17 00:00:00 2001 From: willbot Date: Sat, 12 Sep 2026 10:15:17 +0200 Subject: [PATCH 07/11] docs(orm8): orm-client reference, reader-review round four applied Last wording round. Readers tripped on round three's additions, so this round cut them back: the db.sql example under groupBy() is gone in favour of the link, the Decimal and BigInt aggregate rules are one table, and the comparison-method rules are one line per rule with the field types named. Facts looked up for the marks: firstOrThrow() reads every matching row, createAll() inserts before yielding, aggregate() honours an earlier limit(), every() on a to-one relation also matches a missing row, where({}) still throws ORM.WHERE_MISSING, a second for await throws RUNTIME.ITERATOR_CONSUMED. 2,049 to 2,048 lines. Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- .../content/docs/orm/reference/orm-client.mdx | 269 +++++++++--------- .../c21-orm-client/part1.round4.md | 81 ++++++ .../c21-orm-client/part2.round4.md | 90 ++++++ .../c21-orm-client/part3.round4.md | 81 ++++++ .../c21-orm-client/part4.round4.md | 86 ++++++ 5 files changed, 472 insertions(+), 135 deletions(-) create mode 100644 docs/orm-docs-audit/c21-orm-client/part1.round4.md create mode 100644 docs/orm-docs-audit/c21-orm-client/part2.round4.md create mode 100644 docs/orm-docs-audit/c21-orm-client/part3.round4.md create mode 100644 docs/orm-docs-audit/c21-orm-client/part4.round4.md diff --git a/apps/docs/content/docs/orm/reference/orm-client.mdx b/apps/docs/content/docs/orm/reference/orm-client.mdx index 30f7b90ffbc..1dcb1ee05fc 100644 --- a/apps/docs/content/docs/orm/reference/orm-client.mdx +++ b/apps/docs/content/docs/orm/reference/orm-client.mdx @@ -8,11 +8,11 @@ metaDescription: 'Reference for the Prisma ORM client''s query, mutation, filter The ORM client gives you model-level methods for reading and writing data across PostgreSQL and MongoDB. This page documents every method, its availability on each database, and the behavior that differs between the two. Each method's Remarks say which databases it works on, how it behaves differently on one of them, and where TypeScript rejects something the database itself would accept. -This page assumes a project where `prisma orm init` has already run and your tables exist: [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7) has the packages to install, `prisma orm init`, and `prisma db update`, which replaces `prisma migrate`, and [How migrations work](/orm/migrations/how-migrations-work) creates the tables. For task-oriented walkthroughs, see the Fundamentals guides: [Reading data](/orm/fundamentals/reading-data), [Writing data](/orm/fundamentals/writing-data), [Relations and joins](/orm/fundamentals/relations-and-joins), and [Transactions](/orm/fundamentals/transactions). +This page assumes your tables already exist. [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7) has the packages to install and the `prisma orm init` command. It also covers `prisma db update`, which replaces `prisma migrate`, and `prisma contract emit`, which replaces `prisma generate`. [How migrations work](/orm/migrations/how-migrations-work) shows how the tables get created. For task-oriented walkthroughs, see the Fundamentals guides: [Reading data](/orm/fundamentals/reading-data), [Writing data](/orm/fundamentals/writing-data), [Relations and joins](/orm/fundamentals/relations-and-joins), and [Transactions](/orm/fundamentals/transactions). ## 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). Your schema now lives in `contract.prisma`, the contract, which replaces `schema.prisma` from Prisma ORM 7. 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`. 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/`.
@@ -177,10 +177,11 @@ model Tutorial { Some parts of the schema the examples rely on: +- The `types` block at the top gives a name to a type from an extension package, so your models can use that name. `Embedding1536` comes from the pgvector extension, and `pgvector.` is that package's prefix. See [PostgreSQL](#postgresql) for the extra line it needs when you create the client. - `@@map("user")` sets the table or collection name. Without it, the table name is the model name with a lowercase first letter. - On MongoDB, `id ObjectId @id @map("_id")` renames the field to `_id` everywhere: you filter on `_id`, and the returned document has an `_id` key. That is why the MongoDB examples never say `id`. - `Uuid` is a built-in type for a PostgreSQL `uuid` column. You do not import it. `@default(uuid())` fills the id in when you insert a row. -- `@@type("pg/text@1")` on an `enum` block stores the enum as a text column. Always write it exactly like that. Other versions do not exist yet. The MongoDB form of the same attribute is `@@type("mongo/string@1")`. This page calls an enum written this way a text-backed enum. +- `@@type("pg/text@1")` on an `enum` block stores the enum as a text column. The `@1` is the version of that storage format, and only version 1 exists today, so write it exactly like that. The MongoDB form of the same attribute is `@@type("mongo/string@1")`. This page calls an enum written this way a text-backed enum. - A `native_enum` block stores the same members as a real PostgreSQL enum type instead. Give every member a value. A member with no value is rejected. This page calls an enum written this way a native enum. The two sort differently; see [`orderBy()`](#orderby). ```prisma @@ -192,17 +193,17 @@ Some parts of the schema the examples rely on: ``` - An enum member's stored value is the string after `=`, or the member name when there is no `=`. That string is what you pass to `.eq()`. So `Urgent = "urgent"` is matched by `.eq('urgent')`, and a bare `admin` member is matched by `.eq('admin')`. -- A variant is a model that reuses another model's fields and rows. `@@base(Task, "bug")` makes `Bug` a variant of `Task`. Its second argument, `"bug"`, is the value stored in the discriminator column for a `Bug` row. `@@discriminator(type)` names that column. `variant('Bug')` narrows a collection to that variant. Pass the variant's model name, not the string in `@@base`. +- A variant is a model that reuses another model's fields and rows. `@@base(Task, "bug")` makes `Bug` a variant of `Task`. Its second argument, `"bug"`, is the value stored in the discriminator column for a `Bug` row. `@@discriminator(type)` names that column. `db.orm.public.Task.variant('Bug')` returns only the `Bug` rows. Pass the model name, `'Bug'`, not the string in `@@base`. - `PostTag` is required. To join `Post.tags Tag[]` and `Tag.posts Post[]`, write a third model with one foreign key to each side and an `@@id` of exactly those two columns. Prisma ORM finds it for you, and neither `Post` nor `Tag` names it. A pair of list fields with no such model is rejected. See [Relations and joins](/orm/fundamentals/relations-and-joins#many-to-many). -- The `types` block gives a name to a type from an extension package, so your models can use that name. `Embedding1536` comes from the pgvector extension. See [PostgreSQL](#postgresql) for the extra line it needs when you create the client. +- On PostgreSQL, a `DateTime` field comes back as a `Temporal.Instant`, the standard JavaScript object for a point in time. A filter on that field takes the same type, so you can pass a value straight back in. ## 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 SQL. This page covers none of those three. 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 SQL. 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 -Create a PostgreSQL client with `postgres(...)`. `db.orm` holds your models by model name, grouped by database schema: `db.orm.public.User`, `db.orm.public.Post`. `public` is the PostgreSQL schema your tables are in unless you put them in another one. The import path `./contract.d` is written exactly like that, with no `.ts` on the end, and it resolves to the emitted `contract.d.ts`. +Create a PostgreSQL client with `postgres(...)`. `db.orm` holds your models by model name, grouped by database schema: `db.orm.public.User`, `db.orm.public.Post`. `public` is the PostgreSQL schema your tables are in unless you put them in another one. Import types from `./contract.d`, which resolves to `contract.d.ts`. ```typescript import postgres from '@prisma/orm-postgres/runtime'; @@ -226,11 +227,11 @@ const db = postgres({ }); ``` -To give a model your own methods on top of the built-in ones, import `Collection` from `@prisma/orm-postgres/orm-client`, subclass it, and register the subclass with `orm(...)` from that same package. You still create the client with `postgres(...)`, and `orm(...)` takes two things from that client. [Custom `Collection` subclass](#custom-collection-subclass) has the code. +To give a model your own methods on top of the built-in ones, import `Collection` from `@prisma/orm-postgres/orm-client`, subclass it, and register the subclass with `orm(...)` from that same package. You still create the client with `postgres(...)`. `orm(...)` takes two things from it: the `runtime` argument, which is what `await client.connect()` gives you, and the `context` argument, which is `client.context`. The code is under [Custom `Collection` subclass](#custom-collection-subclass). ### MongoDB -Create a MongoDB client with `mongo(...)`. `db.orm` holds your models by collection name, with no schema in between: `db.orm.users`, `db.orm.posts`, not `db.orm.User`. The collection name is the one you set with `@@map`, or the model name with a lowercase first letter when you did not set one. +Create a MongoDB client with `mongo(...)`. `db.orm` holds your models by collection name, with no schema in between: `db.orm.users`, `db.orm.posts`, not `db.orm.User`. The collection name is the one you set with `@@map`, or the model name with a lowercase first letter when you did not set one. `dbName` is the MongoDB database name. ```typescript import mongo from '@prisma/orm-mongo/runtime'; @@ -242,11 +243,9 @@ const db = mongo({ contractJson, url: process.env.MONGODB_URL, dbName: const users = await db.orm.users.all(); ``` -The examples below follow each database's own form: `db.orm.public.User` on PostgreSQL, `db.orm.users` on MongoDB. - ## 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(...)`. On MongoDB, `users` in `db.orm.users` is also the name of the database collection. 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. 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). @@ -273,7 +272,7 @@ Restrict a query to rows matching a filter. #### Options -| Name | Type | Required | Description | +| Argument | Type | Required | Description | |---|---|---|---| | `filter` | A callback that calls an operator on a column, a shorthand object, or (MongoDB) a `MongoFieldFilter` | Yes | The condition rows must satisfy. | @@ -331,11 +330,11 @@ Return only the scalar fields you name. - Available for PostgreSQL and MongoDB. - On PostgreSQL, `select()` narrows the returned row shape at the type level: fields you didn't select are absent from the result type. -- On MongoDB, `select()` changes what the database returns but not the TypeScript type. Only the fields you named come back. The type still lists every field on the model, so read only the fields you named from each returned document and treat the rest as absent. +- On MongoDB, `select()` changes what the database returns but not the TypeScript type. Only the fields you named come back. The type still lists every field on the model. Read a field you did not name and you get `undefined`, because the returned document does not carry it. #### Options -| Name | Type | Required | Description | +| Argument | Type | Required | Description | |---|---|---|---| | `...fields` | Field names (`string`) | Yes | One or more scalar field names to keep. | @@ -366,12 +365,12 @@ Eagerly load a relation onto the returned rows. - Available for PostgreSQL and MongoDB, with differences noted below. - On PostgreSQL, `include(relationName, refineFn?)` loads both to-one and to-many relations. The optional callback receives the related rows as a collection, so you can filter, order, limit, and aggregate them. See [Refinements, aggregates, and combine](#refinements-aggregates-and-combine). -- On MongoDB, a document's own `_id` comes back as a hex string. The `_id` on a related document loaded by `include()` comes back as an `ObjectId` object instead. Wrap that one in `String(...)` before you compare it. +- On MongoDB, wrap the `_id` of a related document loaded by `include()` in `String(...)` before you compare it. It comes back as an `ObjectId` object, while a document's own `_id` comes back as a hex string. - On MongoDB, `include()` loads a relation that is stored as a reference. It takes the relation name and nothing else, and TypeScript rejects a second argument. #### Options -| Name | Type | Required | Description | +| Argument | Type | Required | Description | |---|---|---|---| | `relationName` | `string` | Yes | The relation to load. | | `refineFn` | A callback that returns the narrowed relation or an aggregate of it | No | PostgreSQL only. Narrows the loaded relation, or reduces it to one value. | @@ -409,25 +408,25 @@ 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. 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. 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 - PostgreSQL only. On MongoDB, `include()` takes no callback, and `count`, `sum`, `avg`, `min`, `max`, and `combine` do not exist on a collection. - On PostgreSQL, those six are only callable **inside** an `include()` callback. Called anywhere else, they throw an error whose `code` is `ORM.INCLUDE_INVALID`. -- `sum()` and `avg()` take a numeric column: an integer, a floating-point number, or a decimal. They also take a `Time` column. They do not take a date or a timestamp, and TypeScript rejects a date or timestamp field name here. +- `sum()` and `avg()` take a numeric column: an integer, a floating-point number, or a decimal. They also take a `Time` column, the time-of-day type. They do not take a date or a timestamp, and TypeScript rejects a date or timestamp field name here. - `min()` and `max()` take more: numeric and text columns, dates, times, timestamps, intervals, IP addresses, and text arrays. They do not take a boolean, a `uuid`, binary data, a bit string, or JSON. - `count()` returns a `number`. With no argument it counts rows; with a field name it counts rows where that field is not null. `countBigInt()` returns a `bigint` instead, for a count too large for a JavaScript `number`. - `sum()` over an integer column returns a `number`. A total too large for a `number` throws an error whose `code` is `RUNTIME.DECODE_FAILED`. Use `sumBigInt(field)` for totals that large. It returns a `bigint`. - `avg()` over an integer column returns a JavaScript floating-point `number`. `avgDecimal(field)` returns the exact average as a decimal string. Pass that string to a decimal library, or call `Number(...)` on it when a floating-point value is fine. - When a to-many relation has no rows, `sum()`, `avg()`, `min()`, and `max()` come back as `null`, not `0`. -- `combine(shape)` takes an object. You choose the keys. Call `posts.combine({ ... })`, and inside the object write `posts` again for each value. It is the same object every time. A value that chains more methods on `posts` comes back as an array of rows. A value that is an aggregate, such as `posts.count()`, comes back as that single value. +- `combine(shape)` takes an object. You choose the keys. Call `posts.combine({ ... })`, and inside the object write `posts` again for each value. The `posts` inside the object is the same collection the callback received, with nothing chained on it yet. A value that chains more methods on `posts` comes back as an array of rows. A value that is an aggregate, such as `posts.count()`, comes back as that single value. #### Options -| Name | Type | Required | Description | +| Argument | Type | Required | Description | |---|---|---|---| -| `filter` / `orderBy` / `limit` / `offset` | Chained on the nested collection | No | Refine which related rows load. | +| `where()` / `orderBy()` / `limit()` / `offset()` | Chained on the nested collection | No | Refine which related rows load. | | `count()` | Aggregate, no arguments | No | Reduces the relation to a row count. | | `sum(field)` / `avg(field)` | Aggregate over a numeric field | No | Reduces the relation to one number. | | `min(field)` / `max(field)` | Aggregate over a sortable field | No | Reduces the relation to its smallest or largest value, in the column's own type. | @@ -494,9 +493,9 @@ 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, add a polyfill: `import 'temporal-polyfill/full/global'`. +`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'`. -`sum('createdAt')` and `avg('createdAt')` do not compile. PostgreSQL has no `SUM` or `AVG` over a date or a timestamp. +`sum('createdAt')` and `avg('createdAt')` do not compile. ::: ### `orderBy()` @@ -508,11 +507,11 @@ Sort the result set. - Available for PostgreSQL and MongoDB, with different argument shapes. - On PostgreSQL, `orderBy()` takes a callback that returns `.asc()` or `.desc()` on one column. Pass an array of such callbacks to sort by more than one column. - On MongoDB, `orderBy()` takes an object instead: `{ field: 1 }` sorts ascending and `{ field: -1 }` sorts descending. -- On PostgreSQL, a native enum column sorts in the order its members are declared, not alphabetically. The example schema declares `Priority` text-backed, so it sorts by its stored text: `'high'`, then `'low'`, then `'urgent'`. Declare the same members in a `native_enum Priority` block instead and `.asc()` sorts them `Low`, `High`, `Urgent`. Do that when the sort order matters. +- The example schema declares `Priority` text-backed, so on PostgreSQL it sorts by its stored text: `'high'`, then `'low'`, then `'urgent'`. A native enum column sorts in the order its members are declared instead, so the same members in a `native_enum Priority` block sort `Low`, `High`, `Urgent`. Use a native enum when the sort order matters. #### Options -| Name | Type | Required | Description | +| Argument | Type | Required | Description | |---|---|---|---| | `sort` | Callback `(fields) => f.field.asc() \| .desc()`, an array of such callbacks (PostgreSQL), or a `{ field: 1 \| -1 }` object (MongoDB) | Yes | The sort key(s) and direction(s). | @@ -556,7 +555,7 @@ Limit the number of returned rows. #### Options -| Name | Type | Required | Description | +| Argument | Type | Required | Description | |---|---|---|---| | `count` | `number` | Yes | Maximum number of rows to return. | @@ -589,7 +588,7 @@ Offset into the ordered result set. #### Options -| Name | Type | Required | Description | +| Argument | Type | Required | Description | |---|---|---|---| | `count` | `number` | Yes | Number of rows to skip. | @@ -618,12 +617,12 @@ Resume pagination from a known position. #### Remarks - PostgreSQL only. `cursor()` does not exist on a MongoDB collection. -- Always call `orderBy()` before `cursor()`. TypeScript rejects the call if you do not. The library does not check this while the query runs, so code that skips TypeScript, such as plain JavaScript or a cast, gets no error: the cursor is ignored and every row comes back. -- The cursor object must name every column the `orderBy()` sorts on. Leave one out and the call throws an error whose `code` is `ORM.CURSOR_VALUE_MISSING`. With two sort columns, give both keys. The rows are compared as a pair, so you get the rows past the first column's value, plus the rows with that same first value and past the second. +- Always call `orderBy()` before `cursor()`. TypeScript rejects the call if you do not. Nothing checks this while the query runs, so without the `orderBy()` the cursor is ignored and every row comes back. +- The cursor object must name every column the `orderBy()` sorts on. Leave one out and the call throws an error whose `code` is `ORM.CURSOR_VALUE_MISSING`. With two sort columns, give both keys. The cursor compares the sort columns in order: first by the first column, then by the second for rows that tie on the first. #### Options -| Name | Type | Required | Description | +| Argument | Type | Required | Description | |---|---|---|---| | `values` | Object of the `orderBy()` key(s) and their values | Yes | The position to resume after. | @@ -655,10 +654,11 @@ Remove duplicate rows, comparing only the fields you name. - PostgreSQL only. `distinct()` does not exist on a MongoDB collection. - You do not need `select()`. `distinct('priority')` keeps one whole row per distinct `priority` value, whatever the query returns. +- Which of the tied rows it keeps is not defined. Call `orderBy()` first to choose, as [`distinctOn()`](#distincton) does. #### Options -| Name | Type | Required | Description | +| Argument | Type | Required | Description | |---|---|---|---| | `...fields` | Field names (`string`) | Yes | The fields to deduplicate on. | @@ -673,8 +673,8 @@ Remove duplicate rows, comparing only the fields you name. ##### Deduplicate on a field (PostgreSQL) ```typescript -const priorities = await db.orm.public.Post.select('priority').distinct('priority').all(); -// priorities holds one row per distinct priority value, such as { priority: 'low' } +const priorities = await db.orm.public.Post.distinct('priority').all(); +// priorities holds one whole Post row per distinct priority value ``` ### `distinctOn()` @@ -685,11 +685,11 @@ Keep the first row per key according to `orderBy()`. - PostgreSQL only. `distinctOn()` does not exist on a MongoDB collection. - Call `orderBy()` first, so that "the first row per key" means something. TypeScript rejects the call if you do not. -- Start the `orderBy()` with the `distinctOn()` columns, as the example does. PostgreSQL rejects a query whose `ORDER BY` does not begin with the `DISTINCT ON` columns. The library does not check this, so a mismatch fails in the database. +- Start the `orderBy()` with the same columns you pass to `distinctOn()`, as the example does. Sort by anything else first and the query fails in the database. #### Options -| Name | Type | Required | Description | +| Argument | Type | Required | Description | |---|---|---|---| | `...fields` | Field names (`string`) | Yes | The key field(s) to keep the first row of. | @@ -717,13 +717,13 @@ Narrow a model to one of the variants declared with `@@base`. #### Remarks - Available for PostgreSQL and MongoDB. -- Pass the variant's model name, such as `'Bug'`, not the string in its `@@base` attribute. -- On PostgreSQL, a variant that sets its own `@@map`, as `Bug` and `Feature` do, gets its own table. That changes how some writes behave; see [`createAndCount()`](#createandcount) and [`upsert()`](#upsert). +- Pass the variant's model name, such as `'Bug'`, not the string in its `@@base` attribute. On MongoDB you pass the model name too, even though the accessor before it is the collection name. +- On PostgreSQL, a variant that sets its own `@@map`, as `Bug` and `Feature` do, is stored in its own table. Read [`createAndCount()`](#createandcount) and [`upsert()`](#upsert) before you write to such a variant. - On MongoDB, every variant is a document in the one collection, told apart by the field named in `@@discriminator`. #### Options -| Name | Type | Required | Description | +| Argument | Type | Required | Description | |---|---|---|---| | `variantName` | `string` | Yes | The model name of the variant. | @@ -749,15 +749,14 @@ const tutorials = await db.orm.posts.variant('Tutorial').all(); 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). -There is no `count()` method on either database. On PostgreSQL, count with `aggregate((a) => ({ n: a.count() }))`. On MongoDB, count with `db.query`, the [pipeline builder](/orm/fundamentals/advanced-queries#mongodb-pipeline-builder): +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: ```typescript const built = db.query.from('posts').count('total').build(); -const [counted] = await (await db.runtime()).query(built); // counted.total is the count +const runtime = await db.runtime(); +const [counted] = await runtime.query(built); // counted.total is the count ``` -Both are shown under [Count records](/orm/fundamentals/reading-data#count-records). - For a task-oriented walkthrough, see [Reading data](/orm/fundamentals/reading-data). ### `all()` @@ -768,8 +767,7 @@ Resolve the query to every matching row. - Available for PostgreSQL and MongoDB. - `all()` returns an [`AsyncIterableResult`](#asynciterableresult): you can `await` it to collect an array, or use `for await` to stream rows one at a time. -- `await`ing a result you have already `await`ed is safe. You get the same array back, and the query does not run again. -- Each result can be used one way only. Awaiting it and then looping it with `for await`, or the reverse, throws an error whose `code` is `RUNTIME.ITERATOR_CONSUMED`. The rules for awaiting versus looping are under [Single consumption and mode switching](#single-consumption-and-mode-switching). +- `await`ing a result you have already `await`ed is safe. You get the same array back, and the query does not run again. Each result can be used one way only, so awaiting it and then looping it with `for await`, or the reverse, throws an error whose `code` is `RUNTIME.ITERATOR_CONSUMED`. See [Single consumption and mode switching](#single-consumption-and-mode-switching). - `orderBy()` is written differently on each database. See [`orderBy()`](#orderby). #### Options @@ -782,6 +780,8 @@ Resolve the query to every matching row. |---|---|---| | `AsyncIterableResult` | `await db.orm.public.User.all()` | Awaitable to `Row[]`, or iterable with `for await` for streaming. | +`Row` is the model's row type, from the emitted `contract.d.ts`. + #### Examples ##### Await to collect an array @@ -803,7 +803,7 @@ for await (const user of db.orm.public.User.orderBy((u) => u.email.asc()).all()) ``` ```typescript tab="MongoDB" -for await (const post of db.orm.posts.orderBy({ createdAt: 1 }).all()) { +for await (const post of db.orm.posts.orderBy({ createdAt: 1 }).all()) { // 1 ascending, -1 descending console.log(post.title); } ``` @@ -822,8 +822,7 @@ Resolve the query to the first matching row, or `null` if none matches. #### Remarks - Available for PostgreSQL and MongoDB. -- On PostgreSQL, `first()` accepts an inline filter: a shorthand object or a callback (`first((p) => p.priority.eq('urgent'))`). -- On PostgreSQL, an inline filter is added to any earlier `where()` call. Both have to match. +- On PostgreSQL, `first()` accepts an inline filter: a shorthand object or a callback (`first((p) => p.priority.eq('urgent'))`). An inline filter is added to any earlier `where()` call, and both have to match. - On MongoDB, `first()` takes no filter argument. Filter with `where(...)` first, then call `first()`. #### Options @@ -862,7 +861,9 @@ For Prisma ORM 7 users, `findUnique` and `findFirst` map onto `first()`: + const alice = await db.orm.public.User.where({ email }).all().firstOrThrow(); ``` -`first()` returns `null` on no match and does not check that only one row matched. `findUniqueOrThrow` and `findFirstOrThrow` both map onto `all().firstOrThrow()`, as the last line above shows. `firstOrThrow()` is a method on the result that `all()` gives you, and calling it does not count as using the result a second time. It throws an error whose `code` is `RUNTIME.NO_ROWS` when no row matched. +`first()` returns `null` on no match and does not check that only one row matched. `findUniqueOrThrow` and `findFirstOrThrow` both map onto `all().firstOrThrow()`, as the last line above shows. + +`firstOrThrow()` is a method on the result that `all()` gives you, so calling it is not a second use of that result. It reads every matching row into memory and returns the first one, and it throws an error whose `code` is `RUNTIME.NO_ROWS` when nothing matched. ### Custom `Collection` subclass @@ -871,8 +872,8 @@ On PostgreSQL you can subclass `Collection` to add your own methods to a model, #### Remarks - PostgreSQL only. You cannot subclass a collection on MongoDB. -- `db.orm` cannot use your subclass. Call the `orm(...)` function with a `collections` object to get a second accessor with the same methods as `db.orm.public`. You now have two accessors, both of them work, and your existing `db.orm.public.X` calls are unaffected. Use the one `orm(...)` returns for the models you subclassed. -- `orm(...)` takes two things from the client that `postgres(...)` created under [Setting up the client](#setting-up-the-client). `client.connect()` gives you the `runtime` argument, and `client.context` is the `context` argument. +- `db.orm` cannot use your subclass. Call the `orm(...)` function with a `collections` object to get a second accessor with the same methods as `db.orm.public`. Use that accessor for the models you subclassed, and keep `db.orm.public` for every other model. Your existing `db.orm.public.X` calls keep working. +- `client` in the example below is what `postgres(...)` returns, the same object [Setting up the client](#setting-up-the-client) calls `db`. The example creates one so that it runs on its own. - `Collection` takes two type arguments: your contract type, and the name of the model this collection is for. - A method that returns `this.variant('Bug')` gives back a collection narrowed to the `Bug` model. It is chainable, so you finish it with `all()`, `first()`, or any other method. @@ -913,10 +914,12 @@ const features = await tasks.Task.features().all(); 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. -For a task-oriented walkthrough, see [Writing data](/orm/fundamentals/writing-data). For grouping several writes into one unit, see [Transactions](/orm/fundamentals/transactions). +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). There is no `skipDuplicates` option. + +To catch one of the errors named below and check its `code`, copy the `catch` block under [`AsyncIterableResult`](#asynciterableresult). For a task-oriented walkthrough, see [Writing data](/orm/fundamentals/writing-data). 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 you only get this protection from TypeScript. Without a filter, `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()`. 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. @@ -930,7 +933,7 @@ Insert a single row and return it. - Available for PostgreSQL and MongoDB. - On PostgreSQL, you may leave out any field your contract gives a default, including the id. Passing an explicit id is accepted. Nullable fields can be left out too. Every other field is required. - On MongoDB, `_id` is the only field you can leave out. Give every other field a value, and write `null` for a field you want empty. -- On MongoDB, `create()` returns the values you passed plus the `_id` the server assigned, not the stored document. On PostgreSQL the returned row is read from the database. +- On MongoDB, `create()` returns the values you passed plus the `_id` the server assigned, not the stored document. Read the row back if you need a value the database filled in. On PostgreSQL the returned row is read from the database already. - On PostgreSQL, `create()` can create or link related rows in the same transaction. Nest `create()` on the side that does not hold the foreign key, and `connect()` on the side that does. - Nested `create()` and `connect()` are not available on MongoDB. @@ -940,7 +943,7 @@ Insert a single row and return it. |---|---|---|---| | The row's fields | Object of field values | Yes | The row to insert. Pass the object as the only argument. There is no `data` wrapper. | -On PostgreSQL, that object can also carry a `create` or `connect` callback for a related row, as shown below. `posts.create([...])` and `tags.disconnect([...])` take arrays because those relations are to-many, and `user.connect({...})` takes one object because `Post.user` is to-one. +On PostgreSQL, that object can also carry a `create` or `connect` callback for a related row, as shown below. `posts.create([...])` and `tags.disconnect([...])` take arrays because a user can have many posts and a post can have many tags. `user.connect({...})` takes one object because a post has one user. #### Return type @@ -1002,6 +1005,7 @@ Insert multiple rows and return them. - Available for PostgreSQL and MongoDB. - Returns an [`AsyncIterableResult`](#asynciterableresult): `await` for an array, or `for await` to stream inserted rows. +- On PostgreSQL every row is inserted by one statement before the first row reaches you. Stopping the loop early does not undo an insert. #### Options @@ -1037,9 +1041,8 @@ Insert rows and return how many were inserted, without reading them back. #### Remarks - Available for PostgreSQL and MongoDB. -- On PostgreSQL, `createAndCount()` does not work on a variant that is stored in its own table. Use `createAll()` instead. -- A variant is stored in its own table when its `@@map` names a different table than its base model does. `Bug` in the [example schema](#example-schema) is one: it maps to `bug` while `Task` maps to `task`. -- Calling it on such a variant throws an error whose `code` is `ORM.OPERATION_UNSUPPORTED`, with the message `createAndCount() is not supported for MTI variant "Bug" on model "Task". Use createAll() instead.` +- On PostgreSQL, `createAndCount()` does not work on a variant that is stored in its own table, meaning its `@@map` names a different table than its base model does. `Bug` in the [example schema](#example-schema) is one: it maps to `bug` while `Task` maps to `task`. Use `createAll()` instead. +- Calling it on such a variant throws an error whose `code` is `ORM.OPERATION_UNSUPPORTED`, with the message `createAndCount() is not supported for MTI variant "Bug" on model "Task". Use createAll() instead.` MTI in the message means multi-table inheritance, the variant stored in its own table described above. - On MongoDB, a variant is one document in one collection, so `createAndCount()` works on a variant as usual. #### Options @@ -1085,17 +1088,17 @@ Update the matched row and return it, or `null` if none matches. - Available for PostgreSQL and MongoDB. Requires a prior `where()` (see the warning at the top of this section). - On MongoDB, `update()` takes an object of field values, or a callback that returns field operations, such as `(p) => [p.content.set('Rewritten')]`. See [Field update operations](#field-update-operations). -- On MongoDB, the update input covers the base model's fields only. Putting a field declared on a variant in the update, such as `duration` on `Tutorial`, makes TypeScript complain even though the update runs correctly. Put `// @ts-expect-error` on the line directly above `.update(...)`. The same is true of `updateAll()` and `updateAndCount()`. +- On MongoDB, the update input covers the base model's fields only. Putting a field declared on a variant in the update, such as `duration` on `Tutorial`, makes TypeScript complain even though the update runs correctly. Put `// @ts-expect-error` on the line directly above the line TypeScript flags. When the complaint goes away, TypeScript reports the comment itself as unused, so delete it then. The same is true of `updateAll()` and `updateAndCount()`. ```typescript // @ts-expect-error duration is a Tutorial field, not a Post field .update((t) => [t.duration.inc(10)]); ``` -- On PostgreSQL, `update()` takes an object of field values only. Passing a function changes nothing and returns `null`. +- Take care on PostgreSQL: `update()` takes an object of field values only, and passing a function instead writes nothing, reports nothing, and returns `null`. - On PostgreSQL, `update()` can also relink related rows. `connect()` points the foreign key at a different row, and `disconnect()` removes a many-to-many link without deleting either row. - Returns `null` when no row matches. On PostgreSQL it also returns `null` when the object you pass is empty and has no keys at all. An object whose values already match the row is different: that update runs and returns the row. -- On PostgreSQL, `select()` and `include()` before `.update()` choose which fields and relations the returned row carries. They change nothing about what is written. +- On PostgreSQL, [`select()`](#select) and [`include()`](#include) before `.update()` choose which fields and relations come back on the returned row. They change nothing about what is written. #### Options @@ -1166,9 +1169,8 @@ Update every matching row and collect the results. - Available for PostgreSQL and MongoDB. Call `where()` first. Without a filter this changes every row. See the warning at the top of this section. - On PostgreSQL, `updateAll()` runs as one statement, so every matching row changes together. -- On MongoDB, `updateAll()` is not a single operation, and two things follow. A document that starts matching your filter only once the call is under way is updated but not returned to you. A document that someone else changes during the call comes back carrying their values, not yours. -- That happens because the update runs by filter, while the read-back runs by the ids collected before the update. -- Prisma ORM has no `db.transaction()` on MongoDB, so you cannot avoid this from the client. To control it, share one `MongoClient` with the MongoDB driver and group the writes in a driver session, shown under [Transactions on MongoDB](/orm/fundamentals/transactions#transactions-on-mongodb). +- On MongoDB, `updateAll()` is not a single operation, and two things follow. If a document begins matching your filter while the call is running, it is updated but not included in the returned rows. If someone else changes a matched document while the call is running, you get their version back, not yours. +- Prisma ORM has no `db.transaction()` on MongoDB, so you cannot avoid this from the client. To control it, share one `MongoClient` with the MongoDB driver and group the writes in one of the driver's own sessions, shown under [Transactions on MongoDB](/orm/fundamentals/transactions#transactions-on-mongodb). #### Options @@ -1302,8 +1304,7 @@ Remove every matching row and return the count. #### Remarks -- Available for PostgreSQL and MongoDB. Call `where()` first. Without a filter this deletes every row. See the warning at the top of this section. -- Returns `0` when no row matches. +- Available for PostgreSQL and MongoDB. Call `where()` first. Without a filter this deletes every row. See the warning at the top of this section. Returns `0` when no row matches. #### Options @@ -1336,8 +1337,7 @@ Insert a row if none matches, otherwise update the existing row. - On MongoDB, call `where()` first. The filter decides which document is updated. - On PostgreSQL, `upsert()` never reads the filters. A `where()` before it is ignored, without an error. `conflictOn` decides which row counts as already existing. - `conflictOn` names the unique column that decides insert or update, such as `conflictOn: { label: 'typescript' }`. Only the column name is used. The value is required by the type and is then ignored, so pass any value of the right type. -- For a unique constraint on several columns together, name every one of those columns. The set you name has to match one `@@unique` constraint. -- Leave `conflictOn` out to use the primary key instead. +- For a unique constraint on several columns together, name every one of those columns, as in `conflictOn: { orgId: '', email: '' }`. The set you name has to match one `@@unique` constraint. - When a row is inserted on PostgreSQL, the `create` side is used as you wrote it. - When a row is inserted on MongoDB, a field that appears in both `create` and `update` takes the `update` value. - On MongoDB, the `update` side takes an object of field values or a field-operations callback. @@ -1369,14 +1369,14 @@ Insert a row if none matches, otherwise update the existing row. const inserted = await db.orm.public.Tag.upsert({ create: { label: 'brand-new' }, update: { label: 'brand-new-updated' }, - conflictOn: { label: 'ignored' }, // only the column name `label` is used + conflictOn: { label: 'brand-new' }, // only the column name `label` is used }); // Update: a row with label 'typescript' already exists, so the update runs against it. const updated = await db.orm.public.Tag.upsert({ create: { label: 'typescript' }, update: { label: 'typescript-renamed' }, - conflictOn: { label: 'ignored' }, + conflictOn: { label: 'typescript' }, }); ``` @@ -1432,7 +1432,7 @@ For Prisma ORM 7 users, `upsert()` replaces the `where` argument with `conflictO ## Grouped aggregates -PostgreSQL supports aggregation over a result set. `aggregate()` treats the whole result set as one group. `groupBy().aggregate()` aggregates per group, and `having()` filters those groups. None of these methods exist on a MongoDB collection. 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. `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). The examples below use a separate `Customer` / `Order` schema, where `Order.amount` is a numeric column: @@ -1469,23 +1469,32 @@ model Order { ### `aggregate()` -Compute aggregates over the current result set. `aggregate()` runs the query, so you `await` it directly instead of calling `.all()`. +Compute aggregates over the rows the query matches. `aggregate()` runs the query, so you `await` it directly instead of calling `.all()`. #### Remarks - PostgreSQL only. Not available on MongoDB. -- `count()` counts rows. It takes no argument and always returns a number, `0` over an empty set. +- `count()` counts rows. It takes no argument and always returns a number, `0` when no rows match. - `count(field)` counts the rows where that field is not null. -- `sum()`, `avg()`, `min()`, and `max()` return `null`, not `0`, over an empty result set. Handle the `null`. `sum('amount')` over an `Int` column is typed `number | null`. -- `sum()` and `avg()` are offered on `Decimal` and `BigInt` columns. Over a `Decimal` column they return the exact value as a string. Over a `BigInt` column `sum()` returns a `number` and throws an error whose `code` is `RUNTIME.DECODE_FAILED` if the total is past `Number.MAX_SAFE_INTEGER`. Use `sumBigInt()` on a `BigInt` column whose total can grow that large. +- To count or sum the related rows of each parent row, use the `include()` callback instead; see [Refinements, aggregates, and combine](#refinements-aggregates-and-combine). +- `sum()`, `avg()`, `min()`, and `max()` return `null`, not `0`, when no rows match. Handle the `null`. +- A `limit()` or `offset()` earlier in the chain is applied first. The aggregate then covers only the rows they kept. +- What `sum()` and `avg()` give you back depends on the column type, as the table below shows. + +| Column type | `sum()` returns | `avg()` returns | Exact variant | +|---|---|---|---| +| `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. #### Options -| Name | Type | Required | Description | +| Argument | Type | Required | Description | |---|---|---|---| | `selector` | Callback `(agg) => ({ total: agg.sum('amount') })` | Yes | The aggregates to compute. Each key you return becomes a key of the result object. | -The callback's `agg` argument gives you `count()`, `count(field)`, `sum(field)`, `avg(field)`, `min(field)`, and `max(field)`. Three more methods avoid JavaScript's `number` precision limits. `agg.countBigInt()` takes no argument and returns a `bigint`. `agg.sumBigInt('amount')` returns a `bigint`. `agg.avgDecimal('amount')` returns the average as a string, such as `'10.3333333333333333'`. +The callback's `agg` argument gives you `count()`, `count(field)`, `sum(field)`, `avg(field)`, `min(field)`, and `max(field)`, plus `countBigInt()`, `sumBigInt(field)`, and `avgDecimal(field)`. `agg.countBigInt()` takes no argument and returns a `bigint`. #### Return type @@ -1564,14 +1573,14 @@ Group rows by one or more fields, then aggregate per group. - PostgreSQL only. Not available on MongoDB. - Pass field names as separate arguments: `groupBy('customerId', 'channel')`. -- `groupBy(...)` returns a `GroupedCollection`, which you never import or name yourself. You cannot `await` it. Call `.aggregate(...)` on it, which runs the query and gives you one row per group. Each row holds the grouping fields, typed as they are on your model, plus the keys you returned from the aggregate callback. +- `groupBy(...)` returns a `GroupedCollection`. You cannot `await` it. Call `.aggregate(...)` on it, which runs the query and gives you one row per group. Each row holds the grouping fields, typed as they are on your model, plus the keys you returned from the aggregate callback. - Chain order: `where(...)` comes before `groupBy(...)` and filters the rows that get grouped. After `groupBy(...)` you can call `having(...)`, `orderBy(...)`, `limit(...)`, and `offset(...)` in any order. `aggregate(...)` comes last. -- `orderBy(...)` on a grouped collection sorts the groups by a field you grouped on. You cannot sort groups by an aggregate value. To do that, write the query with the SQL query builder (`db.sql`). See the example below. -- `limit(n)` and `offset(n)` page the groups, and both require an `orderBy(...)` before them. Without one they are a TypeScript error. +- `orderBy(...)` on a grouped collection sorts the groups by a field you grouped on. You cannot sort groups by an aggregate value. To do that, write the query with the SQL query builder instead. See [Advanced queries](/orm/fundamentals/advanced-queries). +- `limit(n)` and `offset(n)` take a slice of the groups, and both require an `orderBy(...)` before them. Without one they are a TypeScript error. #### Options -| Name | Type | Required | Description | +| Argument | Type | Required | Description | |---|---|---|---| | `...fields` | One or more field names (`string`) | Yes | The fields to group by. | @@ -1579,7 +1588,7 @@ Group rows by one or more fields, then aggregate per group. | Return type | Example | Description | |---|---|---| -| `GroupedCollection` | `db.orm.public.Order.groupBy('customerId')` | Not a promise. Call `.aggregate(...)` on it to run the query. | +| `GroupedCollection` | `db.orm.public.Order.groupBy('customerId')` | Not a promise. Call `.aggregate(...)`. | #### Examples @@ -1602,18 +1611,6 @@ const firstTwoChannels = await db.orm.public.Order.groupBy('channel') .aggregate((agg) => ({ orderCount: agg.count() })); ``` -##### Sort the groups by an aggregate, with `db.sql` (PostgreSQL) - -```typescript -const topCustomers = db.sql.public.order - .select((f, fns) => ({ customerId: f.customerId, orders: fns.count() })) - .groupBy((f) => f.customerId) - .orderBy((f, fns) => fns.count(), { direction: 'desc' }) - .limit(5) - .build(); -const rows = await db.runtime().query(topCustomers); -``` - For Prisma ORM 7 users, `groupBy()` chains into `aggregate()` instead of taking a `by` array with aggregate keys: ```diff @@ -1627,14 +1624,14 @@ Filter groups by an aggregate comparison. #### Remarks -- PostgreSQL only. Not available on MongoDB. Call it on the result of `groupBy()`, before `aggregate()`. +- PostgreSQL only. Not available on MongoDB. Call it on the result of `groupBy()`. - The `having()` callback gives you `count()`, `count(field)`, `sum(field)`, `avg(field)`, `min(field)`, and `max(field)`. Call a comparison on the result: `h.sum('amount').gt(1000)`. The comparisons are `eq`, `neq`, `gt`, `lt`, `gte`, and `lte`. - For more than one condition, call `.having(...)` again. The conditions are combined with AND. - You cannot refer to a key you returned from `aggregate()` here. Write the aggregate out again in `having()`. #### Options -| Name | Type | Required | Description | +| Argument | Type | Required | Description | |---|---|---|---| | `predicate` | Callback `(h) => h.sum('amount').gt(1000)` | Yes | The condition each group must match. | @@ -1642,7 +1639,7 @@ Filter groups by an aggregate comparison. | Return type | Example | Description | |---|---|---| -| `GroupedCollection` | `db.orm.public.Order.groupBy('customerId').having(...)` | The same grouped collection with this condition added. Call `.aggregate(...)` to run the query. | +| `GroupedCollection` | `db.orm.public.Order.groupBy('customerId').having(...)` | Not a promise. Call `.aggregate(...)`. | #### Examples @@ -1656,18 +1653,23 @@ 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(...)`. Stacked `where()` calls are combined with AND, and you can use the shorthand object in one call and the callback in the next. 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. 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. 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 you build by calling a static method on the `MongoFieldFilter` class, 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). +- **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). ### Scalar comparisons (PostgreSQL) -Column-level comparison methods on a field. +Comparison methods on a field. #### Remarks -- PostgreSQL. The methods available on a field depend on the field's type. Autocomplete shows which ones apply. `isNull` and `isNotNull` are on every field. `eq`, `neq`, `in`, and `notIn` need a type that can be compared for equality. `gt`, `lt`, `gte`, and `lte` need a type that can be ordered. `like` and `ilike` need a text type. `Uuid` and `DateTime` can be compared for equality and ordered, so they have all of these except `like` and `ilike`. `Json` can do neither, so a `Json` field has only `isNull` and `isNotNull`. +- PostgreSQL. Which comparison methods a field has depends on its type. Autocomplete shows which ones apply. +- `isNull()` and `isNotNull()` are on every field. +- `eq`, `neq`, `in`, and `notIn` are on a field whose values can be compared for equality. +- `gt`, `lt`, `gte`, and `lte` are on a field whose values can be ordered. +- `like` and `ilike` are on a text field. +- `String` and enum fields have all of them. `Uuid` and `DateTime` have all but `like` and `ilike`. A `Boolean` field has only the equality methods, `isNull()`, and `isNotNull()`. A `Json` field has only `isNull()` and `isNotNull()`. - `like()` matches a SQL pattern case-sensitively. `ilike()` matches the same pattern case-insensitively. - `in([])` matches no rows. `notIn([])` matches every row. @@ -1709,7 +1711,7 @@ const caseInsensitive = await db.orm.public.User.where((u) => u.email.ilike('%@E ```typescript const lowOrHigh = await db.orm.public.Post.where((p) => p.priority.in(['low', 'high'])).all(); const notLowOrHigh = await db.orm.public.Post.where((p) => p.priority.notIn(['low', 'high'])).all(); -const withoutDescription = await db.orm.public.Task.where((t) => t.description.isNull()).all(); // description is String? +const withoutDescription = await db.orm.public.Task.where((t) => t.description.isNull()).all(); // Task.description is String? in the example schema ``` ### Combinators @@ -1718,10 +1720,10 @@ Combine or negate conditions. #### Remarks -- On PostgreSQL, `and`, `or`, `not`, and `all` are **standalone functions** imported from `@prisma/orm-postgres/orm-client`. You import them; they are not methods on a field. +- On PostgreSQL, import `and`, `or`, `not`, and `all` from `@prisma/orm-postgres/orm-client`. You call them as functions, as in `and(a, b)`. They are not methods on a field. - On PostgreSQL, `and(...)` and `or(...)` take any number of conditions, so `and(a, b, c)` is fine. `not(...)` takes one. - On PostgreSQL, `all()` takes no arguments and returns a condition that matches every row. This `all()` is not the `.all()` you call at the end of a chain to run the query. -- On MongoDB, `.and(other)` and `.not()` are **methods on a condition you already built**, such as `MongoFieldFilter.eq('name', 'Alice').and(...)`. There is **no `.or()` method**. To OR conditions, build `MongoOrExpr.of([...])`. +- On MongoDB, call `.and(other)` and `.not()` on a condition you already built, as in `MongoFieldFilter.eq('name', 'Alice').and(...)`. There is no `.or()` method. To OR conditions, build `MongoOrExpr.of([...])`. - On MongoDB, `.and(other)` takes exactly one condition. For three, chain it: `a.and(b).and(c)`. You can also write all three at once with `MongoAndExpr.of([a, b, c])`. `MongoAndExpr` and `MongoOrExpr` come from the same module as `MongoFieldFilter`. #### Examples @@ -1747,7 +1749,7 @@ const everyPost = await db.orm.public.Post.where(() => all()).all(); ##### `and` / `or` / `not` (MongoDB) ```typescript -import { MongoFieldFilter, MongoOrExpr } from '@prisma/orm-mongo/query-ast/execution'; +import { MongoAndExpr, MongoFieldFilter, MongoOrExpr } from '@prisma/orm-mongo/query-ast/execution'; const both = await db.orm.users .where(MongoFieldFilter.eq('role', 'author').and(MongoFieldFilter.eq('name', 'Alice'))) @@ -1756,6 +1758,9 @@ const either = await db.orm.users .where(MongoOrExpr.of([MongoFieldFilter.eq('name', 'Alice'), MongoFieldFilter.eq('name', 'Bob')])) .all(); const negated = await db.orm.users.where(MongoFieldFilter.eq('name', 'Alice').not()).all(); +const bothAtOnce = await db.orm.users + .where(MongoAndExpr.of([MongoFieldFilter.eq('role', 'author'), MongoFieldFilter.eq('name', 'Alice')])) + .all(); ``` ##### AND of two ORs (MongoDB) @@ -1770,10 +1775,6 @@ const authorsNamedAliceOrBob = await db.orm.users .all(); ``` -:::note[No `.or()` method on MongoDB] -MongoDB filter expressions have `.and(...)` and `.not()`, but no `.or(...)`. Use `MongoOrExpr.of([...])` instead. -::: - ### Relation filters (PostgreSQL) Filter parents by their related rows. @@ -1785,7 +1786,7 @@ Filter parents by their related rows. - `some()` with no argument matches parents with at least one related row. - A user with no posts matches `every()`. - `not(u.posts.some(...))` and `u.posts.none(...)` match the same rows. Prefer `none(...)`. -- All three are also methods on a to-one relation, such as `Post.user`, which holds a single related row. There `some()` reads as "the related row matches" and `none()` as "it does not". +- All three are also methods on a to-one relation, such as `Post.user`, which holds a single related row. There `some()` means the related row matches, and `none()` means it does not. `every()` matches when the related row matches, and also when there is no related row. #### Examples @@ -1821,7 +1822,7 @@ A plain object of equality matches, ANDed together. - Available for PostgreSQL and MongoDB. - Multiple keys are combined with implicit AND. A key set to `undefined` is skipped. - On PostgreSQL, a key set to `null` becomes an IS NULL check. On MongoDB it matches documents where the field is null and documents where the field is missing. -- On PostgreSQL, the shorthand object cannot filter a field whose type cannot be compared for equality, such as `Json`. Naming such a field throws an error whose `code` is `ORM.FILTER_UNSUPPORTED`. Use the callback form, which offers whatever that type does support. On a `Json` field that is `isNull()` and `isNotNull()`. +- On PostgreSQL, the shorthand object cannot filter a field whose type cannot be compared for equality, such as `Json`. Using that field as a key compiles, but fails at run time with an error whose `code` is `ORM.FILTER_UNSUPPORTED`. Use the callback form, which has whatever methods that type does support. On a `Json` field those are `isNull()` and `isNotNull()`. #### Examples @@ -1840,13 +1841,13 @@ const alice = await db.orm.users.where({ name: 'Alice', role: 'author' }).first( ### `MongoFieldFilter` -Helpers that build one MongoDB filter condition. Use them for anything other than equality on a top-level field, which `.where({ name: 'Alice' })` already covers. To join two conditions with AND or OR, see [Combinators](#combinators). +Helpers that build one MongoDB filter condition. Use them for anything other than equality on a top-level field, which `.where({ name: 'Alice' })` already covers. Chaining two of them, as in `.where(a).where(b)`, joins them with AND. For OR, see [Combinators](#combinators). #### Remarks - MongoDB. Import from `@prisma/orm-mongo/query-ast/execution`. `MongoExistsExpr` is in the same module. - The helpers are `of`, `eq`, `neq`, `gt`, `lt`, `gte`, `lte`, `in`, `nin`, `isNull`, and `isNotNull`. Each returns one filter, which you can pass straight to `.where()` or hold in a variable. There is no `ne`; use `neq`. -- `of(field, operator, value)`: pass the MongoDB operator as a string, including the `$`, for example `'$regex'`. Use it for operators the named helpers do not cover, such as `MongoFieldFilter.of('tags', '$size', 3)`. `of` does no checking of its own, so a misspelled operator such as `'$regexp'` fails at the database, not in TypeScript. +- `of(field, operator, value)`: pass the MongoDB operator as a string, including the `$`, for example `'$regex'`. Use it for operators the named helpers do not cover, such as `MongoFieldFilter.of('tags', '$size', 3)`. A misspelled operator such as `'$regexp'` is not caught until the query runs. - `isNull(field)` matches documents where the field is `null` and documents where the field is missing. `isNotNull(field)` matches everything else. To test only whether a field is present, use `MongoExistsExpr.exists(field)` or `MongoExistsExpr.notExists(field)`. Those two are all `MongoExistsExpr` offers. - There is no `regex`, `elemMatch`, `all`, or `size` helper. Write those operators through `of`. @@ -1856,7 +1857,7 @@ Helpers that build one MongoDB filter condition. Use them for anything other tha |---|---|---| | `of(field, operator, value)` | Field + MongoDB operator string + value | Any operator, including the `$`, such as `'$regex'`. | | `eq(field, value)` / `neq(field, value)` | Field + value | Equality / inequality. | -| `gt(field, value)` / `lt(field, value)` / `gte(field, value)` / `lte(field, value)` | Field + a value of any type | Greater than, less than, greater or equal, less or equal. | +| `gt(field, value)` / `lt(field, value)` / `gte(field, value)` / `lte(field, value)` | Field + value | Greater than, less than, greater or equal, less or equal. | | `in(field, values)` / `nin(field, values)` | Field + array | Membership / exclusion. | | `isNull(field)` / `isNotNull(field)` | Field | `isNull` matches a `null` value or a missing field. `isNotNull` matches everything else. | @@ -1901,7 +1902,7 @@ Filter into a field of an object stored inside the document, rather than in its #### Remarks - MongoDB. Use `MongoFieldFilter.eq('address.city', 'San Francisco')` for a dotted path. It takes the path as a plain `string` and needs no cast. -- You can also write `.where({ 'address.city': 'San Francisco' })`. It works, but TypeScript rejects it unless you add a cast, because `.where()` only accepts your model's top-level field names. +- You can also write `.where({ 'address.city': 'San Francisco' })`. That object form runs correctly, but it does not type-check. `.where()` only knows your model's top-level field names, so you must cast. #### Examples @@ -1913,7 +1914,7 @@ import { MongoFieldFilter } from '@prisma/orm-mongo/query-ast/execution'; const usersInSf = await db.orm.users.where(MongoFieldFilter.eq('address.city', 'San Francisco')).all(); ``` -The object form works at runtime too, with a cast: +`MongoFieldFilter.eq` is the recommended form. 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 @@ -1922,27 +1923,28 @@ 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, called `u` or `t` in the examples below, and returns an array of the operations you want. 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()`. `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 callback's argument as a property: `u.bio.set('Hello')`. -- For a field inside an embedded object, call the callback's argument as a function with the dot path: `u('address.city').set('San Francisco')`. +- 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', country: 'US' })`. -All four methods require `.where()`. Without one they throw an error whose `code` is `ORM.WHERE_MISSING`. 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()` returns the number of documents it changed. +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. ### Available operations (MongoDB) #### Remarks -- MongoDB only. These are the four operations for single-value fields: `set()`, `unset()`, `inc()`, and `mul()`. The four array operations follow under [Array operations](#array-operations-mongodb-unverified). +- MongoDB only. These are the four operations the update builder gives you for single-value fields: `set()`, `unset()`, `inc()`, and `mul()`. The four array operations follow under [Array operations](#array-operations-mongodb-unverified). - `set(value)` assigns a field. - `unset()` removes a field. - `inc(n)` increments a numeric field. TypeScript offers `inc` and `mul` on numeric fields only. Applied to a missing field, `inc` sets the field to `n`. - `mul(n)` multiplies a numeric field. Applied to a missing field, it sets the field to `0` whatever `n` is. -- After `variant(...)`, `t.duration.inc(10)` is a type error even though it runs correctly. Put `// @ts-expect-error` on the line directly above the `.update(...)` line, as the example below shows. If that type error ever goes away, TypeScript reports the comment as unused, and you delete the comment then. +- After `variant(...)`, `u.duration.inc(10)` is a type error even though it runs correctly. Put `// @ts-expect-error` on the line directly above the line TypeScript flags. When the type error goes away, TypeScript reports the comment as unused, so delete the comment then. #### Examples These examples use the MongoDB accessors from [Setting up the client](#setting-up-the-client). + ##### `set()` (MongoDB) Return as many operations as you want. They are applied together, in one write. @@ -1957,26 +1959,24 @@ const moved = await db.orm.users.where({ _id: '6650f1c2a1b2c3d4e5f60002' }).upda ##### `inc()` and `mul()` (MongoDB) -`duration` is declared on `Tutorial`, not on the base `Post` model, so each line that touches it needs `// @ts-expect-error`. +`duration` is declared on `Tutorial`, not on the base `Post` model, so each `.update(...)` line that touches it needs its own `// @ts-expect-error` directly above it. ```typescript const incremented = await db.orm.posts .variant('Tutorial') .where({ _id: '6650f1c2a1b2c3d4e5f60003' }) // @ts-expect-error duration is a Tutorial field, not a Post field - .update((t) => [t.duration.inc(10)]); + .update((u) => [u.duration.inc(10)]); const multiplied = await db.orm.posts .variant('Tutorial') .where({ _id: '6650f1c2a1b2c3d4e5f60003' }) // @ts-expect-error duration is a Tutorial field, not a Post field - .update((t) => [t.duration.mul(3)]); + .update((u) => [u.duration.mul(3)]); ``` ##### `unset()` (MongoDB) -`updateAndCount()` takes the callback in the same place. `upsert()` takes it as its `update` key, beside `create`, and [`upsert()`](#upsert) shows a full call. That callback cannot use a dot path. Given one, `upsert()` throws an error whose `code` is `ORM.OPERATION_UNSUPPORTED`. To change a field inside an embedded object in an `upsert()`, set the whole embedded object on its top-level field instead, as in `u.address.set({ street: '1 Market St', city: 'San Francisco', country: 'US' })`. - ```typescript const updated = await db.orm.users.where({ _id: '6650f1c2a1b2c3d4e5f60001' }).update((u) => [u.bio.unset()]); const changed = await db.orm.users.where({ _id: '6650f1c2a1b2c3d4e5f60002' }).updateAndCount((u) => [u.bio.unset()]); @@ -1985,16 +1985,16 @@ const changed = await db.orm.users.where({ _id: '6650f1c2a1b2c3d4e5f60002' }).up ### Operations that do not exist (MongoDB) :::warning[No `min`, `max`, `rename`, or `currentDate` field operations] -The callback's argument implements eight operations, and only these eight: `set`, `unset`, `inc`, `mul`, `push`, `pull`, `addToSet`, and `pop`. `min`, `max`, `rename`, and `currentDate` are absent from the API, and TypeScript rejects them: `rename` is not a property of `u.bio`. +The update builder implements eight operations, and only these eight: `set`, `unset`, `inc`, `mul`, `push`, `pull`, `addToSet`, and `pop`. `min`, `max`, `rename`, and `currentDate` are absent from the API, and TypeScript rejects them: `rename` is not a property of `u.bio`. ::: ### Array operations (MongoDB) [#array-operations-mongodb-unverified] #### Remarks -- MongoDB. `push()`, `pull()`, `addToSet()`, and `pop()` are available on the callback's argument. `push(value)` appends one element. `addToSet(value)` appends it only if it is not already there. `pop(1)` removes the last element, and `pop(-1)` removes the first. Those are MongoDB's own `$pop` values. +- MongoDB. `push()`, `pull()`, `addToSet()`, and `pop()` are available on the update builder. `push(value)` appends one element. `addToSet(value)` appends it only if it is not already there. `pop(1)` removes the last element, and `pop(-1)` removes the first. - `pull(match)` removes every element equal to the argument. If the array holds objects, pass part of an object to remove every element that matches it, as in `u.tags.pull({ kind: 'draft' })`. -- These operations need an array field, declared with `[]` in your contract, as in `tags String[]`. Applied to any other field, the write fails with an error raised by MongoDB, not by Prisma ORM. In a `catch` block, `isRuntimeError(error)` is `false` for it and there is no `error.code` to match, so read the message. +- These operations need an array field, declared with `[]` in your contract, as in `tags String[]`. Applied to any other field, the write fails. There is no `error.code` to match, because the error is MongoDB's own. - The four lines below are separate calls because one update cannot apply two operations to the same field. One update can still change two different fields, such as pushing to `tags` and setting `bio`. The example schema has no array field, so these four lines assume `tags String[]` on `User` and will not run against the schema on this page. ```typescript @@ -2020,13 +2020,12 @@ The methods `all()` / `createAll()` / `updateAll()` / `deleteAll()` return an `A 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. -- Switching from one way to the other throws an error whose `code` is `RUNTIME.ITERATOR_CONSUMED`. The message contains `already been consumed`. Awaiting a result and then looping it with `for await`, or the reverse, is what throws. There is no error class to test with `instanceof`. Import `isRuntimeError` from `@prisma/orm-postgres/components/runtime` on PostgreSQL, or from `@prisma/orm-mongo/components/runtime` on MongoDB, and check `error.code`. +- 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, with the same error message and the same rule. 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)). +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. ```typescript import { isRuntimeError } from '@prisma/orm-postgres/components/runtime'; -// all() returns the result object straight away and `await` is what runs the query, so this example holds it in a variable. const result = db.orm.public.User.all(); const first = await result; const again = await result.toArray(); // safe: same array @@ -2046,4 +2045,4 @@ try { - `aggregate((agg) => ({ ... }))` resolves to a **single object** keyed by your aliases. `agg` is the callback's argument, and it carries the aggregate functions. `db.orm.public.Order.aggregate((agg) => ({ total: agg.count() }))` resolves to one object, such as `{ total: 10 }`. - [Grouped aggregates](#grouped-aggregates) documents `aggregate()`, `groupBy()` with more than one field, and every function `agg` gives you. The bullets here cover only the shape of what you get back. - `groupBy(...).aggregate((agg) => ({ ... }))` resolves to an **array**, one object per group. Each object carries the fields you grouped by plus your aliases. `db.orm.public.Order.groupBy('customerId').aggregate((agg) => ({ orderCount: agg.count(), totalAmount: agg.sum('amount') }))` resolves to `[{ customerId, orderCount, totalAmount }, ...]`. -- `count()` is always a `number` (`0` over an empty set). `sum()`, `avg()`, `min()`, and `max()` resolve to `null`, not `0`, over an empty result set, so their TypeScript type includes `null`. +- `count()` is always a `number` (`0` over an empty set). `sum()`, `avg()`, `min()`, and `max()` resolve to `null`, not `0`, over an empty result set. diff --git a/docs/orm-docs-audit/c21-orm-client/part1.round4.md b/docs/orm-docs-audit/c21-orm-client/part1.round4.md new file mode 100644 index 00000000000..0ba97ba6411 --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-client/part1.round4.md @@ -0,0 +1,81 @@ +Read once, top to bottom, as a Prisma ORM 7 user who has never seen version 8. + +## Sentences I could not restate after one reading + +**Line 11** — "This page assumes a project where `prisma orm init` has already run and your tables exist: [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7) has the packages to install, `prisma orm init`, and `prisma db update`, which replaces `prisma migrate`, and [How migrations work](/orm/migrations/how-migrations-work) creates the tables." + +Too many ideas in one sentence, and the grammar breaks at the end. The last clause reads as if the *page* creates my tables. I also can't tell whether `prisma db update` is the thing that creates tables or something else. Plainer: "This page assumes your tables already exist. To install the packages and run `prisma orm init`, see Coming from Prisma ORM 7. `prisma db update` replaces `prisma migrate`; see How migrations work." + +**Line 15** — "Your schema now lives in `contract.prisma`, the contract, which replaces `schema.prisma` from Prisma ORM 7." + +The floating appositive "the contract" stopped me. Is "the contract" the file, the concept, or a product name? I read it three times. Plainer: "`contract.prisma` replaces `schema.prisma`. This page calls it the contract." + +**Line 229** — "You still create the client with `postgres(...)`, and `orm(...)` takes two things from that client." + +Which two things? The sentence deliberately withholds the answer and sends me to a section I don't have. This is the single most frustrating sentence on the page — it tells me a fact exists without telling me the fact. + +**Line 249** — "On this page, a collection is the query object you chain more methods on… On MongoDB, `users` in `db.orm.users` is also the name of the database collection." + +Two different meanings for "collection", one paragraph apart. After one reading I could not say which meaning applies when the page later says "`distinct()` does not exist on a MongoDB collection" — the query object, or the database collection? + +**Line 424** — "Call `posts.combine({ ... })`, and inside the object write `posts` again for each value. It is the same object every time." + +"It is the same object every time" is the sentence I could not restate at all. Same as what? I guessed it means the `posts` you write inside the object is the same collection the callback received, so reusing it does not accumulate earlier chaining. The page should say that literally. + +**Line 511** — "On PostgreSQL, a native enum column sorts in the order its members are declared, not alphabetically. The example schema declares `Priority` text-backed, so it sorts by its stored text." + +The bullet starts on native enums and switches to text-backed enums mid-bullet, so the first sentence looks like it describes the example and then does not. I had to reread to work out these are two different cases. + +**Line 622** — "The rows are compared as a pair, so you get the rows past the first column's value, plus the rows with that same first value and past the second." + +Dense. I think it means the cursor compares sort keys as a tuple, but I could not restate it cleanly on one reading. + +**Line 334** — "The type still lists every field on the model, so read only the fields you named from each returned document and treat the rest as absent." + +I understand the instruction. I do not know what actually happens if I read one anyway — `undefined`? a crash? The page tells me the rule but not the consequence. + +## Words and phrases I had to guess + +- **"the contract"** — guessed: the new name for the schema file and everything generated from it. +- **`prisma contract emit`** — guessed: the replacement for `prisma generate`. The page never says so, and that is the one comparison a version-7 user needs. +- **`@@type("pg/text@1")`** — guessed the `@1` is a format version I must not change, because the page says "Other versions do not exist yet." I do not know what a version of a storage format is. +- **`types { Embedding1536 = pgvector.Vector(1536) }`** — guessed `pgvector.` is a namespace from an installed extension package. Explained 175 lines later at line 197, long after I hit it. +- **"variant"** — guessed: single-table inheritance. The word is new and the explanation at line 195 arrives before "collection" is defined, so "`variant('Bug')` narrows a collection to that variant" was meaningless where I read it. +- **"refinement"** (line 412) — guessed: the callback. The page names it and then never uses the name again, so naming it served no purpose. +- **`Time` column** (line 418) — guessed: a time-of-day type. Not in the example schema, not defined anywhere on the page. I do not know how to declare one. +- **"native enum"** vs **"text-backed enum"** — guessed correctly, but the example schema uses the text-backed form while the advice at line 511 pushes me to the native form, so I could not tell which the page recommends. +- **`dbName: 'app'`** (line 240) — guessed: the MongoDB database name. Never stated. +- **`db.sql`, `db.query`, `db.raw`** (line 201) — guessed from the names. The page says "This page covers none of those three" and gives no link, so the mention is a dead end. +- **`all()` and `first()`** — used in every example from line 214 onward; the definition is in a section I do not have. I guessed "run it and return an array" and "run it and return one row or null". + +## Places I asked "so what do I actually type?" + +1. **Where do I run `prisma contract emit` from, and where does it write?** Line 15 says "A new project keeps all three files in `src/prisma/`" but never says whether `emit` puts them there or I configure a path. +2. **What is the real import path?** Every example is `from './contract.d'`. If my code is in `src/routes/`, do I write `'../prisma/contract.d'`? The page insists the path is "written exactly like that", which made me think the literal string matters. +3. **Which package do I install?** Imports come from `@prisma/orm-postgres/runtime`, `@prisma/orm-postgres/orm-client`, `@prisma/orm-mongo/runtime`, `@prisma/orm-mongo/query-ast/execution`, and `@prisma/orm-extension-pgvector/runtime`. I cannot tell how many npm packages that is. +4. **`orm(...)` at line 229** — I want the two lines of code and get a forward reference. +5. **The Temporal polyfill (line 497)** — "add a polyfill: `import 'temporal-polyfill/full/global'`". Do I `npm install temporal-polyfill` first? Not said. +6. **What type is `createdAt` in a result?** The cursor example passes `last.createdAt` straight back in, the MongoDB filter passes `new Date(...)`, and `min('createdAt')` returns a `Temporal.Instant`. Three shapes, no statement of which one a plain row field is. +7. **Options tables call the argument `filter` (line 278) and `filter / orderBy / limit / offset` (line 430)**, but the methods are `where()`, `orderBy()`, `limit()`, `offset()`. There is no method called `filter`. I do not know what to type. +8. **`distinct()` (line 657)** — "keeps one whole row per distinct `priority` value". Which row? The example then uses `select('priority')` anyway, contradicting the advice that I don't need it. +9. **`Customer` / `Order` (line 471)** — the sum and avg examples use models whose schema is not on this part of the page, so I cannot run them. +10. **MongoDB `variant()` (line 745)** — `db.orm.posts.variant('Tutorial')`, but MongoDB models are addressed by collection name everywhere else. Is `'Tutorial'` the model name even though `Post` is addressed as `posts`? The page says "Pass the variant's model name", which contradicts the collection-name rule at line 233 and left me unsure. + +## Places explaining internals when I wanted instructions + +- **Line 205** — "The import path `./contract.d` is written exactly like that, with no `.ts` on the end, and it resolves to the emitted `contract.d.ts`." I only needed the line of code. +- **Line 621** — "The library does not check this while the query runs, so code that skips TypeScript, such as plain JavaScript or a cast, gets no error." This is about the library's implementation. The instruction is "always call `orderBy()` before `cursor()`." +- **Line 688** — "PostgreSQL rejects a query whose `ORDER BY` does not begin with the `DISTINCT ON` columns. The library does not check this, so a mismatch fails in the database." I am told the generated SQL and where validation is missing. I wanted: "start the `orderBy()` with the same columns." +- **Line 721** — "a variant that sets its own `@@map`… gets its own table. That changes how some writes behave; see `createAndCount()` and `upsert()`." Storage layout plus two forward references, and no statement of what I should do. +- **Line 369** — the `_id` hex-string versus `ObjectId` split. Actionable in the end, but it is a description of a leaked implementation detail rather than a rule. +- **Line 499** — "PostgreSQL has no `SUM` or `AVG` over a date or a timestamp." Fine as a footnote, but it explains the database's limits when "these do not compile" already covered it. + +## Could I do what the page is for after one reading? + +Partly. I could write `where()`, `select()`, `orderBy()`, `limit()`, `offset()`, `distinct()`, and `variant()` calls by copying the examples, and I could tell PostgreSQL and MongoDB apart. Two things I could not do. + +**I could not get to the first line of working code.** The setup section assumes `contract.json` and `contract.d.ts` already sit next to my source file. I do not know which packages to install, where the emitted files land, what import path to write from my own directory, or whether `prisma contract emit` is the thing I run instead of `prisma generate`. For a version-7 user, that is the whole migration question and the page skips it. + +**I could not use `combine()`, or `orm(...)` for custom methods.** `combine()` because "It is the same object every time" never resolved. `orm(...)` because the page tells me it takes two things from the client and refuses to say which. + +Smaller gaps that would still stop me: what a `Time` column is, whether I should use `enum` or `native_enum` in a new schema, and what type a `DateTime` field actually holds when it comes back from a query. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-client/part2.round4.md b/docs/orm-docs-audit/c21-orm-client/part2.round4.md new file mode 100644 index 00000000000..bda8cd0f994 --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-client/part2.round4.md @@ -0,0 +1,90 @@ +Read once, straight through, as a Prisma ORM 7 user. Findings below. All line numbers are from `/Users/wmadden/Projects/prisma/web/.claude/worktrees/error-reference-follow-ups-81a8e7/wip/c21/r4/part2.mdx`. + +## Sentences I could not restate after one reading + +**Line 5–10, the MongoDB count example.** +> "On MongoDB, count with `db.query`, the [pipeline builder](...)" +> ``` +> const built = db.query.from('posts').count('total').build(); +> const [counted] = await (await db.runtime()).query(built); +> ``` +Three unexplained things in two lines. `db.query` is a builder, but `(await db.runtime()).query(...)` is a different `query` that runs things — same word, two meanings, no explanation. `db.runtime()` has never appeared; part 1 only showed `db` being made by `postgres(...)`/`mongo(...)`. And `await (await ...)` looks like a typo until I stare at it. Plainest version I'd have understood: "Counting on MongoDB takes two steps. Build the pipeline with `db.query`, then run it with the runtime: `const runtime = await db.runtime(); const [counted] = await runtime.query(built);`" + +**Line 118, the whole paragraph.** Five separate rules in one block: +> "`first()` returns `null` on no match and does not check that only one row matched. `findUniqueOrThrow` and `findFirstOrThrow` both map onto `all().firstOrThrow()`, as the last line above shows. `firstOrThrow()` is a method on the result that `all()` gives you, and calling it does not count as using the result a second time." +The middle clause fights what I read 90 lines earlier at line 25: "Each result can be used one way only." I stopped and re-read to work out whether `all().firstOrThrow()` was legal. Also: `all()` loads every row and then throws away all but the first? That's what it looks like, and the page never says whether it actually fetches everything. Split this into a bulleted list and say plainly whether `firstOrThrow()` fetches one row or all of them. + +**Line 127.** +> "`db.orm` cannot use your subclass. Call the `orm(...)` function with a `collections` object to get a second accessor with the same methods as `db.orm.public`. You now have two accessors, both of them work, and your existing `db.orm.public.X` calls are unaffected." +I could not tell what I am supposed to do going forward. Do I keep both accessors in my app forever? Do I replace every `db.orm.public.X` with `tasks.X`? The sentence reassures me nothing breaks but never tells me which one to use. + +**Line 172.** +> "On PostgreSQL you only get this protection from TypeScript." +I do not know what that means concretely. Does my code fail to compile without `where()`? Is it a red squiggle I can ignore? Is it only in strict mode? Say the literal thing: "On PostgreSQL, `updateAll()` without `where()` does not compile" — or whatever actually happens. + +**Line 295 and 597, the error messages.** +> `createAndCount() is not supported for MTI variant "Bug" on model "Task".` +"MTI" is never defined anywhere on this page. I guessed it means the multiple-table storage the surrounding prose describes, but the page uses "variant stored in its own table" in prose and "MTI variant" in the error, and never connects them. Worse, line 597 says `upsert()` is unsupported and the error tells me `Use createAll() instead.` — `createAll()` is not an upsert. Either the error text is wrong or the page copied it wrong. Line 598 then gives me the real answer (`first()` then `create()`/`update()`), which contradicts the quoted message directly above it. + +**Line 341.** +> "Putting a field declared on a variant in the update, such as `duration` on `Tutorial`, makes TypeScript complain even though the update runs correctly. Put `// @ts-expect-error` on the line directly above `.update(...)`." +Two ideas fighting: this is a known defect and the fix is to silence the compiler. I understood the words. What I could not work out is the consequence: `@ts-expect-error` itself becomes an error once the underlying problem is fixed, so my build breaks on upgrade. The page does not warn me. + +**Line 348.** +> "On PostgreSQL, `update()` takes an object of field values only. Passing a function changes nothing and returns `null`." +So a function argument fails silently and I get the same `null` I'd get from "no row matched"? I re-read this three times to be sure it says what it says. If that is real, say it as a warning, not a remark. + +**Lines 422–424.** +> "On MongoDB, `updateAll()` is not a single operation, and two things follow. A document that starts matching your filter only once the call is under way is updated but not returned to you. A document that someone else changes during the call comes back carrying their values, not yours." +I got there eventually, but it took two passes. "starts matching your filter only once the call is under way" is a lot of clause. Plainer: "If a document begins matching your filter while the call is running, it is updated but not included in the returned rows. If someone else changes a matched document while the call is running, you get their version back, not yours." + +**Line 591.** +> "Only the column name is used. The value is required by the type and is then ignored, so pass any value of the right type." +I understood each sentence and still did not believe it. The example writes `conflictOn: { label: 'ignored' }`. As a reader I would want one line saying why — is this a temporary limitation or the intended design? Right now it reads like a bug I am being told to live with. + +## Words and phrases I had to guess + +- **"read terminals" / "mutation terminals"** (anchors on lines 1 and 165). Guessed: the method that ends the chain. Never used in the visible text, so the anchor is the only place the word appears — a reader who lands on that URL gets a word the page never defines. +- **`AsyncIterableResult`** (lines 23, 36, 257, 269, 436, 538). Linked to `#asynciterableresult`, which is not in this part. Guessed: an object you can `await` or `for await`. The table on line 36 says as much, so I coped, but I never learned whether streaming actually keeps memory down or just looks different. +- **`Row`** (lines 36, 92, 202, 365). Guessed: the generated TypeScript type for that model. The page never says where it comes from or how I import it if I want to name it. +- **"contract type"** (line 129, `Collection`). Guessed: the type exported by the generated `contract.d.ts`. The import on line 140 says `from './contract.d'`, which looks wrong to me — I would normally write `'./contract'`. I would have to try it. +- **`select()` and `include()`** (lines 351, 400). Used in a full example with no definition anywhere I can see. Guessed from Prisma 7 habit: choose fields, choose relations. The callback form `include('tags', (tag) => tag.select(...).orderBy(...))` is new and unexplained. +- **`orderBy({ createdAt: 1 })`** (line 59). Guessed `1` means ascending. The PostgreSQL tab right above uses `u.email.asc()`, so the two tabs look like different products. Line 26 tells me they differ but the page never says what `1` and `-1` mean. +- **"to-many" / "to-one"** (line 196). Guessed: relation cardinality. Fine, but they appear once with no anchor. +- **"driver session"** (line 424). Guessed: MongoDB's own transaction session object. +- **`client.context`** (lines 128, 157). Guessed: some bundle of state the ORM needs. The page tells me where to get it but not what it is. + +## "So what do I actually type?" + +1. **Reading an error code.** The page names five codes — `RUNTIME.ITERATOR_CONSUMED`, `RUNTIME.NO_ROWS`, `ORM.WHERE_MISSING`, `ORM.OPERATION_UNSUPPORTED` — and never shows one line of code that catches one. Do I write `catch (e) { if (e.code === 'ORM.NO_ROWS') }`? Is there an error class to `instanceof`? Is `code` typed? Nothing. +2. **The MongoDB `updateAll()` workaround (line 424).** The whole point of the remark is that my writes are unsafe. The fix is a cross-reference and one sentence of prose. I have no code to copy. +3. **The MongoDB count (line 8).** I do not know where `db.runtime()` comes from or whether I should cache the runtime rather than awaiting it per query. +4. **Streaming vs awaiting on writes.** Line 257 says `createAll()` returns something streamable. If I `for await` over `createAll()`, have the rows already been inserted, like `deleteAll()` on line 528? The page answers this for deletes and not for creates. +5. **`firstOrThrow()` has no entry.** It has no heading, no signature, no options table, and no return-type row — just a mention inside a paragraph on line 118. Every other method here gets a full section. +6. **The custom `Collection` subclass.** I have `tasks` at line 155 and `db` from part 1. Do I now call `postgres(...)` twice, or is `client` on line 152 meant to be the same `client` part 1 showed? The example builds a whole new client, so I cannot tell whether I am replacing my setup or adding to it. +7. **`conflictOn` for a multi-column unique constraint (line 592).** The rule is stated; no example is shown. I would have to guess `conflictOn: { orgId: 0, email: '' }`. +8. **Prisma 7 mappings I did not get.** The page maps `findMany`, `findUnique`, `findFirst`, `create`, `update`, `delete`, `upsert`. It never maps `createMany`, `updateMany`, `deleteMany`, or `count`. Those are the four I use most after `findMany`, and `createAll` vs `createAndCount` vs `createMany`'s `skipDuplicates` is exactly the kind of thing I would search this page for. + +## Places that explain the inside when I wanted the outside + +- **Line 423:** "That happens because the update runs by filter, while the read-back runs by the ids collected before the update." This is the implementation. I wanted to know what to do about it, which is the next line, and that line only links elsewhere. +- **Line 294:** "A variant is stored in its own table when its `@@map` names a different table than its base model does. `Bug` ... maps to `bug` while `Task` maps to `task`." Useful, but it arrives as storage-layout trivia before I am told the practical rule. The practical rule — "if your variant has its own `@@map`, `createAndCount()` and `upsert()` do not work" — should come first. +- **Line 186:** "On MongoDB, `create()` returns the values you passed plus the `_id` the server assigned, not the stored document. On PostgreSQL the returned row is read from the database." I understand why this matters (database defaults won't show up on Mongo) but the page makes me infer that consequence myself. +- **Line 128:** "`orm(...)` takes two things from the client that `postgres(...)` created ... `client.connect()` gives you the `runtime` argument, and `client.context` is the `context` argument." This is wiring narration. The code example on lines 152–159 already shows it more clearly than the prose does. + +## Could I do what the page is for, after one reading? + +Partly. For the plain cases — yes. I could write `all()`, `first()`, `create()`, `update()`, `delete()`, and their `All`/`AndCount` variants on PostgreSQL, and translate my Prisma 7 `findMany`/`create`/`update`/`delete` calls with confidence. The diff blocks are the most useful thing on the page and I would have gone straight to them. + +What I would still not know: + +- How to catch and check any of the errors the page keeps naming. +- What `AsyncIterableResult` really costs me — whether `for await` streams from the database or just iterates an array it already fetched. +- Whether `all().firstOrThrow()` fetches one row or the whole table. I would avoid it for that reason alone. +- What `MTI` means, and whether the `upsert()` error message telling me to "Use createAll() instead" is a typo I should ignore. +- What `select()` and `include()` do, or how to write the `include` callback for anything other than the one example. +- How to translate `createMany`, `updateMany`, `deleteMany`, or `count` from my existing code. +- How to make MongoDB `updateAll()` safe. I would leave the page knowing my code has a race condition and not knowing how to fix it. +- Whether I should restructure my client setup to use a custom `Collection`, or leave it alone. + +The biggest single blocker is the error handling. The page mentions error codes eight times and never once shows me the `catch`. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-client/part3.round4.md b/docs/orm-docs-audit/c21-orm-client/part3.round4.md new file mode 100644 index 00000000000..241ea5664c2 --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-client/part3.round4.md @@ -0,0 +1,81 @@ +Read once, top to bottom, as a Prisma ORM 7 user. + +## Sentences I could not restate after one reading + +**Line 3** — "PostgreSQL supports aggregation over a result set." +This reads as a fact about the database, which I already knew, so I could not work out what it was telling me. I think you mean "On PostgreSQL, a collection has aggregation methods." Plainest wording: "These methods exist only on PostgreSQL collections." + +**Line 48** — "`sum()` and `avg()` are offered on `Decimal` and `BigInt` columns. Over a `Decimal` column they return the exact value as a string." +"Offered" stopped me. The bullet above already showed `sum('amount')` on an `Int` column, so "offered on Decimal and BigInt" cannot mean "only on". And "they return the exact value as a string" — does that include `avg()`? If `avg()` on a `Decimal` already returns a string, why does `avgDecimal()` exist (line 56)? Three ideas in two sentences and they contradict the next paragraph. Plainest wording: state the return type per column type in a small table: column type → `sum()` returns → `avg()` returns. + +**Line 48** — "Over a `BigInt` column `sum()` returns a `number` and throws an error whose `code` is `RUNTIME.DECODE_FAILED` if the total is past `Number.MAX_SAFE_INTEGER`." +Too many ideas in one sentence, and it never says *when* it throws — at query time? on property access? Plainest wording: split into two sentences, and say the error comes from running the query. + +**Line 135** — "`groupBy(...)` returns a `GroupedCollection`, which you never import or name yourself." +I could not work out why I was being told this. It answers a question I did not ask. If the point is "don't try to type this type", say "You never write this type in your own code." + +**Line 137** — "To do that, write the query with the SQL query builder (`db.sql`)." +"SQL query builder" appears here for the first time with no definition, and the example below it (line 176) uses four things this page never explains: `db.sql.public.order`, `(f, fns)`, `.build()`, and `db.runtime().query(...)`. I could not restate what any of them do. + +**Line 227** — "Stacked `where()` calls are combined with AND, and you can use the shorthand object in one call and the callback in the next." +"Stacked" is a guess. Plainest wording: "If you call `where()` more than once, the conditions are ANDed. One call can take the object form and the next the callback form." + +**Line 238** — the whole bullet. +This is one paragraph carrying seven separate rules. The parts that stopped me: "need a type that can be compared for equality" and "need a type that can be ordered" are abstract categories the page never lists. I get two examples (`Uuid`, `DateTime` yes; `Json` no) and nothing else, so I cannot predict whether `Boolean`, `Decimal`, `Bytes`, or an enum is ordered. Plainest wording: a table of field type → available methods. + +**Line 392** — "Naming such a field throws an error whose `code` is `ORM.FILTER_UNSUPPORTED`." +"Naming such a field" made me stop — I think it means "putting that field as a key in the object". And "throws" tells me it is a runtime failure, but the `groupBy` section said a missing `orderBy` is a *TypeScript* error, so now I do not know which failures I catch at compile time and which blow up in production. Plainest wording: "Using that field as a key compiles, but fails at runtime with `ORM.FILTER_UNSUPPORTED`." + +**Line 356** — "There `some()` reads as 'the related row matches' and `none()` as 'it does not'." +I could restate this, but only after re-reading, because `some`/`every`/`none` are plural words and a to-one relation has one row. It also does not say what `every()` means on a to-one relation, which it just told me is available. + +## Words and phrases I had to guess + +- **"result set"** (lines 3, 40, 47) — guessed: the rows my filters match, not rows already fetched into memory. +- **"collection"** in "None of these methods exist on a MongoDB collection" — I was told "collection" means the query object, but MongoDB also calls its tables collections, and line 230 says "the accessor is the collection name". I guessed the query-object meaning here. +- **"offered on"** (line 48) — guessed: "also work on". +- **"pipeline builder"** (line 3) — guessed: a separate chaining API. I did not follow the link. +- **"selector"** and **"predicate"** as the Name column in the Options tables (lines 54, 207) — guessed these are just labels for the positional callback, not keys I type. The tables look exactly like options-object tables, so I first read them as `aggregate({ selector: ... })`. +- **"page the groups"** (line 138) — guessed: pagination. +- **"Column-level comparison methods"** (line 233) — guessed: comparisons on one field at a time. "Column-level" is not a term I know. +- **"to-many relation"** / **"to-one relation"** (lines 351, 356) — guessed from the examples; the page defines to-many but not to-one until it is already used. +- **`@prisma/orm-mongo/query-ast/execution`** — "query-ast" and "execution" look like internal package paths. I guessed I copy it verbatim and do not think about it. +- **`MongoAndExpr` / `MongoOrExpr` / `MongoFieldFilter`** — guessed "Expr" means expression. The page never expands it. +- **`h`, `agg`, `f`, `fns`, `o`, `u`, `p`, `t`, `group`** — guessed all of these are arbitrary parameter names I choose. + +## Places where I asked "so what do I actually type?" + +1. **`db.sql.public.order` (line 176) is lowercase, but `db.orm.public.Order` is capitalized.** The schema has `@@map("order")`. The page never says that `db.sql` uses the mapped table name while `db.orm` uses the model name. I would have typed `db.sql.public.Order` and got an error I could not explain. +2. **`db.runtime().query(topCustomers)` (line 182).** What is `runtime()`? Do I need a different import? Is this inside a transaction? This is the one escape hatch the page offers for a real need (sorting groups by an aggregate), and it is the least explained code on the page. +3. **`(f, fns)` in the `db.sql` example.** Two callback arguments appear with no word about what the second one is. +4. **`.build()` then `await db.runtime().query(...)`** — two steps, no explanation of why the query is not awaited directly like every other example on the page. +5. **`import { MongoAndExpr }`.** Line 293 tells me to use `MongoAndExpr.of([a, b, c])` but no example imports or uses it. I would have to infer the import line from the neighbouring one. +6. **`having()` placement.** Line 136 says `having()`, `orderBy()`, `limit()`, `offset()` can come in any order, and line 198 says `having()` comes "before `aggregate()`". Both are fine, but the only `having()` example has no `orderBy()`, so I do not know if `.orderBy(...).having(...)` compiles. +7. **Aggregates plus `orderBy`/`limit`.** Does `aggregate()` on a non-grouped collection honour a `limit()` earlier in the chain, or does it always aggregate everything the `where()` matched? The page never says. +8. **`count()` on a to-many relation.** Nothing here says how to count a group without `groupBy`, or how to aggregate related rows. I would have gone looking. +9. **The `Task` model in line 280** (`t.description.isNull()`) is not in either schema I was shown. Minor, but I could not check the field type against the claim. + +## Places explaining internals when I only wanted to know what to do + +- **Line 135**, `GroupedCollection` "which you never import or name yourself" — internal type naming. The useful half is "you cannot `await` it; call `.aggregate(...)`." +- **Line 150**, the Return type table saying `GroupedCollection` / "Not a promise." This repeats line 135 and still tells me about a type I never write. +- **Line 213**, the same table again for `having()`: "The same grouped collection with this condition added." That is a description of the builder's internal state, not of what I do. +- **Line 292**, "`.and(other)` and `.not()` are **methods on a condition you already built**" — the bolded contrast between "standalone functions" and "methods on a condition" is API-design commentary. What I need is the two code shapes side by side, which line 300 and line 318 already give. +- **Line 230**, "a filter you build by calling a static method on the `MongoFieldFilter` class". "Static method on the class" is implementation vocabulary. `MongoFieldFilter.eq('name', 'Alice')` shows me everything I need. + +## Could I do what the page is for, after one reading? + +Partly. + +I could write a simple `aggregate()`, a `groupBy().aggregate()`, a `having()`, and PostgreSQL `where()` filters with `and`/`or`/`not` and relation filters. Those sections have enough examples that I could copy and adapt. + +What I would still not know: + +- **Which comparison methods my field has.** For anything that is not `String`, `Int`, `Uuid`, `DateTime`, or `Json`, I would have to poke at autocomplete. The page tells me to rely on autocomplete instead of telling me the rule. +- **What `sum()` and `avg()` actually return on a `Decimal` column**, and therefore whether I need `avgDecimal()`. Lines 48 and 56 point in different directions. +- **How to sort groups by an aggregate.** This is a common thing to want, the page names it as the reason to drop to `db.sql`, and then the escape hatch is four unexplained API calls. I would have to go find the `db.sql` docs before I could write it. +- **Which mistakes the compiler catches.** `limit()` without `orderBy()` is called a TypeScript error; a `Json` key in a shorthand object is a runtime throw. I have no rule for predicting which is which. +- **How to aggregate on MongoDB.** Told to go elsewhere, which is fair, but it means half the page does not apply to a MongoDB reader and the split is only announced once, in line 3. +- **Whether `every()` works on a to-one relation**, and what it would mean. + +The largest single problem is the `db.sql` example at line 176. It is the answer to a real question the page raises itself, and it is the one block I could not type from. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-client/part4.round4.md b/docs/orm-docs-audit/c21-orm-client/part4.round4.md new file mode 100644 index 00000000000..6978db99533 --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-client/part4.round4.md @@ -0,0 +1,86 @@ +# Reader review — `wip/c21/r4/part4.mdx` + +I have used Prisma ORM 7 for two years. Here is what happened on one read. + +## Sentences I could not restate after one reading + +**1.** "You can also write `.where({ 'address.city': 'San Francisco' })`. It works, but TypeScript rejects it unless you add a cast, because `.where()` only accepts your model's top-level field names." + +"It works, but TypeScript rejects it" reads as a contradiction until you work out that "works" means "at runtime". Plainer: "That object form runs correctly, but it does not type-check: `.where()` only knows your model's top-level field names, so you must cast." + +**2.** "After `variant(...)`, `t.duration.inc(10)` is a type error even though it runs correctly. Put `// @ts-expect-error` on the line directly above the `.update(...)` line, as the example below shows. If that type error ever goes away, TypeScript reports the comment as unused, and you delete the comment then." + +Three ideas in one bullet and the last sentence is backwards. I could not tell whether this is a known bug I should report, a permanent design, or something I did wrong. The example also contradicts the instruction: the comment sits above the `.update(...)` line in the example, but the prose in the second example block says "each line that touches it needs `// @ts-expect-error`" — those are different rules. + +**3.** The whole paragraph under the heading `##### unset() (MongoDB)`: + +"`updateAndCount()` takes the callback in the same place. `upsert()` takes it as its `update` key, beside `create`, and [`upsert()`](#upsert) shows a full call. That callback cannot use a dot path. Given one, `upsert()` throws an error whose `code` is `ORM.OPERATION_UNSUPPORTED`." + +Nothing here is about `unset()`. It is filed under the wrong heading. And "That callback" has two candidates in the preceding sentence (`updateAndCount`'s and `upsert`'s); I had to guess it means `upsert`'s only. + +**4.** "Applied to any other field, the write fails with an error raised by MongoDB, not by Prisma ORM. In a `catch` block, `isRuntimeError(error)` is `false` for it and there is no `error.code` to match, so read the message." + +`isRuntimeError` has not been introduced at this point in the page — it is first explained 25 lines later. And "read the message" tells me to read something the page never shows. Read it and match on what? + +**5.** "`pop(1)` removes the last element, and `pop(-1)` removes the first. Those are MongoDB's own `$pop` values." + +Fine until the second sentence, which is trivia about where the numbers come from. It does not tell me anything I can act on. + +**6.** "// all() returns the result object straight away and `await` is what runs the query, so this example holds it in a variable." + +I had to read this twice. It is the only place the page tells me `all()` is lazy — a fact that changes how I write code — and it is buried in a code comment. + +## Words and phrases I had to guess + +- **`updateAndCount()`** — never defined anywhere on this page. It appears first in the "Field update operations" intro as if I already know it. I guessed it is like `updateAll()` but returns a count instead of the rows. The page later half-confirms that, but never says whether it exists on PostgreSQL. +- **"Field + a value of any type"** in the `gt`/`lt` table row, versus plain "Field + value" for `eq`. I guessed the difference is meaningless. If it is meaningless, make both rows say the same thing. If it means "no type-checking against the field's declared type", say that. +- **"the callback's argument"** — used about ten times. I guessed it means the `u` / `t` object. Naming it once ("the update builder, `u` in these examples") and then using that name would have saved me. +- **`variant('Tutorial')`** vs the base `Post` — I could follow this from the prompt context, but the sentence "`duration` is declared on `Tutorial`, not on the base `Post` model" is the only place this page explains why a type error is expected, and it sits under one heading while the rule sits under another. +- **"mutation terminals"** — the link `[Write methods](#mutation-terminals)`. The visible text says "Write methods"; the URL says something else. I guessed they are the same thing. Internal vocabulary leaking into a URL. +- **`[#array-operations-mongodb-unverified]`** — the literal word "unverified" in the heading id. I guessed this section has not been tested and I should not trust it. If that is true, say it. If it is not true, this should never have shipped. +- **`_id: '6650f1c2a1b2c3d4e5f60002'`** — a plain string. In Prisma 7 I passed ObjectId-ish values. I guessed a hex string is accepted and converted. The page never says. + +## Places I asked "so what do I actually type?" + +**1. Combining two `MongoFieldFilter`s.** The opening paragraph says "To join two conditions with AND or OR, see Combinators." It never shows whether I can just pass two filters to `.where()`, or whether `.where(a).where(b)` chains as AND. That is the first thing I would try. + +**2. Atomic increment on PostgreSQL.** "PostgreSQL has no callback form: pass an object to `update()` instead." So how do I increment a counter on PostgreSQL without a read-modify-write race? Prisma 7 had `{ increment: 1 }`. The page does not say whether that still exists, or points me nowhere. + +**3. Array operations on PostgreSQL.** Same gap. `push`, `pull`, `addToSet`, `pop` are all marked MongoDB. Postgres has arrays. The page is silent. + +**4. The array examples do not run.** "The example schema has no array field, so these four lines assume `tags String[]` on `User` and will not run against the schema on this page." So I cannot copy anything. Add `tags String[]` to the example schema, or show the contract line beside the code. + +**5. The cast.** `as unknown as Record` throws away the typing of every other key in that object. The page does not warn me, and does not say whether this is the recommended route or the one I should avoid in favour of `MongoFieldFilter.eq`. + +**6. Reading the raw MongoDB error.** Told to "read the message", shown no message and no example `catch`. + +**7. Does `.where({})` satisfy the "all four methods require `.where()`" rule?** If I want to update every document, what do I type? The page says only that the absence of `.where()` throws `ORM.WHERE_MISSING`. + +**8. Reusing an `AsyncIterableResult`.** I know awaiting twice is safe and switching modes throws. I do not know whether looping twice is safe. That is the case I would actually hit. + +## Places explaining internals when I only wanted the instruction + +- "`of` does no checking of its own, so a misspelled operator such as `'$regexp'` fails at the database, not in TypeScript." The useful half is "typos are not caught until the query runs". Where the check does not happen is internals. +- "Those are MongoDB's own `$pop` values." +- "an error raised by MongoDB, not by Prisma ORM" — I care that `error.code` is absent, not whose code threw. +- "There is no error class to test with `instanceof`." Then just tell me what to do: import `isRuntimeError` and check `error.code`. +- "`sum()`, `avg()`, `min()`, and `max()` resolve to `null`, not `0`, over an empty result set, so their TypeScript type includes `null`." The trailing clause restates the first clause in compiler terms. + +## Formatting bug + +Line 105 ("These examples use the MongoDB accessors from…") is immediately followed by the `##### set() (MongoDB)` heading with no blank line between them. + +## Could I do what the page is for, after one read? + +Partly. + +**Yes:** I could write MongoDB filters with `eq`/`gt`/`in`/`isNull`, reach into an embedded object with a dot path, run `set`/`unset`/`inc`/`mul` on MongoDB, and read a `groupBy().aggregate()` result shape correctly. + +**Still would not know:** + +- How to do an atomic increment or an array push on PostgreSQL, or whether it is possible at all. +- Whether `updateAndCount()` exists outside MongoDB, and what it actually returns beyond "a number". +- How to AND or OR two `MongoFieldFilter`s without leaving the page. +- What the raw MongoDB error looks like when I `pull` on a non-array, so I cannot write the `catch`. +- Whether the `// @ts-expect-error` workaround is a bug I should expect to be fixed, and whether I need one comment per line or one per call. +- Whether the array-operations section is trustworthy, given the word "unverified" is in its own heading id and its examples admittedly do not run. \ No newline at end of file From c9673775fe29ff88827376acd61d0a25b851daa4 Mon Sep 17 00:00:00 2001 From: willbot Date: Sat, 12 Sep 2026 10:28:34 +0200 Subject: [PATCH 08/11] docs(orm8): orm-client reference, final fact re-check against rc.9 Four checkers re-read every claim after the wording rounds. Thirteen corrections: a PostgreSQL DateTime filter takes a Temporal.Instant, not a Date; isRuntimeError is false for ORM.* errors, so catch blocks compare error.code; create() and connect() take an object or an array on any relation; prisma db update replaces prisma migrate dev; db.raw is raw queries, not SQL, on MongoDB; sum() and avg() accept an interval; ORM.CURSOR_VALUE_MISSING is thrown when the query runs; count() can throw out of range; countBigInt() also takes a field; the grouped chain order is having/orderBy then limit/offset; an example result made self-consistent; count maps to aggregate() on PostgreSQL only; the embedded-object set() needs every field. Two verified omissions added: MongoDB updateAndCount() excludes documents already holding the new values, and MongoDB writes reject orderBy/limit/offset and include() on the chain with ORM.OPERATION_UNSUPPORTED. Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- .../content/docs/orm/reference/orm-client.mdx | 31 ++++++++++--------- .../c21-orm-client/factcheck2.partA.md | 5 +++ .../c21-orm-client/factcheck2.partB.md | 7 +++++ .../c21-orm-client/factcheck2.partC.md | 5 +++ .../c21-orm-client/factcheck2.partD.md | 7 +++++ 5 files changed, 41 insertions(+), 14 deletions(-) create mode 100644 docs/orm-docs-audit/c21-orm-client/factcheck2.partA.md create mode 100644 docs/orm-docs-audit/c21-orm-client/factcheck2.partB.md create mode 100644 docs/orm-docs-audit/c21-orm-client/factcheck2.partC.md create mode 100644 docs/orm-docs-audit/c21-orm-client/factcheck2.partD.md diff --git a/apps/docs/content/docs/orm/reference/orm-client.mdx b/apps/docs/content/docs/orm/reference/orm-client.mdx index 1dcb1ee05fc..6c8052e1abb 100644 --- a/apps/docs/content/docs/orm/reference/orm-client.mdx +++ b/apps/docs/content/docs/orm/reference/orm-client.mdx @@ -8,7 +8,7 @@ metaDescription: 'Reference for the Prisma ORM client''s query, mutation, filter The ORM client gives you model-level methods for reading and writing data across PostgreSQL and MongoDB. This page documents every method, its availability on each database, and the behavior that differs between the two. Each method's Remarks say which databases it works on, how it behaves differently on one of them, and where TypeScript rejects something the database itself would accept. -This page assumes your tables already exist. [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7) has the packages to install and the `prisma orm init` command. It also covers `prisma db update`, which replaces `prisma migrate`, and `prisma contract emit`, which replaces `prisma generate`. [How migrations work](/orm/migrations/how-migrations-work) shows how the tables get created. For task-oriented walkthroughs, see the Fundamentals guides: [Reading data](/orm/fundamentals/reading-data), [Writing data](/orm/fundamentals/writing-data), [Relations and joins](/orm/fundamentals/relations-and-joins), and [Transactions](/orm/fundamentals/transactions). +This page assumes your tables already exist. [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7) has the packages to install and the `prisma orm init` command. It also covers `prisma db update`, which replaces `prisma migrate dev`, and `prisma contract emit`, which replaces `prisma generate`. [How migrations work](/orm/migrations/how-migrations-work) shows how the tables get created. For task-oriented walkthroughs, see the Fundamentals guides: [Reading data](/orm/fundamentals/reading-data), [Writing data](/orm/fundamentals/writing-data), [Relations and joins](/orm/fundamentals/relations-and-joins), and [Transactions](/orm/fundamentals/transactions). ## Example schema @@ -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 SQL. 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 @@ -414,7 +414,7 @@ On PostgreSQL, the callback you pass to `include()` receives the related rows as - PostgreSQL only. On MongoDB, `include()` takes no callback, and `count`, `sum`, `avg`, `min`, `max`, and `combine` do not exist on a collection. - On PostgreSQL, those six are only callable **inside** an `include()` callback. Called anywhere else, they throw an error whose `code` is `ORM.INCLUDE_INVALID`. -- `sum()` and `avg()` take a numeric column: an integer, a floating-point number, or a decimal. They also take a `Time` column, the time-of-day type. They do not take a date or a timestamp, and TypeScript rejects a date or timestamp field name here. +- `sum()` and `avg()` take a numeric column: an integer, a floating-point number, or a decimal. They also take an interval or a `Time` column, the time-of-day type. They do not take a date or a timestamp, and TypeScript rejects a date or timestamp field name here. - `min()` and `max()` take more: numeric and text columns, dates, times, timestamps, intervals, IP addresses, and text arrays. They do not take a boolean, a `uuid`, binary data, a bit string, or JSON. - `count()` returns a `number`. With no argument it counts rows; with a field name it counts rows where that field is not null. `countBigInt()` returns a `bigint` instead, for a count too large for a JavaScript `number`. - `sum()` over an integer column returns a `number`. A total too large for a `number` throws an error whose `code` is `RUNTIME.DECODE_FAILED`. Use `sumBigInt(field)` for totals that large. It returns a `bigint`. @@ -618,7 +618,7 @@ Resume pagination from a known position. - PostgreSQL only. `cursor()` does not exist on a MongoDB collection. - Always call `orderBy()` before `cursor()`. TypeScript rejects the call if you do not. Nothing checks this while the query runs, so without the `orderBy()` the cursor is ignored and every row comes back. -- The cursor object must name every column the `orderBy()` sorts on. Leave one out and the call throws an error whose `code` is `ORM.CURSOR_VALUE_MISSING`. With two sort columns, give both keys. The cursor compares the sort columns in order: first by the first column, then by the second for rows that tie on the first. +- The cursor object must name every column the `orderBy()` sorts on. Leave one out and running the query throws an error whose `code` is `ORM.CURSOR_VALUE_MISSING`. With two sort columns, give both keys. The cursor compares the sort columns in order: first by the first column, then by the second for rows that tie on the first. #### Options @@ -914,9 +914,9 @@ const features = await tasks.Task.features().all(); 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. -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). There is no `skipDuplicates` option. +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 and check its `code`, copy the `catch` block under [`AsyncIterableResult`](#asynciterableresult). For a task-oriented walkthrough, see [Writing data](/orm/fundamentals/writing-data). For grouping several writes into one unit, see [Transactions](/orm/fundamentals/transactions). +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. @@ -943,7 +943,7 @@ Insert a single row and return it. |---|---|---|---| | The row's fields | Object of field values | Yes | The row to insert. Pass the object as the only argument. There is no `data` wrapper. | -On PostgreSQL, that object can also carry a `create` or `connect` callback for a related row, as shown below. `posts.create([...])` and `tags.disconnect([...])` take arrays because a user can have many posts and a post can have many tags. `user.connect({...})` takes one object because a post has one user. +On PostgreSQL, that object can also carry a `create` or `connect` callback for a related row, as shown below. `create()` and `connect()` each take one object or an array of objects, on any relation. `disconnect()` takes an array, and on a many-to-many relation the array is required. #### Return type @@ -1204,6 +1204,7 @@ Update every matching row and return the count. #### Remarks - Available for PostgreSQL and MongoDB. Call `where()` first. Without a filter this changes every row. See the warning at the top of this section. +- On MongoDB the count leaves out documents that already held the new values. On PostgreSQL it counts every matched row. #### Options @@ -1474,7 +1475,7 @@ Compute aggregates over the rows the query matches. `aggregate()` runs the query #### Remarks - PostgreSQL only. Not available on MongoDB. -- `count()` counts rows. It takes no argument and always returns a number, `0` when no rows match. +- `count()` counts rows and returns a number, `0` when no rows match. - `count(field)` counts the rows where that field is not null. - To count or sum the related rows of each parent row, use the `include()` callback instead; see [Refinements, aggregates, and combine](#refinements-aggregates-and-combine). - `sum()`, `avg()`, `min()`, and `max()` return `null`, not `0`, when no rows match. Handle the `null`. @@ -1494,7 +1495,7 @@ Use `sumBigInt()` when the total can pass `Number.MAX_SAFE_INTEGER`. A `sum()` w |---|---|---|---| | `selector` | Callback `(agg) => ({ total: agg.sum('amount') })` | Yes | The aggregates to compute. Each key you return becomes a key of the result object. | -The callback's `agg` argument gives you `count()`, `count(field)`, `sum(field)`, `avg(field)`, `min(field)`, and `max(field)`, plus `countBigInt()`, `sumBigInt(field)`, and `avgDecimal(field)`. `agg.countBigInt()` takes no argument and returns a `bigint`. +The callback's `agg` argument gives you `count()`, `count(field)`, `sum(field)`, `avg(field)`, `min(field)`, and `max(field)`, plus `countBigInt()`, `sumBigInt(field)`, and `avgDecimal(field)`. `agg.countBigInt()` counts the same rows as `agg.count()` and returns a `bigint`. #### Return type @@ -1523,7 +1524,7 @@ const stats = await db.orm.public.Order.where({ totalAmount: agg.sum('amount'), avgAmount: agg.avg('amount'), })); -// { totalAmount: 1500, avgAmount: 300 } +// { totalAmount: 900, avgAmount: 300 } ``` ##### Min and max (PostgreSQL) @@ -1574,7 +1575,7 @@ Group rows by one or more fields, then aggregate per group. - PostgreSQL only. Not available on MongoDB. - Pass field names as separate arguments: `groupBy('customerId', 'channel')`. - `groupBy(...)` returns a `GroupedCollection`. You cannot `await` it. Call `.aggregate(...)` on it, which runs the query and gives you one row per group. Each row holds the grouping fields, typed as they are on your model, plus the keys you returned from the aggregate callback. -- Chain order: `where(...)` comes before `groupBy(...)` and filters the rows that get grouped. After `groupBy(...)` you can call `having(...)`, `orderBy(...)`, `limit(...)`, and `offset(...)` in any order. `aggregate(...)` comes last. +- Chain order: `where(...)` comes before `groupBy(...)` and filters the rows that get grouped. After `groupBy(...)` come `having(...)` and `orderBy(...)`, then `limit(...)` and `offset(...)`. `aggregate(...)` comes last. - `orderBy(...)` on a grouped collection sorts the groups by a field you grouped on. You cannot sort groups by an aggregate value. To do that, write the query with the SQL query builder instead. See [Advanced queries](/orm/fundamentals/advanced-queries). - `limit(n)` and `offset(n)` take a slice of the groups, and both require an `orderBy(...)` before them. Without one they are a TypeScript error. @@ -1695,7 +1696,9 @@ const notAlice = await db.orm.public.User.where((u) => u.email.neq('alice@exampl ##### Ordered comparisons (PostgreSQL) ```typescript -const after = await db.orm.public.Post.where((p) => p.createdAt.gt(new Date('2024-01-02T10:00:00.000Z'))).all(); +const after = await db.orm.public.Post.where((p) => + p.createdAt.gt(Temporal.Instant.from('2024-01-02T10:00:00.000Z')), +).all(); // gte() is the same comparison, including the boundary value. ``` @@ -1847,7 +1850,7 @@ Helpers that build one MongoDB filter condition. Use them for anything other tha - MongoDB. Import from `@prisma/orm-mongo/query-ast/execution`. `MongoExistsExpr` is in the same module. - The helpers are `of`, `eq`, `neq`, `gt`, `lt`, `gte`, `lte`, `in`, `nin`, `isNull`, and `isNotNull`. Each returns one filter, which you can pass straight to `.where()` or hold in a variable. There is no `ne`; use `neq`. -- `of(field, operator, value)`: pass the MongoDB operator as a string, including the `$`, for example `'$regex'`. Use it for operators the named helpers do not cover, such as `MongoFieldFilter.of('tags', '$size', 3)`. A misspelled operator such as `'$regexp'` is not caught until the query runs. +- `of(field, operator, value)`: pass the MongoDB operator as a string, including the `$`, for example `'$regex'`. Use it for operators the named helpers do not cover, such as `MongoFieldFilter.of('tags', '$size', 3)` on an array field `tags`. A misspelled operator such as `'$regexp'` is not caught until the query runs. - `isNull(field)` matches documents where the field is `null` and documents where the field is missing. `isNotNull(field)` matches everything else. To test only whether a field is present, use `MongoExistsExpr.exists(field)` or `MongoExistsExpr.notExists(field)`. Those two are all `MongoExistsExpr` offers. - There is no `regex`, `elemMatch`, `all`, or `size` helper. Write those operators through `of`. @@ -1926,7 +1929,7 @@ const usersInSf = await db.orm.users 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. - 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', country: 'US' })`. +- 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. diff --git a/docs/orm-docs-audit/c21-orm-client/factcheck2.partA.md b/docs/orm-docs-audit/c21-orm-client/factcheck2.partA.md new file mode 100644 index 00000000000..44f83fccb53 --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-client/factcheck2.partA.md @@ -0,0 +1,5 @@ +# Final fact re-check, slice A (Example schema, Setting up the client, Query-building methods) + +Four corrections: `prisma db update` replaces `prisma migrate dev` (not all of `prisma migrate`); `db.raw` is "raw queries", since on MongoDB it is a raw collection API, not SQL; `sum()`/`avg()` accept an interval as well as a `Time` column (restored); `ORM.CURSOR_VALUE_MISSING` is thrown when the query runs, not by the `cursor()` call. + +Everything else confirmed, including the `native_enum` block and the bare-member rejection, the `@@base` discriminator value, the many-to-many rule, `db.close()`, MongoDB `select()` giving `undefined`, the `_id` decode difference for included relations, `DateTime` as `Temporal.Instant` on both read and filter, `distinct()` keeping an undefined row among ties, and `distinctOn()` ordering. Native enum sort order comes from code, not a test. Q: the illustrative result numbers in the `Customer`/`Order` and count examples have no fixture behind them (self-consistent, left as illustrations; the page no longer claims they come from tests). diff --git a/docs/orm-docs-audit/c21-orm-client/factcheck2.partB.md b/docs/orm-docs-audit/c21-orm-client/factcheck2.partB.md new file mode 100644 index 00000000000..50ce7a6e39f --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-client/factcheck2.partB.md @@ -0,0 +1,7 @@ +# Final fact re-check, slice B (Read methods, Custom Collection, Write methods) + +Three corrections: `count` maps to `aggregate()` on PostgreSQL only, with the two-step pipeline count on MongoDB; the error-handling pointer no longer sends readers to the `isRuntimeError` example, because `ORM.*` errors are `StructuredError`s without category/severity and `isRuntimeError` returns `false` for them (compare `error.code` directly); `create()` and `connect()` each take one object or an array on any relation, `disconnect()` takes an array, required on a many-to-many. + +Everything else confirmed, including the two-step MongoDB count, `firstOrThrow()` reading every row, the `orm(...)` example against `examples/paradedb-demo`, `ORM.WHERE_MISSING` at seven call sites, all six PostgreSQL write methods typed `never` without `where()`, nested create without `userId` type-checking (`NestedCreateInput` makes child foreign keys optional), `createAll()` single statement, the MTI messages character for character, `conflictOn` reading keys only and a multi-column set matching one `@@unique`, and the MongoDB upsert `_id` hex string. No Q items. + +Two verified omissions, added by the orchestrator after this report: MongoDB `updateAndCount()` returns the driver's `modifiedCount`, so documents already holding the new values are not counted; on MongoDB, `orderBy()`, `limit()`, or `offset()` before any write, and `include()` before any write except `updateAll()`/`deleteAll()`, throw `ORM.OPERATION_UNSUPPORTED`. diff --git a/docs/orm-docs-audit/c21-orm-client/factcheck2.partC.md b/docs/orm-docs-audit/c21-orm-client/factcheck2.partC.md new file mode 100644 index 00000000000..817f87c89e0 --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-client/factcheck2.partC.md @@ -0,0 +1,5 @@ +# Final fact re-check, slice C (Grouped aggregates, Filter conditions through Shorthand object filter) + +Five corrections: `count()` no longer "always" returns a number (an out-of-range count throws); `countBigInt()` also takes a field; the sum/avg example result made internally consistent (900, not 1500); the grouped chain order is `having`/`orderBy` then `limit`/`offset` (the types require `orderBy` first); the PostgreSQL `DateTime` comparison example uses `Temporal.Instant.from(...)`, because the `pg/timestamptz-temporal@1` codec's input type is `Temporal.Instant`, not `Date`. + +Everything else confirmed, including the sum/avg return-type table, `aggregate()` after `limit()`, `every()` on a to-one relation, the `is`/`isNot` mapping, and the `Json` shorthand behaviour. No Q items. Two facts have code but no test: `RUNTIME.DECODE_FAILED` from an over-range `sum()`, and `every()` on a to-one relation with no related row. `ports/.../sum.test.ts` marks its "honors query arguments" cases `it.fails`; the ORM's own `aggregate.test.ts` passes for `limit()`/`offset()`. diff --git a/docs/orm-docs-audit/c21-orm-client/factcheck2.partD.md b/docs/orm-docs-audit/c21-orm-client/factcheck2.partD.md new file mode 100644 index 00000000000..0052a71be89 --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-client/factcheck2.partD.md @@ -0,0 +1,7 @@ +# Final fact re-check, slice D (MongoFieldFilter, dot notation, field update operations, result types) + +One correction: `u.address.set({...})` must give every field of the embedded object, including `zip`, because the generated type makes optional fields required keys of type `string | null`. + +Everything else confirmed, including `.where(a).where(b)` ANDing, the eleven helpers, `of()` passthrough, `isNull` semantics, `MongoExistsExpr`, the cast (compiled with the repo's TypeScript), the four callback-taking methods, the function-call form for dot paths, `ORM.WHERE_MISSING` on `where({})`, `_id` as a hex string, the variant typing (no test), the eight operations and signatures, `AsyncIterableResult` rules, and the aggregate result shapes. + +Q (left as is): `inc`/`mul` on a missing field is MongoDB server behaviour; `isNotNull('_id')` matching every document rests on MongoDB's `_id` guarantee; the `$size` illustration names a `tags` field the schema does not have (reworded by the orchestrator to say "on an array field"); two different operators on the same field in one update is a MongoDB conflict, untested here. From 09a16341155872e28e147e90d7129fdd12c0f2bf Mon Sep 17 00:00:00 2001 From: willbot Date: Sat, 12 Sep 2026 10:28:34 +0200 Subject: [PATCH 09/11] docs: remove the C21 working reports for the orm-client page The reader reports, checker reports, and conventions files stay in this branch's history at d104536a5 for anyone reviewing the pass; they are not part of the docs. Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- .../c21-orm-client/conventions.md | 24 ---- docs/orm-docs-audit/c21-orm-client/extract.py | 9 -- .../c21-orm-client/factcheck1.partA.md | 9 -- .../c21-orm-client/factcheck1.partB.md | 9 -- .../c21-orm-client/factcheck1.partC.md | 7 -- .../c21-orm-client/factcheck1.partD.md | 7 -- .../c21-orm-client/factcheck2.partA.md | 5 - .../c21-orm-client/factcheck2.partB.md | 7 -- .../c21-orm-client/factcheck2.partC.md | 5 - .../c21-orm-client/factcheck2.partD.md | 7 -- .../c21-orm-client/page-conventions.md | 9 -- .../c21-orm-client/part1.round1.md | 86 --------------- .../c21-orm-client/part1.round2.md | 103 ------------------ .../c21-orm-client/part1.round3.md | 92 ---------------- .../c21-orm-client/part1.round4.md | 81 -------------- .../c21-orm-client/part2.round1.md | 101 ----------------- .../c21-orm-client/part2.round2.md | 87 --------------- .../c21-orm-client/part2.round3.md | 76 ------------- .../c21-orm-client/part2.round4.md | 90 --------------- .../c21-orm-client/part3.round1.md | 82 -------------- .../c21-orm-client/part3.round2.md | 85 --------------- .../c21-orm-client/part3.round3.md | 75 ------------- .../c21-orm-client/part3.round4.md | 81 -------------- .../c21-orm-client/part4.round1.md | 65 ----------- .../c21-orm-client/part4.round2.md | 98 ----------------- .../c21-orm-client/part4.round3.md | 86 --------------- .../c21-orm-client/part4.round4.md | 86 --------------- 27 files changed, 1472 deletions(-) delete mode 100644 docs/orm-docs-audit/c21-orm-client/conventions.md delete mode 100644 docs/orm-docs-audit/c21-orm-client/extract.py delete mode 100644 docs/orm-docs-audit/c21-orm-client/factcheck1.partA.md delete mode 100644 docs/orm-docs-audit/c21-orm-client/factcheck1.partB.md delete mode 100644 docs/orm-docs-audit/c21-orm-client/factcheck1.partC.md delete mode 100644 docs/orm-docs-audit/c21-orm-client/factcheck1.partD.md delete mode 100644 docs/orm-docs-audit/c21-orm-client/factcheck2.partA.md delete mode 100644 docs/orm-docs-audit/c21-orm-client/factcheck2.partB.md delete mode 100644 docs/orm-docs-audit/c21-orm-client/factcheck2.partC.md delete mode 100644 docs/orm-docs-audit/c21-orm-client/factcheck2.partD.md delete mode 100644 docs/orm-docs-audit/c21-orm-client/page-conventions.md delete mode 100644 docs/orm-docs-audit/c21-orm-client/part1.round1.md delete mode 100644 docs/orm-docs-audit/c21-orm-client/part1.round2.md delete mode 100644 docs/orm-docs-audit/c21-orm-client/part1.round3.md delete mode 100644 docs/orm-docs-audit/c21-orm-client/part1.round4.md delete mode 100644 docs/orm-docs-audit/c21-orm-client/part2.round1.md delete mode 100644 docs/orm-docs-audit/c21-orm-client/part2.round2.md delete mode 100644 docs/orm-docs-audit/c21-orm-client/part2.round3.md delete mode 100644 docs/orm-docs-audit/c21-orm-client/part2.round4.md delete mode 100644 docs/orm-docs-audit/c21-orm-client/part3.round1.md delete mode 100644 docs/orm-docs-audit/c21-orm-client/part3.round2.md delete mode 100644 docs/orm-docs-audit/c21-orm-client/part3.round3.md delete mode 100644 docs/orm-docs-audit/c21-orm-client/part3.round4.md delete mode 100644 docs/orm-docs-audit/c21-orm-client/part4.round1.md delete mode 100644 docs/orm-docs-audit/c21-orm-client/part4.round2.md delete mode 100644 docs/orm-docs-audit/c21-orm-client/part4.round3.md delete mode 100644 docs/orm-docs-audit/c21-orm-client/part4.round4.md diff --git a/docs/orm-docs-audit/c21-orm-client/conventions.md b/docs/orm-docs-audit/c21-orm-client/conventions.md deleted file mode 100644 index 3cc56bc1c2f..00000000000 --- a/docs/orm-docs-audit/c21-orm-client/conventions.md +++ /dev/null @@ -1,24 +0,0 @@ -# Shared conventions for the C21 plain-language pass (fundamentals) - -Verified facts (rc.9 source at wip/prisma-src/). 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`. The list is at /orm/reference/raw-queries. -- 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-orm-client/extract.py b/docs/orm-docs-audit/c21-orm-client/extract.py deleted file mode 100644 index d91320e717e..00000000000 --- a/docs/orm-docs-audit/c21-orm-client/extract.py +++ /dev/null @@ -1,9 +0,0 @@ -import sys,json -out=[] -for line in open(sys.argv[1]): - try: o=json.loads(line) - except: continue - if o.get("type")!="assistant": continue - for b in o.get("message",{}).get("content",[]): - if isinstance(b,dict) and b.get("type")=="text": out=[b["text"]] -print(out[0] if out else "") diff --git a/docs/orm-docs-audit/c21-orm-client/factcheck1.partA.md b/docs/orm-docs-audit/c21-orm-client/factcheck1.partA.md deleted file mode 100644 index 39becf06550..00000000000 --- a/docs/orm-docs-audit/c21-orm-client/factcheck1.partA.md +++ /dev/null @@ -1,9 +0,0 @@ -# Fact re-check 1, slice A (Example schema, Setting up the client, Query-building methods) - -746 lines before and after. Corrections: removed "every example is copied from a test suite" (no fixture in rc.9 has this schema; PR #8033 says the harness was kept out of git) and "rows the test suite inserted first"; MongoDB related `_id` wording; a second argument to MongoDB `include()` is a TypeScript error first, ignored at runtime; aggregates outside `include()` throw `ORM.INCLUDE_INVALID`; `sum`/`avg` accept interval and `Time` (not `timetz`), and the TypeScript error rejects the field name rather than naming the operation; `count(field)` counts non-null values; `avgDecimal` returns a decimal string; `min`/`max` also `null` over an empty relation; `Temporal` note replaced by the source's own polyfill instruction (`temporal-polyfill/full/global`); a variant gets its own table only when it sets `@@map`; em-dash removed. - -Confirmed: every PSL construct in the schema individually (`types`, `type`, `@@type("pg/text@1")`, `native_enum`, `Uuid @default(uuid())`, `@@base`, `@@discriminator`, `PostTag` requirement, `ObjectId @id @map("_id")`, `@@index(... sort: Desc)`), `.eq()` takes the stored value, `variant()` takes the model name on both databases, `postgres(...)`/`mongo(...)` options, `extensions: [pgvector]`, `orm(...)` registration, import paths, `where`/`select`/`include`/`orderBy`/`limit`/`offset`/`cursor`/`distinct`/`distinctOn` claims, native enum ordering (code only, no test sorts an enum). - -Q: every result comment and id literal (invented, internally consistent); "write your own ranking expression" is general SQL advice. - -Not changed, for the operator: `distinctOn()` also throws `ORM.CAPABILITY_MISSING` without `postgres.distinctOn` in the contract; `cursor()` throws `ORM.CURSOR_VALUE_MISSING` when the cursor object leaves out an `orderBy` column; MongoDB `select()` projects field names, so with `@map("_id")` the field is `_id`. diff --git a/docs/orm-docs-audit/c21-orm-client/factcheck1.partB.md b/docs/orm-docs-audit/c21-orm-client/factcheck1.partB.md deleted file mode 100644 index 3c618ff1465..00000000000 --- a/docs/orm-docs-audit/c21-orm-client/factcheck1.partB.md +++ /dev/null @@ -1,9 +0,0 @@ -# Fact re-check 1, slice B (Read methods, Custom Collection, Write methods) - -693 lines before, 692 after. Corrections: `all()` takes no required argument (it has an optional annotations callback); on PostgreSQL TypeScript refuses all six update/delete methods without `where()`, not just `update()`/`delete()`; the multi-table variant in the page's PostgreSQL schema is `Bug` on `Task`, so the `ORM.OPERATION_UNSUPPORTED` messages for `createAndCount()` and `upsert()` now name those; PostgreSQL `update()` returns `null` whenever the data maps to no columns (a function or an empty object); the MongoDB update input is typed on the base model only, so the `t.duration` examples were replaced with `set()` on base fields; the MongoDB upsert `_id` comes back decoded as a hex string, not an `ObjectId` (codecs.ts, CHANGELOG). - -Confirmed: no `count()` read method; `first()` inline filter ANDs with `where()` on PostgreSQL and takes no argument on MongoDB; `orm(...)` with `collections`, `client.connect()`, `client.context`; the `updateAll() requires a .where() filter...` message (code `ORM.WHERE_MISSING`); PostgreSQL create optional fields (nullable, column default, execution default); MongoDB `_id` only optional field; MongoDB create returns passed values plus `_id`; nested `create`/`connect`/`disconnect` PostgreSQL only; MongoDB `updateAll()` three steps; no `db.transaction()` on MongoDB; `deleteAll()` deletes before yielding; `deleteAndCount()` returns 0; `conflictOn` reads only the column name, value unused; PostgreSQL `upsert()` ignores `where()`; omitted `conflictOn` uses the primary key; MongoDB upsert `update` wins over `create` on insert. - -Q: the Prisma ORM 7 halves of the diff blocks (no ORM 7 client in the tree); whether the nested `create()` example type-checks without `userId` (matches the source's own doc example). - -Operator note: the MongoDB example schema has no numeric field on a base model, so `inc()`/`mul()` cannot be shown on MongoDB without `@ts-expect-error` or a schema change. diff --git a/docs/orm-docs-audit/c21-orm-client/factcheck1.partC.md b/docs/orm-docs-audit/c21-orm-client/factcheck1.partC.md deleted file mode 100644 index 31cb21f85ac..00000000000 --- a/docs/orm-docs-audit/c21-orm-client/factcheck1.partC.md +++ /dev/null @@ -1,7 +0,0 @@ -# Fact re-check 1, slice C (Grouped aggregates, filter conditions through shorthand filter) - -Six edits, line count unchanged (408). Corrections: `quantity Int?` in the aggregate schema; `min()`/`max()` also `null` over an empty set (Remarks and Return table); selector also has `countBigInt`, `sumBigInt`, `avgDecimal`; to-one relation filtering works from either side; shorthand `null` on MongoDB also matches a missing field; `ORM.FILTER_UNSUPPORTED` is PostgreSQL only. - -Q: the result comments in the `aggregate()`/`groupBy()` examples (`// { total: 10 }` etc.) and the Customer/Order schema itself have no seed data or fixture in the rc.9 source. The real aggregate tests run on the fixture in `test/integration/test/sql-orm-client/fixtures/contract.ts` (User/Post with `Post.views`). Page-wide decision needed on the "every example is copied from a test suite" claim. - -Confirmed: everything else in the slice, including `some()` taking an object or a callback, `MongoOrExpr.and()`, variadic `groupBy`, grouped `orderBy` by grouping field only, `limit`/`offset` typed `never` without `orderBy`, no `having` by alias, `in([])` renders FALSE and `notIn([])` TRUE. diff --git a/docs/orm-docs-audit/c21-orm-client/factcheck1.partD.md b/docs/orm-docs-audit/c21-orm-client/factcheck1.partD.md deleted file mode 100644 index 8ca7f2dbeaa..00000000000 --- a/docs/orm-docs-audit/c21-orm-client/factcheck1.partD.md +++ /dev/null @@ -1,7 +0,0 @@ -# Fact re-check 1, slice D (MongoFieldFilter, dot notation, field update operations, result types) - -Line count unchanged (209). Corrections: the library does pass `$regex`, `$elemMatch`, `$size` through `MongoFieldFilter.of` (and the pipeline builder has `$size`/`$regexMatch`), so the "no support anywhere" claim was wrong; `of()` example now uses `'$regex'` on a real field (`age` did not exist); `updateAll()` also takes the field-operations callback and the callback argument is callable for dot paths; `set`/`unset`/`inc`/`mul` no longer "verified" (`mul`/`unset` have unit tests only); `inc`/`mul` are typed on numeric fields only; a misspelled operation is a TypeScript error (closed object type), not `undefined` at runtime; `upsert()` rejects a dot path; the four array operations are tested (`orm/test/collection.test.ts:294-322`, `push`/`pull` also on a live database), so the body no longer says "not tested"; four operations on the same field in one `update()` conflict, so the example is split; the array example moved to `db.orm.users` assuming `tags String[]`; the `ITERATOR_CONSUMED` message wording is now "contains". - -Confirmed: eleven `MongoFieldFilter` factories, `isNull` is equals-null, `MongoExistsExpr`, dot path at runtime and the cast, eight field operations and their signatures (`push(value)`, `pull(match)`, `addToSet(value)`, `pop(1 | -1)`), callback typed against the base model after `variant()` so `t.duration.inc(10)` needs `// @ts-expect-error` (slice B's copy without it is wrong), MongoDB `update()` returns `null` only on no match (bad `_id` write throws `ORM.FIELD_IMMUTABLE`), both `components/runtime` import paths, re-await safety, `RUNTIME.ITERATOR_CONSUMED`, aggregate result shapes. - -Q: `inc`/`mul` on a missing field (MongoDB server behaviour, not in the tree); the server error text for an array operation on a non-array field (not in the tree). Heading "Array operations (MongoDB, unverified)": "unverified" is no longer true. diff --git a/docs/orm-docs-audit/c21-orm-client/factcheck2.partA.md b/docs/orm-docs-audit/c21-orm-client/factcheck2.partA.md deleted file mode 100644 index 44f83fccb53..00000000000 --- a/docs/orm-docs-audit/c21-orm-client/factcheck2.partA.md +++ /dev/null @@ -1,5 +0,0 @@ -# Final fact re-check, slice A (Example schema, Setting up the client, Query-building methods) - -Four corrections: `prisma db update` replaces `prisma migrate dev` (not all of `prisma migrate`); `db.raw` is "raw queries", since on MongoDB it is a raw collection API, not SQL; `sum()`/`avg()` accept an interval as well as a `Time` column (restored); `ORM.CURSOR_VALUE_MISSING` is thrown when the query runs, not by the `cursor()` call. - -Everything else confirmed, including the `native_enum` block and the bare-member rejection, the `@@base` discriminator value, the many-to-many rule, `db.close()`, MongoDB `select()` giving `undefined`, the `_id` decode difference for included relations, `DateTime` as `Temporal.Instant` on both read and filter, `distinct()` keeping an undefined row among ties, and `distinctOn()` ordering. Native enum sort order comes from code, not a test. Q: the illustrative result numbers in the `Customer`/`Order` and count examples have no fixture behind them (self-consistent, left as illustrations; the page no longer claims they come from tests). diff --git a/docs/orm-docs-audit/c21-orm-client/factcheck2.partB.md b/docs/orm-docs-audit/c21-orm-client/factcheck2.partB.md deleted file mode 100644 index 50ce7a6e39f..00000000000 --- a/docs/orm-docs-audit/c21-orm-client/factcheck2.partB.md +++ /dev/null @@ -1,7 +0,0 @@ -# Final fact re-check, slice B (Read methods, Custom Collection, Write methods) - -Three corrections: `count` maps to `aggregate()` on PostgreSQL only, with the two-step pipeline count on MongoDB; the error-handling pointer no longer sends readers to the `isRuntimeError` example, because `ORM.*` errors are `StructuredError`s without category/severity and `isRuntimeError` returns `false` for them (compare `error.code` directly); `create()` and `connect()` each take one object or an array on any relation, `disconnect()` takes an array, required on a many-to-many. - -Everything else confirmed, including the two-step MongoDB count, `firstOrThrow()` reading every row, the `orm(...)` example against `examples/paradedb-demo`, `ORM.WHERE_MISSING` at seven call sites, all six PostgreSQL write methods typed `never` without `where()`, nested create without `userId` type-checking (`NestedCreateInput` makes child foreign keys optional), `createAll()` single statement, the MTI messages character for character, `conflictOn` reading keys only and a multi-column set matching one `@@unique`, and the MongoDB upsert `_id` hex string. No Q items. - -Two verified omissions, added by the orchestrator after this report: MongoDB `updateAndCount()` returns the driver's `modifiedCount`, so documents already holding the new values are not counted; on MongoDB, `orderBy()`, `limit()`, or `offset()` before any write, and `include()` before any write except `updateAll()`/`deleteAll()`, throw `ORM.OPERATION_UNSUPPORTED`. diff --git a/docs/orm-docs-audit/c21-orm-client/factcheck2.partC.md b/docs/orm-docs-audit/c21-orm-client/factcheck2.partC.md deleted file mode 100644 index 817f87c89e0..00000000000 --- a/docs/orm-docs-audit/c21-orm-client/factcheck2.partC.md +++ /dev/null @@ -1,5 +0,0 @@ -# Final fact re-check, slice C (Grouped aggregates, Filter conditions through Shorthand object filter) - -Five corrections: `count()` no longer "always" returns a number (an out-of-range count throws); `countBigInt()` also takes a field; the sum/avg example result made internally consistent (900, not 1500); the grouped chain order is `having`/`orderBy` then `limit`/`offset` (the types require `orderBy` first); the PostgreSQL `DateTime` comparison example uses `Temporal.Instant.from(...)`, because the `pg/timestamptz-temporal@1` codec's input type is `Temporal.Instant`, not `Date`. - -Everything else confirmed, including the sum/avg return-type table, `aggregate()` after `limit()`, `every()` on a to-one relation, the `is`/`isNot` mapping, and the `Json` shorthand behaviour. No Q items. Two facts have code but no test: `RUNTIME.DECODE_FAILED` from an over-range `sum()`, and `every()` on a to-one relation with no related row. `ports/.../sum.test.ts` marks its "honors query arguments" cases `it.fails`; the ORM's own `aggregate.test.ts` passes for `limit()`/`offset()`. diff --git a/docs/orm-docs-audit/c21-orm-client/factcheck2.partD.md b/docs/orm-docs-audit/c21-orm-client/factcheck2.partD.md deleted file mode 100644 index 0052a71be89..00000000000 --- a/docs/orm-docs-audit/c21-orm-client/factcheck2.partD.md +++ /dev/null @@ -1,7 +0,0 @@ -# Final fact re-check, slice D (MongoFieldFilter, dot notation, field update operations, result types) - -One correction: `u.address.set({...})` must give every field of the embedded object, including `zip`, because the generated type makes optional fields required keys of type `string | null`. - -Everything else confirmed, including `.where(a).where(b)` ANDing, the eleven helpers, `of()` passthrough, `isNull` semantics, `MongoExistsExpr`, the cast (compiled with the repo's TypeScript), the four callback-taking methods, the function-call form for dot paths, `ORM.WHERE_MISSING` on `where({})`, `_id` as a hex string, the variant typing (no test), the eight operations and signatures, `AsyncIterableResult` rules, and the aggregate result shapes. - -Q (left as is): `inc`/`mul` on a missing field is MongoDB server behaviour; `isNotNull('_id')` matching every document rests on MongoDB's `_id` guarantee; the `$size` illustration names a `tags` field the schema does not have (reworded by the orchestrator to say "on an array field"); two different operators on the same field in one update is a MongoDB conflict, untested here. diff --git a/docs/orm-docs-audit/c21-orm-client/page-conventions.md b/docs/orm-docs-audit/c21-orm-client/page-conventions.md deleted file mode 100644 index 14a95fa9bef..00000000000 --- a/docs/orm-docs-audit/c21-orm-client/page-conventions.md +++ /dev/null @@ -1,9 +0,0 @@ -# Conventions for the orm-client reference page (in addition to conventions.md) - -- This is a reference page. Keep the per-method layout (Remarks, Options, Return, Examples, PostgreSQL/MongoDB tabs). Do not add tutorial prose; one sentence and a link to the fundamentals page is the most a "how do I" question gets, unless the answer is a fact about the method (an argument, a return value, what happens on no match). -- The page is edited in four slices by four agents at once. Edit only your slice file. Do not renumber or rename headings; if you must rename a heading, pin the old anchor with `[#old-anchor]`. -- Terms defined once, in part 1 (Example schema / Setting up the client), and used bare elsewhere: contract ("your contract, the `contract.prisma` file that replaced `schema.prisma`; `npx prisma contract emit` writes `contract.json` and `contract.d.ts` from it"); `db.orm.public.User` (models by model name; `public` is the PostgreSQL schema); `db.orm.users` (on MongoDB the accessor is the collection name from `@@map`, or the model name with a lowercase first letter); variant ("a model declared with `@@base`, stored with its base model; `variant("Bug")` narrows a collection to that model; the name is the model name"). Parts 2 to 4 may say "see [Example schema](#example-schema)" but must not redefine these. -- Banned on this page in addition to the list: terminal, facet, root name(s), roots map, aggregate map, emitted contract, type-only guard, consumption mode, child-owned/parent-owned (say "the side that holds the foreign key"), filter surface, relation refinement (say "relation filter"), AST node, textual-trait, adapter-registered, "unverified on the current test contract" (say "not tested; try it on a real field before relying on it", or state the fact if the source shows it), "vacuously true", materializing, `float8` as a JavaScript type (say `number`), `$set`/`$setOnInsert`/`$lookup`/`RETURNING`/`LIMIT 1` when the sentence is about what the reader gets rather than how to write a query. -- Every example's ids: declare them once at the top of the Examples block of the section (`const aliceId = "..."`) or replace with a literal. A page that claims examples are transcribed from tests must have examples that run as printed. -- Error codes: "throws an error whose `code` is `X`". Give one string, not two. -- Facts the readers asked for that you must look up in the rc.9 source before writing (do not guess; mark `Q` if not found): whether `create` requires an explicit id or nullable fields; what `conflictOn` takes and whether the value must match `create`; whether PostgreSQL `upsert` needs `where()`; whether `.eq()` on an enum takes the member name or the mapped value; whether `variant()` takes the model name or the discriminator value; whether `some()` takes an object as well as a callback; whether `MongoOrExpr` has `.and()`; `groupBy` with several fields; `orderBy`/`limit` on a grouped collection; `having` by alias; `in([])` behaviour; the full list of MongoDB field operations and the signatures of `push`/`pull`/`addToSet`/`pop`; whether nested `create`/`connect` work on MongoDB; whether `update()` returning `null` can also mean a wrong argument shape (if so, say so plainly); native vs text-backed enums (define both in one sentence each, with the contract syntax). diff --git a/docs/orm-docs-audit/c21-orm-client/part1.round1.md b/docs/orm-docs-audit/c21-orm-client/part1.round1.md deleted file mode 100644 index c206db6691b..00000000000 --- a/docs/orm-docs-audit/c21-orm-client/part1.round1.md +++ /dev/null @@ -1,86 +0,0 @@ -Read once, top to bottom. Here's what stopped me. - -## Sentences I could not restate after one reading - -1. **Line 186:** "Models hang off `db.orm.public` and use the contract's **root names**, which match your Prisma Schema Language (PSL) model names" - Stopped by: "the contract's root names." I don't know what a contract is, what a root name is, or why an extra concept is needed to say "the name matches your model." Plainer: "Models hang off `db.orm.public` under their model names from the schema: `db.orm.public.User`." - -2. **Line 204:** "use the contract's **root names too, but those are the lowercase plural collection names** from the contract's `roots` map, not the PSL model names" - Stopped by: three ideas at once — the same term meaning something different, a `roots` map I've never seen, and a negation. Also it contradicts itself in feel: if "root name" is defined as "matches the PSL model name" one paragraph earlier, then here root names don't match model names. Plainer: "On MongoDB, models hang off `db.orm` under the collection name you mapped with `@@map`: `db.orm.users`." - -3. **Line 382:** "Which fields each of those functions accepts comes from the aggregate map in your emitted contract, not from a fixed rule." - Stopped by: "aggregate map," "emitted contract." I cannot look at my schema and predict what will compile. Plainer: "`sum()` and `avg()` accept numeric columns. `min()` and `max()` also accept text and date/time columns. Anything else is a compile error." - -4. **Line 383:** "`sum()` over an integer column returns `number` and raises `RUNTIME.DECODE_FAILED` outside ±(2^53 − 1); `avg()` over an integer column returns a `float8` `number`; `sumBigInt(field)` and `avgDecimal(field)` are the lossless forms." - Stopped by: four separate facts in one sentence, plus `RUNTIME.DECODE_FAILED` (an error code I have no context for — is it thrown? a rejected promise? what do I catch?) and `float8` (a Postgres type name used to describe a JavaScript value). I'd split this into a short list. - -5. **Line 334:** "passing a second argument to `include()` does **not** throw. JavaScript does not arity-check, so the extra argument is silently ignored and a plain, unrefined `$lookup` runs. This differs from calling a genuinely absent method, which throws `TypeError`." - Stopped by: this is a paragraph about JavaScript's calling convention, not about the ORM. I read it twice trying to find the instruction. The only fact I need: "On MongoDB `include()` takes no second argument; if you pass one it is ignored." - -6. **Line 478:** "On PostgreSQL, **native** enum columns sort in the enum's declaration order... A text-backed enum sorts by its stored value instead" - Stopped by: "native enum" vs "text-backed enum." The page never defines either, and both enums in the example schema carry `@@type("pg/text@1")`, so I have no example of the native kind and no way to tell which one I have. This directly changes my sort results and I cannot act on it. - -7. **Line 466 (note):** "columns stored as date/time strings or backed by `Temporal`. So `min('createdAt')` over a `DateTime` column type-checks and returns a `Temporal.Instant`." - Stopped by: `Temporal` appears with no introduction. Is that the TC39 `Temporal` global? Do I need a polyfill? What does `Temporal.Instant` support? This is a return type I have to write code against. - -8. **Line 466, last clause:** "those calls are a compile error, naming the operation that is unavailable for that input." - Stopped by: "naming the operation that is unavailable for that input" — I think it means the TypeScript error message mentions `SUM`, but I had to guess. - -9. **Line 588:** "`cursor()` is typed to require a preceding `orderBy()`. This is a **type-only** guard: at runtime there is no such check." - Stopped by: "type-only guard" is invented phrasing, and the whole point is an implementation detail. What I need: "Always call `orderBy()` before `cursor()`. Without it the cursor is ignored and you get every row." - -10. **Line 200:** "To attach domain methods to a model, register a custom `Collection` subclass when you build the client. That path uses the `orm(...)` factory instead of the client's built-in facet" - Stopped by: "domain methods," "facet," "the `orm(...)` factory" — three unexplained things and no code. "Facet" is used twice on the page (line 182 too) and never defined. - -## Words and phrases I had to guess - -- **"contract"** / **`contract.json`** / **`./contract.d`** — I guessed this is Prisma 8's replacement for the generated client, produced by something like `prisma generate`. The page never says. This is the single biggest gap: it appears in the first code block and is never explained or linked. -- **"facet"** (line 182, 200) — guessed: a namespace on the client object, i.e. `db.orm` vs presumably `db.something-else`. -- **"root names"** / **"`roots` map"** — guessed: the keys the client uses to expose each model. -- **"extension pack"** (line 198) — guessed: an npm package adding database-specific types. -- **"read method (#read-terminals)"** (line 222) — "terminals" guessed as "methods that end the chain and execute." -- **"reducer"** (line 341, in the `refineFn` type) — guessed: one of `count`/`sum`/`avg`/`min`/`max`. -- **"polymorphic (PostgreSQL) or discriminated (MongoDB) model"** (line 677) — guessed these both mean the `@@base` / `@@discriminator` thing in the schema. Neither is defined, and `@@base`, `@@discriminator`, `types { ... }`, `@@type("pg/text@1")`, and the `Uuid` type all appear in the example schema with no explanation or link. -- **"native"** vs **"text-backed"** enum — guessed text-backed means the `@@type("pg/text@1")` annotation, native means its absence. Not stated. -- **"multi-table inheritance (a base table plus a variant table)"** (line 682) — guessed, but then the sentence says it "affects some mutations" and points at two sections not on this page, so I still don't know what it affects. - -## Places I asked "so what do I actually type?" and got nothing - -- **Where do `contract.json` and `contract.d` come from?** No command, no link. I cannot run the very first example. -- **The pgvector example (line 198)** describes the option in prose — "pass the pack when you create the client," "needs `extensions: [pgvector]`" — but shows no code. Every other setup step gets a code block; this one doesn't, and it's the one with an unfamiliar import path. -- **What do I pass to `.eq()` for an enum?** Line 254 uses `u.kind.eq('admin')` (a member name of `user_type`) and line 271 uses `p.priority.eq('urgent')` (the *mapped value* of the `Urgent` member). Both enums are declared the same way. I cannot tell which form is correct or why they differ. -- **`variant('Bug')` and `variant('Tutorial')`** both use the capitalized PSL model name, even though every other MongoDB accessor on the page is lowercase plural. Is the variant name the model name, or the discriminator string (`"bug"`, `"tutorial"`)? The schema shows both, the page doesn't say which one `variant()` takes. -- **`combine()`** (line 394, 454): "Object of sub-views and aggregates." In the example, `posts.combine({ recent: posts.orderBy(...), total: posts.count() })` references `posts` from inside its own callback. I don't know whether that's required, whether the keys are free-form, or what shapes are legal values. -- **Every id in every example is undefined:** `aliceId`, `carolId`, `acmeId`, `postId`, `emptyCustomerId`. None is ever assigned. I can't copy any example and run it. -- **`select()` on MongoDB** — Remarks say the return type isn't narrowed. So what do I write to get a typed result? Cast it myself? Not said. -- **`where()` callback fields on PostgreSQL** — what operators exist besides `.eq()`? The page says "column-level operators" and defers to "[Filter conditions and operators]," a section not on this page. Same for `MongoFieldFilter` (`.eq`, `.gte` shown; the rest deferred). -- **`Customer` / `Order`** are used in three examples (lines 431–448) and defined nowhere in this file, only forward-referenced to "Grouped aggregates." -- **`countBigInt()`** is named in the Remarks (line 383) but has no row in the Options table and no example. -- **`PostTag`** exists in the example schema with explicit `postId`/`tagId`, while `Post.tags Tag[]` and `Tag.posts Post[]` look implicit. I can't tell whether the join model is required or vestigial, and it's never used in an example. - -## Places explaining how it works inside when I only wanted what to do - -- Line 334, the JavaScript arity paragraph. Pure internals. -- Lines 588 and 651, "type-only guard, not a runtime check." I only need "always call `orderBy()` first." -- Line 382, "comes from the aggregate map in your emitted contract, not from a fixed rule." Tells me about the code generator, not about what compiles. -- Line 332, "`include(relationName)` adds a `$lookup`." I don't need the Mongo aggregation stage name to load a relation. -- Line 682, "variants use multi-table inheritance (a base table plus a variant table)." Schema internals in a method reference. -- Line 383, `float8` and `RUNTIME.DECODE_FAILED`. The engine's type name and error code, where I wanted "use `sumBigInt()` for large sums." -- Line 297, "narrows the projected fields at runtime, but does not strip fields from the returned row type at compile time" — the fact is important, but it's phrased as a compiler/runtime split rather than "on MongoDB the result type still lists every field, even though only the selected ones have values." - -## Could I look up a method and use it correctly after one reading? - -For the plain chainers — `where`, `orderBy`, `limit`, `offset`, `distinct` — yes, once I already have a working client. The Remarks/Options/Return/Examples layout is easy to scan and the Postgres/Mongo tabs are genuinely useful. - -But I could not get to a working client at all, because the page opens with `contract.json` and `contract.d` and never says where they come from. That blocks everything. - -Beyond that, still unknown after one reading: -- What a contract is, and therefore what "root names," "the `roots` map," and "the aggregate map" mean — three of the page's load-carrying facts rest on a term it never defines. -- Whether my enums are native or text-backed, so I can't predict `orderBy` results. -- Whether `.eq()` takes an enum member name or its mapped string. -- Whether `variant()` takes the model name or the discriminator value. -- What operators exist on either database — both lists are deferred off-page. -- What `Temporal.Instant` is or whether I need a polyfill before using `min('createdAt')`. -- How to write a `combine()` shape from scratch, rather than modifying the one example. - -One more thing: the page promises at line 17 that "every example is transcribed from an executable test suite that runs against a live database," but no example is runnable as printed, because none of the id variables exist. That claim raised my expectations and then didn't hold. diff --git a/docs/orm-docs-audit/c21-orm-client/part1.round2.md b/docs/orm-docs-audit/c21-orm-client/part1.round2.md deleted file mode 100644 index 6abed9bfbad..00000000000 --- a/docs/orm-docs-audit/c21-orm-client/part1.round2.md +++ /dev/null @@ -1,103 +0,0 @@ -Read once, cold, as a Prisma ORM 7 user. Findings below. - ---- - -## Sentences I could not restate after one reading - -**Line 19 — "You write this schema in your contract, the `contract.prisma` file that replaced `schema.prisma`."** -Stopped by "contract". The sentence uses the word three ways in one line: a thing you write in, a file name, and later a `.json` and a `.d.ts`. I don't know whether "contract" is a file, a concept, or a directory. Plainer: "Write your schema in `contract.prisma`. This file replaces `schema.prisma` from ORM 7." - -**Line 19–20 — "run `npx prisma contract emit`. That command writes `contract.json` and `contract.d.ts`, the two files the examples below import."** -Writes them *where*? Next to `contract.prisma`? In my source folder? The examples import `'./contract.json'`, so apparently next to my code, but the page never says. - -**Line 187 — "`@@type("pg/text@1")` on an `enum` block stores the enum as a text column, with a check constraint that allows only the declared values."** -I cannot decode `pg/text@1`. Is `@1` a version? Are there other values (`pg/text@2`)? The MongoDB schema has `mongo/string@1`, which makes me think it's a registry of names I've never seen and the page doesn't list. - -**Line 188 — "A `native_enum` block is the other kind."** -"The other kind" of what — enum declaration syntax? And `native_enum` appears nowhere in the example schema, so I can't see what one looks like. This is the first of two places where the page names a thing and never shows it. - -**Line 190 — "`@@base(Task, "bug")` makes `Bug` a variant of `Task`: a model stored together with the model it is based on."** -Too many new ideas at once: `@@base`, "variant", "@@discriminator", and `variant('Bug')` all land in one bullet. I got roughly "subclassing", but I could not restate what is stored where until I read line 722, 500 lines later, which says a variant with `@@map` uses a second table. Those two facts belong together. - -**Line 367 — "the `_id` of a loaded related document comes back as a driver `ObjectId`, not as the hex string a document's own id field comes back as."** -Three clauses and two different `_id`s. Plainer: "A document's own `_id` comes back as a hex string. An `_id` on a related document loaded by `include()` comes back as an `ObjectId` object instead." - -**Line 416 — "They do not take a date or a timestamp: TypeScript rejects the field name, because the fields these two accept leave those columns out."** -The clause after "because" restates the clause before it and adds nothing I can use. Plainer: "TypeScript rejects a date or timestamp field name here." - -**Line 422 — "Each value must start from the collection the callback was given, which is why the example below writes `posts` inside its own callback."** -I could not restate this. "Start from the collection the callback was given" is abstract, and "writes `posts` inside its own callback" describes the example rather than the rule. Looking at the code, `posts.combine({ recent: posts.orderBy(...), total: posts.count() })` — `posts` referring to itself inside its own argument is genuinely confusing, and the prose made it worse, not better. - -**Line 504 — the whole `Temporal` note.** -One sentence carries: the return type, what `Temporal.Instant` is, a polyfill import, a global check, plus two methods that don't compile and why. Four separate facts. I stopped at "if your runtime has no global `Temporal`" — how do I know whether mine does? - -**Line 516 — "To sort a text-backed enum by meaning, store numbers rather than words, or write your own ranking expression."** -"Store numbers rather than words" — in the enum's `= "..."` values? Then `.eq('urgent')` becomes `.eq('3')`? The page told me on line 189 that the `=` string is what I pass to `.eq()`, so this advice silently breaks every other example. And "write your own ranking expression" is given no syntax at all. - -**Line 626 — "TypeScript rejects the call if you do not, but nothing checks at runtime. If you get past the type system, the cursor value is ignored and you get every row back, with no error."** -I understood the words but not why I am being told. I am not planning to get past the type system. The same construction appears three times (lines 368, 626, 690), and each time it reads as a note to the maintainers. - ---- - -## Words and phrases I had to guess at - -- **"collection"** — used constantly from line 246 on, never defined. I guessed "the chainable query object", but on MongoDB "collection" also means the actual Mongo collection (line 228, 723), so I was guessing which one was meant every time. -- **"contract"** — guessed: the new name for the schema plus its generated output. -- **"terminals"**, in the link `#read-terminals` — guessed: methods that end a chain and run the query. The visible text says "read method", so the anchor uses a word the page never does. -- **"`db.orm`"** — guessed that `orm` is a namespace on the client and that `db` has other namespaces. Never said. -- **"Refinements"** (line 408 heading) — guessed: the callback passed to `include()`. -- **"variant"** — guessed: subclass. Mostly right. -- **"text-backed enum"** / **"native enum"** — I worked these out from lines 187–188, but only because they are defined there. They are then used hundreds of lines later with no reminder. -- **"Project a row down to"** (line 325) — guessed "select fewer fields". -- **"`Uuid`"** — guessed it replaces `String @db.Uuid`. The page says it's "the type for a PostgreSQL `uuid` column", which does not tell me it's a new built-in scalar rather than something I import. -- **"`./contract.d`"** (lines 204, 232) — I assumed this is a typo for `./contract`. I have never imported from a path ending in `.d` and I would have spent time on this. -- **"`@prisma/orm-mongo/query-ast/execution`"** (line 312) — "query-ast/execution" reads like an internal module path that leaked into a public import. I would not trust it to be stable. - ---- - -## Places I asked "so what do I actually type?" - -1. **Installing anything.** The page imports `@prisma/orm-postgres/runtime`, `@prisma/orm-mongo/runtime`, and `@prisma/orm-extension-pgvector/runtime`, and runs `npx prisma contract emit`. There is no `npm install` line anywhere. As a v7 user I have `@prisma/client` installed; I don't know what to remove or add. -2. **Where `contract.prisma` lives and where the emitted files land.** Covered above. -3. **What happened to my v7 code.** I have two years of `prisma.user.findMany({ where: ... })`. The page never says `findMany` is now `.where(...).all()`, or that `PrismaClient` is now `postgres(...)`. One mapping table, or even one sentence, would have oriented me. Line 13 sends me to "Fundamentals guides" instead. -4. **Closing the client.** No `$disconnect` equivalent, no statement about whether `db` is a singleton I create once per process. -5. **A `native_enum` block.** Named on line 188, never shown. -6. **Any Postgres operator other than `.eq()`.** Line 267 says `where()` takes "a callback that calls an operator on a column". The only operator shown in this entire part is `.eq()`. Line 248 points at a section I don't have. -7. **Any MongoFieldFilter operator list.** I get `.eq` and `.gte`. Line 268 says "For anything else, such as greater-than, use `MongoFieldFilter`" — so what else is there? -8. **How the `PostTag` model connects.** Line 191 says "`PostTag` is required. A many-to-many needs the model for the join table, and a pair of list fields with no such model is rejected." But in the schema, `Post` has `tags Tag[]` and `Tag` has `posts Post[]`, and neither mentions `PostTag`. Nothing I can see links them. If I were writing my own many-to-many, I would not know what to type. -9. **`Customer` and `Order`.** Lines 466, 469, 474, 483 run examples against models the page explicitly says are shown somewhere else. I cannot run the `sum()`/`avg()` examples as written. Line 17 warns me, which does not help. -10. **What to do about MongoDB `select()`.** Line 331: "changes what the database returns but not the TypeScript type." So my type says `bio` exists and at runtime it does not. What am I supposed to do — cast? Pick fields by hand? The page states the problem and stops. -11. **`avgDecimal(field)` returns "the exact average ... as a decimal string."** Do I `Number(...)` it? Feed it to a decimal library? Which one? -12. **Custom `Collection` subclass** (line 224): "subclass `Collection` and register the subclass with the `orm(...)` function instead of calling `postgres(...)`." So I give up `postgres(...)` — do I still pass `url` and `contractJson`? Where does `Collection` come from? Deferred to a section I don't have, but the deferral is placed in the setup section where I'm actively trying to create a client. -13. **`distinct()` vs `distinctOn()`.** Both examples produce a value with no comment showing what came back. Every aggregate example has a `// result is ...` comment; these two don't, and they're the two I'd most need it for. - ---- - -## Places explaining internals when I only wanted to know what to do - -- Line 187: "with a check constraint that allows only the declared values." -- Line 188: "It creates a real PostgreSQL enum type with `CREATE TYPE ... AS ENUM`." -- Line 414, 625, 661, 689: "Calling one throws a `TypeError`." Told four times. I am not going to call a method that doesn't exist; telling me the JavaScript error class is for someone debugging the runtime. -- Line 416: "because the fields these two accept leave those columns out." -- Line 419: "Past ±(2^53 − 1) a `number` cannot hold the total." I know what a float is. The actionable half is "use `sumBigInt`". -- Lines 368, 626, 690: the three "if you get past the type system" notes. -- Line 722: "keeps its own fields in a second table alongside the base model's table." Storage layout, when the thing I wanted was "call `variant('Bug')`". -- Line 312: `query-ast/execution` in a public import path. - ---- - -## Could I do what the page is for, after one reading? - -Partly. I could chain `where`, `select`, `orderBy`, `limit`, `offset`, `distinct`, and `variant` on Postgres and produce a query that looks right, because the examples are concrete and the Postgres and MongoDB forms are clearly separated. The per-method structure (Remarks / Options / Return type / Examples) is easy to skim. - -What I would still not know: - -- **How to get to the point where any of this runs.** No install step, no file locations, no statement of what replaces my existing `PrismaClient` setup. I would be stuck before the first example. -- **Whether `./contract.d` is literally what I type.** -- **Any filter operator beyond `.eq()` on Postgres.** That is the single most common thing I do, and this part of the page cannot answer it. -- **How to declare a many-to-many**, because `PostTag` is asserted to be required and is not wired to anything visible. -- **What `combine()` actually requires.** I could copy the example. I could not write a different one. -- **What a `native_enum` block looks like**, despite the page using the native-vs-text distinction to explain sort order. -- **Whether "collection" means the query builder or the Mongo collection** at any given mention. - -The page assumes I already know what a "contract" and a "collection" are. It defines the schema attributes carefully on lines 184–192 and then never gives the same treatment to the two words it leans on hardest. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-client/part1.round3.md b/docs/orm-docs-audit/c21-orm-client/part1.round3.md deleted file mode 100644 index efef933071f..00000000000 --- a/docs/orm-docs-audit/c21-orm-client/part1.round3.md +++ /dev/null @@ -1,92 +0,0 @@ -I read `part1.mdx` once, top to bottom, as someone coming from Prisma ORM 7. - -## Sentences I could not restate after one reading - -**Line 17** — "Write this schema in `contract.prisma`, the file that replaces `schema.prisma` from Prisma ORM 7. This page calls that file your contract." -Stopped by: "contract" is a new word introduced by renaming, and the page never says what it *is* beyond "the new name for schema.prisma". Then `contract.json` and `contract.d.ts` appear, and I now have three "contract" things. Plainest wording I'd have understood: "Your schema now lives in `contract.prisma`. Running `npx prisma contract emit` compiles it into two generated files: `contract.json` (read at runtime) and `contract.d.ts` (read by TypeScript). You import both." - -**Line 185** — "The name inside is a PostgreSQL type plus a version, and the version is always `@1` today." -Stopped by: if the version is always `@1`, why am I typing it? The sentence tells me a fact about the tool's future, not what to do. I'd have written: "Always write `@@type(\"pg/text@1\")`. Other versions do not exist yet." - -**Line 186** — "A `native_enum` block stores the same members as a real PostgreSQL enum type instead." -Stopped by: "the same members" — the same as what? The example schema has no `native_enum` block, so I have nothing to compare against. And there is no `native_enum` code sample anywhere on the page. I would have needed three lines of actual `native_enum Priority { ... }` code. - -**Line 189** — "`Post.tags Tag[]` and `Tag.posts Post[]` are joined through `PostTag` because `PostTag` has a foreign key to each of the two models and its `@@id` lists exactly those two foreign key columns." -Stopped by: too many ideas in one sentence, and the mechanism is implicit. Nothing on `Post` or `Tag` mentions `PostTag`. So the rule is "the tool scans for a model shaped like this and wires it up silently." I had to read this three times. Plainer: "Write a third model with one foreign key to each side and an `@@id` of exactly those two columns. Prisma finds it automatically — you do not reference it from `Post` or `Tag`." - -**Line 194** — "The same `db` also has a query builder, `db.sql` on PostgreSQL and `db.query` on MongoDB, and `db.raw`, which this page does not cover." -Stopped by: I cannot tell whether "which this page does not cover" applies to `db.raw` alone or to all three. Split it into two sentences. - -**Line 242** — "On MongoDB the accessor name is also the name of the database collection the documents are in, which is why `db.orm.users` reads the way it does." -Stopped by: "reads the way it does" explains nothing. The paragraph has just told me "collection" means a query object, then immediately uses "collection" to mean a MongoDB collection. I finished the paragraph less sure what the word means than when I started. - -**Line 406** — "The heading calls that callback a refinement." -Stopped by: which heading? A sentence about the page's own headings is not a sentence about the tool. Say "This page calls that callback a refinement," or just call it one and move on. - -**Line 418** — "Build every value from the collection the callback was given, which is `posts` in the example below. A value that is `posts` with more methods chained on it comes back as an array of rows." -Stopped by: in the example, `combine` is called *on* `posts` and `posts` is also used *inside* the object. I could not work out whether the inner `posts` is the same object, a copy, or something rebuilt each time. The rule "a value that is X with more methods chained on it" is a description of syntax, not of what I should write. - -**Line 624** — "TypeScript rejects the call if you do not. Nothing checks this while the query runs, so a cursor on an unordered query is ignored and every row comes back with no error." -Stopped by: these two sentences contradict each other on first read. If TypeScript rejects it, how do I ever reach the runtime case? (I eventually guessed: plain JavaScript, or a `any` cast.) Say that. - -**Line 722** — "a variant that sets its own `@@map`, as `Bug` and `Feature` do, is stored in a second table beside the base model's table. A variant without `@@map` is stored in the base model's table." -Stopped by: too many ideas, and I cannot act on any of them here. It also forward-references two methods I do not have yet. - -## Words and phrases I had to guess - -- **"contract"** — guessed: the new name for the schema file, plus the generated files next to it. -- **"emit"** (`prisma contract emit`) — guessed: generate/compile. It is not a word I associate with running a schema step; `generate` was the ORM 7 word and the page never connects the two. -- **"variant"** — guessed: subclass / single-table inheritance. Line 188 defines it as "a model that reuses another model's fields and rows", which I only half-understood until the `Bug` example. -- **"discriminator"** — guessed from Java/ORM background. The page never says what value goes in the column; I guessed it is the string in `@@base(Task, "bug")`, i.e. `"bug"`. The page explicitly tells me *not* to pass that string to `variant()` but never says what it is for. -- **`@@base(Task, "bug")`** — guessed the second argument is the stored discriminator value. -- **"text-backed enum"** vs **"native enum"** — guessed: stored as a `text` column vs a real PostgreSQL `ENUM` type. The page does say this; my problem was that `Priority` is used as the example of both (line 186/514 describe `native_enum Priority`, but the schema declares `Priority` as a text-backed enum, and line 549 says so). Same name, two different declarations, no code for one of them. -- **`Expression`** (line 272) — guessed: whatever `u.email.eq(...)` returns. Not defined or linked. -- **`aggregate`** in `(relation: Collection) => Collection | aggregate` (line 371) — lowercase, so not a TypeScript type. Guessed it means "or one of the aggregate methods". -- **`@prisma/orm-mongo/query-ast/execution`** (line 244) — "query-ast" reads like an internal package path. Guessed AST = abstract syntax tree. I have no idea why a user-facing filter class lives behind that path, and it makes me think I am importing something private. -- **`types { Embedding1536 = pgvector.Vector(1536) }`** (line 24) — guessed: a type alias block. Never explained. Line 190 only tells me where `Embedding1536` comes from, not what the `types` block is or that I must write one. -- **"refinement"** — guessed: the callback passed to `include()`. -- **"which rounds"** (line 416) — guessed: returns an integer. To what precision? Unknown. - -## Places I asked "so what do I actually type?" and got no answer - -1. **Installing anything.** The page imports `@prisma/orm-postgres/runtime`, `@prisma/orm-mongo/runtime`, `@prisma/orm-extension-pgvector/runtime`, and `temporal-polyfill`. There is no `npm install` line anywhere. Line 11 says the migration page "lists the packages to install", but I am on the reference page with a code sample in front of me and I cannot run it. -2. **Getting from zero to `contract.prisma`.** Is there an `init` command? Does `src/prisma/` have to be created by hand? Line 17 says "A new project keeps all three files in `src/prisma/`" — is that a rule the tool enforces, a default I can change, or just a convention? -3. **Creating the tables.** `contract emit` writes `contract.json` and `contract.d.ts`. Nothing creates the `user` table. I came from `prisma migrate` and the page does not say what replaces it or link to it. -4. **tsconfig.** Two things in the first code sample need specific compiler settings and neither is mentioned: `import contractJson from './contract.json' with { type: 'json' }` (import attributes) and `from './contract.d'` (line 198 tells me to write it "exactly like that" but not what makes that resolve). If it needs `"module": "nodenext"` or `resolveJsonModule`, say so. -5. **Counting rows on a top-level query.** Line 411: `count`, `sum`, `avg`, `min`, `max` "are only callable **inside** an `include()` callback." So how do I count all users? This is the most basic thing I do every day in ORM 7 (`prisma.user.count()`) and the page tells me the method exists but I may not call it. Even a pointer ("see [Aggregates](#…)") would do. -6. **Getting correct types out of `select()` on MongoDB.** Line 328 tells me the type is wrong and to ignore it. It does not tell me what to write instead — a cast? `Pick<>`? Nothing. -7. **Writing a `native_enum`.** Described at line 186 and 514, recommended at 514 ("Declare the enum as a `native_enum` block when the sort order matters"), never shown. -8. **`interval` and `Time` columns** (line 412). Neither appears in the example schema and neither is a type I know how to declare. What do I write in `contract.prisma`? -9. **The `Customer` / `Order` schema.** Lines 462 and 465 run examples against models I cannot see, pointing at a section further down. Same for `acmeId` and `emptyCustomerId` at lines 252–253. -10. **Where the example rows come from.** Line 246: "these ids, which stand for rows inserted before the query runs." Inserted how? There is no seed snippet, so I cannot reproduce a single example on this page. -11. **Multi-column cursors.** Line 625 says the cursor object "must name every column the `orderBy()` sorts on", but the only example uses one column. With two columns, what does the object look like, and does it compare them as a tuple? -12. **`distinctOn` ordering.** Line 689 says call `orderBy()` first. In PostgreSQL, `DISTINCT ON` requires the leading `ORDER BY` columns to match the distinct columns. The example (line 708) obeys that, but the page never says it is a rule, so I would have written a broken query and not known why. -13. **`distinct()` with no `select()`.** Line 678 chains `select('priority').distinct('priority')`. Do I need the `select()`? The page does not say. -14. **The `Temporal` polyfill** (line 500). It shows `import 'temporal-polyfill/full/global'` but not the install command or where the import goes. -15. **Custom `Collection` subclass** (line 222): "create the client with `orm(...)` ... instead of `postgres(...)`". Does `orm()` take the same `{ contractJson, url }` object? Does it still connect to Postgres? Unanswered here. - -## Places that explain internals when I only wanted to know what to do - -- **Line 722** — variants with `@@map` go "in a second table beside the base model's table", without `@@map` in the base table. That is storage layout. What I want to know is which one to pick and what breaks. -- **Line 189** — the whole explanation of why `PostTag` is detected (foreign keys, `@@id` contents). I want the rule as an instruction, not as a description of the detection algorithm. -- **Line 185** — "The name inside is a PostgreSQL type plus a version, and the version is always `@1` today." Internal format of a string I am told to copy verbatim. -- **Line 502** — "TypeScript rejects `'createdAt'`, because PostgreSQL has no `SUM` or `AVG` over a date or a timestamp." The reason is fine, but it is the third sentence in a row about why the type system says no, when the actionable content is one sentence. -- **Line 244** — `@prisma/orm-mongo/query-ast/execution` exposes the tool's internal module layout in an import path I am told to type. -- **Line 328 and 363** — both are honest descriptions of bugs or leaks in the MongoDB layer (`select()` lies about types; related `_id` is an `ObjectId` instead of a string). I understand them, but both stop at describing the leak instead of telling me the workaround. Line 363 at least gives me `String(...)`; line 328 gives me nothing. - -## Could I do what the page is for, after one reading? - -Partly. I could write query chains: `where`, `select`, `orderBy`, `limit`, `offset`, `distinct`, `variant`. The method sections are consistent and the Remarks/Options/Return type/Examples shape is easy to scan. The PostgreSQL-vs-MongoDB differences are called out where they matter. - -I could not get to the point of running one. After one reading I still would not know: - -- What to install. No package names in a command anywhere on the page. -- How to create `contract.prisma` in a new project, or whether `src/prisma/` is required. -- How the tables get created. Nothing replaces `prisma migrate` here, and nothing links to it. -- What tsconfig settings the first code sample needs. -- How to count, sum, or average anything that is not inside an `include()` callback. -- How to write a `native_enum`, an `interval` column, or a `Time` column, all three of which the page tells me to use. -- What the second argument to `@@base("bug")` is for. -- How to reproduce any example, because the rows the examples query are never inserted anywhere on the page. - -The biggest single problem is the gap between line 17 ("Write this schema in `contract.prisma`... run `npx prisma contract emit`") and line 200 (a code sample importing three packages I have never installed). Everything between those two points is missing, and the page reads as if I had already done it. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-client/part1.round4.md b/docs/orm-docs-audit/c21-orm-client/part1.round4.md deleted file mode 100644 index 0ba97ba6411..00000000000 --- a/docs/orm-docs-audit/c21-orm-client/part1.round4.md +++ /dev/null @@ -1,81 +0,0 @@ -Read once, top to bottom, as a Prisma ORM 7 user who has never seen version 8. - -## Sentences I could not restate after one reading - -**Line 11** — "This page assumes a project where `prisma orm init` has already run and your tables exist: [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7) has the packages to install, `prisma orm init`, and `prisma db update`, which replaces `prisma migrate`, and [How migrations work](/orm/migrations/how-migrations-work) creates the tables." - -Too many ideas in one sentence, and the grammar breaks at the end. The last clause reads as if the *page* creates my tables. I also can't tell whether `prisma db update` is the thing that creates tables or something else. Plainer: "This page assumes your tables already exist. To install the packages and run `prisma orm init`, see Coming from Prisma ORM 7. `prisma db update` replaces `prisma migrate`; see How migrations work." - -**Line 15** — "Your schema now lives in `contract.prisma`, the contract, which replaces `schema.prisma` from Prisma ORM 7." - -The floating appositive "the contract" stopped me. Is "the contract" the file, the concept, or a product name? I read it three times. Plainer: "`contract.prisma` replaces `schema.prisma`. This page calls it the contract." - -**Line 229** — "You still create the client with `postgres(...)`, and `orm(...)` takes two things from that client." - -Which two things? The sentence deliberately withholds the answer and sends me to a section I don't have. This is the single most frustrating sentence on the page — it tells me a fact exists without telling me the fact. - -**Line 249** — "On this page, a collection is the query object you chain more methods on… On MongoDB, `users` in `db.orm.users` is also the name of the database collection." - -Two different meanings for "collection", one paragraph apart. After one reading I could not say which meaning applies when the page later says "`distinct()` does not exist on a MongoDB collection" — the query object, or the database collection? - -**Line 424** — "Call `posts.combine({ ... })`, and inside the object write `posts` again for each value. It is the same object every time." - -"It is the same object every time" is the sentence I could not restate at all. Same as what? I guessed it means the `posts` you write inside the object is the same collection the callback received, so reusing it does not accumulate earlier chaining. The page should say that literally. - -**Line 511** — "On PostgreSQL, a native enum column sorts in the order its members are declared, not alphabetically. The example schema declares `Priority` text-backed, so it sorts by its stored text." - -The bullet starts on native enums and switches to text-backed enums mid-bullet, so the first sentence looks like it describes the example and then does not. I had to reread to work out these are two different cases. - -**Line 622** — "The rows are compared as a pair, so you get the rows past the first column's value, plus the rows with that same first value and past the second." - -Dense. I think it means the cursor compares sort keys as a tuple, but I could not restate it cleanly on one reading. - -**Line 334** — "The type still lists every field on the model, so read only the fields you named from each returned document and treat the rest as absent." - -I understand the instruction. I do not know what actually happens if I read one anyway — `undefined`? a crash? The page tells me the rule but not the consequence. - -## Words and phrases I had to guess - -- **"the contract"** — guessed: the new name for the schema file and everything generated from it. -- **`prisma contract emit`** — guessed: the replacement for `prisma generate`. The page never says so, and that is the one comparison a version-7 user needs. -- **`@@type("pg/text@1")`** — guessed the `@1` is a format version I must not change, because the page says "Other versions do not exist yet." I do not know what a version of a storage format is. -- **`types { Embedding1536 = pgvector.Vector(1536) }`** — guessed `pgvector.` is a namespace from an installed extension package. Explained 175 lines later at line 197, long after I hit it. -- **"variant"** — guessed: single-table inheritance. The word is new and the explanation at line 195 arrives before "collection" is defined, so "`variant('Bug')` narrows a collection to that variant" was meaningless where I read it. -- **"refinement"** (line 412) — guessed: the callback. The page names it and then never uses the name again, so naming it served no purpose. -- **`Time` column** (line 418) — guessed: a time-of-day type. Not in the example schema, not defined anywhere on the page. I do not know how to declare one. -- **"native enum"** vs **"text-backed enum"** — guessed correctly, but the example schema uses the text-backed form while the advice at line 511 pushes me to the native form, so I could not tell which the page recommends. -- **`dbName: 'app'`** (line 240) — guessed: the MongoDB database name. Never stated. -- **`db.sql`, `db.query`, `db.raw`** (line 201) — guessed from the names. The page says "This page covers none of those three" and gives no link, so the mention is a dead end. -- **`all()` and `first()`** — used in every example from line 214 onward; the definition is in a section I do not have. I guessed "run it and return an array" and "run it and return one row or null". - -## Places I asked "so what do I actually type?" - -1. **Where do I run `prisma contract emit` from, and where does it write?** Line 15 says "A new project keeps all three files in `src/prisma/`" but never says whether `emit` puts them there or I configure a path. -2. **What is the real import path?** Every example is `from './contract.d'`. If my code is in `src/routes/`, do I write `'../prisma/contract.d'`? The page insists the path is "written exactly like that", which made me think the literal string matters. -3. **Which package do I install?** Imports come from `@prisma/orm-postgres/runtime`, `@prisma/orm-postgres/orm-client`, `@prisma/orm-mongo/runtime`, `@prisma/orm-mongo/query-ast/execution`, and `@prisma/orm-extension-pgvector/runtime`. I cannot tell how many npm packages that is. -4. **`orm(...)` at line 229** — I want the two lines of code and get a forward reference. -5. **The Temporal polyfill (line 497)** — "add a polyfill: `import 'temporal-polyfill/full/global'`". Do I `npm install temporal-polyfill` first? Not said. -6. **What type is `createdAt` in a result?** The cursor example passes `last.createdAt` straight back in, the MongoDB filter passes `new Date(...)`, and `min('createdAt')` returns a `Temporal.Instant`. Three shapes, no statement of which one a plain row field is. -7. **Options tables call the argument `filter` (line 278) and `filter / orderBy / limit / offset` (line 430)**, but the methods are `where()`, `orderBy()`, `limit()`, `offset()`. There is no method called `filter`. I do not know what to type. -8. **`distinct()` (line 657)** — "keeps one whole row per distinct `priority` value". Which row? The example then uses `select('priority')` anyway, contradicting the advice that I don't need it. -9. **`Customer` / `Order` (line 471)** — the sum and avg examples use models whose schema is not on this part of the page, so I cannot run them. -10. **MongoDB `variant()` (line 745)** — `db.orm.posts.variant('Tutorial')`, but MongoDB models are addressed by collection name everywhere else. Is `'Tutorial'` the model name even though `Post` is addressed as `posts`? The page says "Pass the variant's model name", which contradicts the collection-name rule at line 233 and left me unsure. - -## Places explaining internals when I wanted instructions - -- **Line 205** — "The import path `./contract.d` is written exactly like that, with no `.ts` on the end, and it resolves to the emitted `contract.d.ts`." I only needed the line of code. -- **Line 621** — "The library does not check this while the query runs, so code that skips TypeScript, such as plain JavaScript or a cast, gets no error." This is about the library's implementation. The instruction is "always call `orderBy()` before `cursor()`." -- **Line 688** — "PostgreSQL rejects a query whose `ORDER BY` does not begin with the `DISTINCT ON` columns. The library does not check this, so a mismatch fails in the database." I am told the generated SQL and where validation is missing. I wanted: "start the `orderBy()` with the same columns." -- **Line 721** — "a variant that sets its own `@@map`… gets its own table. That changes how some writes behave; see `createAndCount()` and `upsert()`." Storage layout plus two forward references, and no statement of what I should do. -- **Line 369** — the `_id` hex-string versus `ObjectId` split. Actionable in the end, but it is a description of a leaked implementation detail rather than a rule. -- **Line 499** — "PostgreSQL has no `SUM` or `AVG` over a date or a timestamp." Fine as a footnote, but it explains the database's limits when "these do not compile" already covered it. - -## Could I do what the page is for after one reading? - -Partly. I could write `where()`, `select()`, `orderBy()`, `limit()`, `offset()`, `distinct()`, and `variant()` calls by copying the examples, and I could tell PostgreSQL and MongoDB apart. Two things I could not do. - -**I could not get to the first line of working code.** The setup section assumes `contract.json` and `contract.d.ts` already sit next to my source file. I do not know which packages to install, where the emitted files land, what import path to write from my own directory, or whether `prisma contract emit` is the thing I run instead of `prisma generate`. For a version-7 user, that is the whole migration question and the page skips it. - -**I could not use `combine()`, or `orm(...)` for custom methods.** `combine()` because "It is the same object every time" never resolved. `orm(...)` because the page tells me it takes two things from the client and refuses to say which. - -Smaller gaps that would still stop me: what a `Time` column is, whether I should use `enum` or `native_enum` in a new schema, and what type a `DateTime` field actually holds when it comes back from a query. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-client/part2.round1.md b/docs/orm-docs-audit/c21-orm-client/part2.round1.md deleted file mode 100644 index e6c3c9c0dcb..00000000000 --- a/docs/orm-docs-audit/c21-orm-client/part2.round1.md +++ /dev/null @@ -1,101 +0,0 @@ -Read it once, top to bottom, as a Prisma 7 user looking something up. Report below. - -## Sentences I could not restate after one reading - -**1.** "On **PostgreSQL** only `update()` and `delete()` are typed to require a prior `where()`, and even there it is a **type-only** guard with no runtime check. If you bypass the type system, `update()` and `delete()` do not throw and do not mass-mutate; they narrow to a single row by identity (a `SELECT ... LIMIT 1` over the current, possibly empty, filters, then act on that one row)." - -What stopped me: four ideas jammed together, and "bypass the type system" is never explained (do I mean `as any`? plain JavaScript? a dynamic call?). "narrow to a single row by identity" is an invented phrase — identity of what? And "possibly empty filters" contradicts "narrow to a single row." Plainest wording I'd have understood: "On PostgreSQL, TypeScript will refuse to compile `update()` or `delete()` without `where()`, but nothing checks at runtime. If you call them from JavaScript with no filter, they pick one arbitrary row and change or delete that row." - -**2.** "**Switching consumption mode** on a consumed result throws `RUNTIME.ITERATOR_CONSUMED` (`already been consumed`)." - -"Consumption mode" is not defined anywhere on this part of the page. I *guess* it means: you awaited it, now you can't `for await` it, or vice versa. The page should just say that. Also two different strings are given — `RUNTIME.ITERATOR_CONSUMED` and `already been consumed` — and I don't know which one I'd see in a log or which one to match on. - -**3.** "On PostgreSQL, `create()` supports nested `create()` on a child-owned relation and nested `connect()` on a parent-owned relation, all within one transaction." - -"Child-owned relation" and "parent-owned relation" appear here for the first time with no definition. I tried to reverse-engineer them from the two examples: `User.create({ posts: ... })` is labeled child-owned, `Post.create({ user: ... })` is labeled parent-owned. So I *think* it means "which table holds the foreign key," but I'm guessing, and I can't tell from a schema I'm looking at which of my own relations is which. Plainest wording: "You can nest `create()` on a one-to-many side, and `connect()` on the side that holds the foreign key." - -**4.** "These nested mutators are type-checked but their runtime behavior on MongoDB is unverified on the current test contract." - -"Unverified on the current test contract" tells me about your test suite, not about my code. Does it work on MongoDB or not? Should I use it? The sentence leaves me with no decision. Say either "not supported on MongoDB" or "supported on MongoDB but untested — we recommend not relying on it." - -**5.** "On PostgreSQL, `update()` accepts a data object only. It has **no** field-operations callback overload: passing a function is silently a no-op (it resolves to `null`), not an error. A bare function has no enumerable own properties, so no column is targeted." - -The last sentence explains your implementation to justify a bug. I don't need to know about enumerable own properties. Also this contradicts the doc line above it: `update()` "Returns `null` when no row matches" — so if I get `null` back I cannot tell whether nothing matched or I passed the wrong shape. - -**6.** "On MongoDB, `updateAll()` is **not atomic**. It (1) reads the matching `_id`s, (2) runs an update against the original filter, then (3) re-reads by those captured `_id`s. A concurrent write between these steps could change which documents match or their values; the result reflects the `_id` set from step 1, not one atomic snapshot." - -I can follow the mechanics, but I cannot restate what it means for me. What is the bad outcome? Can a document be updated and not returned? Returned but not updated? Returned with someone else's newer values? And what should I do instead — a transaction? The page doesn't say. - -**7.** "The base and variant rows live in separate tables, so a single `RETURNING`-less `INSERT` can't populate both." - -Internals. I wanted to know "use `createAll()` instead", which the error message already says. - -**8.** "On insert, `update` fields apply via `$set` and the remaining create-only fields via `$setOnInsert`, so `update` values win over `create` values on any overlapping field." - -The `$set`/`$setOnInsert` detail is MongoDB-driver internals. The only part I needed is the clause after "so". Plainest wording: "If a field appears in both `create` and `update`, the `update` value is used, even on the insert path." - -**9.** "Registering custom collections requires building the client with the `orm(...)` factory, not the `postgres()` client's built-in `.orm` facet. The `postgres()` client has no `collections` option, so its facet always resolves models to the base `Collection`." - -"Facet" is undefined. "Resolves models to the base `Collection`" is implementation language. I think it means: "`db.orm` from `postgres()` can't use your subclasses. Build the client with `orm()` instead." That's all I needed. - -## Words and phrases I had to guess at - -- **"terminals"** — in the anchors `#read-terminals` and `#mutation-terminals`. Guessed: methods that end the chain and actually hit the database. The visible headings say "Read methods"/"Write methods", so the anchors leak a word the page otherwise avoided. -- **`AsyncIterableResult`** — guessed: a thing that is both a promise and an async iterator. The link goes to a section not in what I read, so I can't confirm. -- **`.toArray()`** — appears once, in a Remarks parenthetical for `all()`. Never listed in any return-type table, never shown in an example. Guessed it's an alternative to `await`. -- **"already-buffered"** — guessed: you already awaited it, so the rows are in memory. -- **`variant('Bug')`, `variant('Tutorial')`** — appears in six code samples and is never explained in this part. Guessed: filters to a subclass/subtype of the model. -- **"multi-table-inheritance variant" / "MTI"** — guessed: a subtype stored in its own table with a shared base table. The abbreviation MTI is used in the quoted error message without ever being spelled out next to it. -- **"discriminated variant"** — guessed: same idea, but stored as one document with a type field. -- **"materializing"** — in "Insert rows without materializing them". Guessed: without reading the inserted rows back. -- **"field-operations callback"** — guessed from the examples (`t.duration.inc(5)`). No list of available operations here; `inc`, `set`, `mul` appear in samples and that's all I know exists. -- **"conflict target"** — guessed: the unique index Postgres uses to decide insert vs update. -- **"contract"** — `Contract`, `contract.json`, `contract.d`, "test contract". Guessed: the generated schema types. -- **"facet"** — guessed: the `.orm` property on the client. -- **"atomic"** used three different ways: "atomic single-document update", "is a single `UPDATE ... RETURNING` statement, and is atomic", "not one atomic snapshot". I can't tell if these mean the same guarantee. - -## "So what do I actually type?" — places the page doesn't say - -1. **Do I have to supply `id` myself?** `Tag.create({ label: 'typescript-2' })` has no id. `User.create({ id: '00000000-...-099', ... })` has one. `Tag.upsert({ create: { id: '30000000-...', label } })` has one, and the ORM 7 migration diff quietly adds `id` to the new version (`create: { id, label: 'typescript' }`) with no note. I don't know when ids are generated for me. -2. **Do I have to pass every nullable field?** Every MongoDB create sample spells out `bio: null` and `address: null`. Can I omit them? The page never says. -3. **`conflictOn: { label: 'brand-new' }`** — why does the conflict target carry a *value*? In SQL, `ON CONFLICT` names columns. If it's just the column name, why not `conflictOn: ['label']`? If the value matters, what does it do? And does it have to match the `create` value? Both samples make it match, without saying whether that's required. -4. **`first()` on PostgreSQL with both `where()` and an inline filter** — do they AND together, or does the inline one replace the earlier filter? Never stated. -5. **PostgreSQL `upsert()` and `where()`** — the section warning says "Always call `where()` before ... or `upsert`", but the `upsert()` Remarks say "Requires a prior `where()` on **MongoDB**", and the PostgreSQL example calls `upsert()` with no `where()` at all. Three statements, and I can't tell what to type on PostgreSQL. -6. **Comparing a MongoDB `_id` after upsert** — "Compare it with `String(...)`." Show the line. Is it `String(user._id) === String(otherId)`? I'd like to copy it, not reconstruct it. -7. **What to do about non-atomic `updateAll()` on MongoDB.** No alternative is offered. -8. **How to get a count without writing.** The read methods are `all()`, `first()`, `aggregate()`, `groupBy()`. There's no `count()` or `exists()` in the list. Did they go away? For a `findMany`+`length` user, that's an unanswered question. -9. **`bugs()` returns what?** In the subclass example, `this.variant('Bug')` is returned and then `.all()` is called on it. The return type is never named, so I don't know what I can chain onto my own domain methods. -10. **`deleteAll()` returns the deleted rows as a stream.** If I `for await` over it, are rows deleted as I iterate or all up front? Matters if I abandon the loop halfway. -11. **The elided error strings** — `... Use createAll() instead.` and `upsert() is not supported for MTI variant "" ...`. If I want to catch and match these, the `...` makes that impossible. -12. **`orm({ runtime, context, collections }).public`** — the `.public` at the end appears without comment, and `client.context` is never explained. - -## Places the page explains internals when I only wanted instructions - -- `SELECT ... LIMIT 1` in the warning block. -- "PostgreSQL's `RETURNING`-backed `create()`". -- "A bare function has no enumerable own properties, so no column is targeted." -- The three-step read/update/re-read breakdown of MongoDB `updateAll()`. -- "$set" / "$setOnInsert" in `upsert()`. -- "backed by `findOneAndUpdate`" / "backed by `findOneAndDelete`" (harmless, but it's implementation, and it is where the atomicity claim hides). -- "a single `RETURNING`-less `INSERT` can't populate both". -- "its facet always resolves models to the base `Collection`". -- "The callback groups operators into one `findOneAndUpdate`". - -## Could I look up a method and use it correctly after one reading? - -For the simple reads, yes. `all()` and `first()` are clear, the ORM 7 diffs are the most useful thing on the page, and I could write those calls immediately. - -For the writes, no. Specifically, after one reading I still would not know: - -- Whether I need to pass `id` on create, ever. -- What `conflictOn` actually takes, or whether PostgreSQL `upsert()` needs `where()` — the page says both. -- What `variant()` is, despite it appearing in six samples I'd be copying. -- Whether nested `create`/`connect` works on MongoDB. -- What the full set of field-update operations is (I've seen `inc`, `set`, `mul`, and have no idea if that's all). -- Whether `null` from a PostgreSQL `update()` means "no match" or "you passed the wrong argument shape". -- What the MongoDB `updateAll()` non-atomicity costs me in practice, or what to do instead. -- Whether a count-only read exists. - -The single biggest problem is the warning block at the top of the write section. It is the most important paragraph on the page — it's about accidentally wiping a table — and it is the hardest one to read. It packs seven method names, two databases, a type-versus-runtime distinction, and a SQL implementation detail into one block, and I would have to read it three times to act on it. The one-line version I needed is: "Always call `where()` before an update or delete. On MongoDB you get an error if you forget. On PostgreSQL you do not, and `updateAll`/`deleteAll` will hit every row." - -Path read: `/private/tmp/claude-502/-Users-will-Projects-prisma-web--claude-worktrees-prisma-orm-8-docs-audit-5bdfd9/7636eb55-44f8-408f-b81e-481bd0140ea6/scratchpad/c21/oc/part2.mdx` diff --git a/docs/orm-docs-audit/c21-orm-client/part2.round2.md b/docs/orm-docs-audit/c21-orm-client/part2.round2.md deleted file mode 100644 index 0c782f52d80..00000000000 --- a/docs/orm-docs-audit/c21-orm-client/part2.round2.md +++ /dev/null @@ -1,87 +0,0 @@ -# Reader review: `wip/c21/r2/part2.mdx` - -I read it once, top to bottom, as a Prisma ORM 7 user. - -## Sentences I could not restate after one reading - -**1.** `"On MongoDB, count with the pipeline builder."` (line 5) -Stopped by: "the pipeline builder" is a proper noun I have never seen. It has no link and no definition anywhere on this part. The link that follows goes to a task page for both databases, so I cannot even tell whether I need an import. -Plainer: say what it is, or link the words "pipeline builder" itself. - -**2.** `"To register a subclass, build the client with the `orm(...)` function and pass a `collections` object. The client's own `.orm` cannot do this: `postgres(...)` has no `collections` option, so `db.orm` always uses the plain `Collection`."` (line 128) -Stopped by: three ideas in one bullet, and it depends on my knowing that the `db.orm` I have used on every previous example came out of `postgres(...)`. Part 1 never told me that. -Plainer: "`db.orm` cannot use a subclass. Build a second accessor with `orm({ runtime, context, collections })` and use that instead." - -**3.** `"Once the type check is bypassed, `update()` and `delete()` change one arbitrary row, and `updateAll()` and `deleteAll()` change every row."` (line 172) -Stopped by: "Once the type check is bypassed" — bypassed how? By me writing `as any`? By calling from JavaScript? The page never says, so I cannot tell whether this warning applies to me at all. "One arbitrary row" is frightening and unexplained: arbitrary by what rule? - -**4.** `"It collects the matching ids, updates, then reads those documents back. A document someone else changes in between can come back with their values, and a document that starts matching only after the first step is updated but not returned."` (line 424) -Stopped by: the last clause contradicts the first. If step one collects ids, a document that only starts matching *later* is not in that id list, so how does it get updated? Either the update runs by filter rather than by id, or the sentence is wrong. I could not resolve it. - -**5.** `"Only the column name is read: it names the unique constraint the database checks. The value is required by the type but is not used, so it does not have to match the value in `create`."` (line 603) -Stopped by: it tells me the value is ignored, then both examples pass a value that carefully matches. So what do I type? The text and the examples pull in opposite directions. - -**6.** `"Every matching row is deleted before the first row reaches you. Stopping the loop early still leaves nothing behind."` (line 540) -Stopped by: "leaves nothing behind" is ambiguous on first read — nothing deleted, or nothing remaining? I had to read it twice to land on "no matching rows survive". - -**7.** `"On MongoDB, the `_id` on the returned row is decoded the same way as on a read, so you get a hex string, not the driver's `ObjectId`."` (line 607) -Stopped by: "decoded" describes machinery. The fact I need is "`_id` comes back as a string". - -## Words and phrases I had to guess - -- **"shorthand object"** (lines 70, 79). Guessed: a plain object of field/value pairs meaning equality. Never defined. -- **"domain methods"** (line 123). Guessed: methods I write myself. -- **"multi-table inheritance variant, a variant stored in its own table"** (line 304). Guessed: some variants live in the base table and some get their own. Part 1 told me variants are "stored with its base model", so this is the opposite and nothing tells me how to declare or recognise which kind I have. "MTI" then appears only inside a quoted error message. -- **"field operations"** (lines 350, 361). Guessed from the example `p.content.set(...)` that these are per-field setter calls. -- **"Single consumption and mode switching"** (line 18). Guessed: awaiting versus `for await`. -- **`AsyncIterableResult`** (lines 16, 260). Guessed correctly from the bullet, but the type itself is never defined in this part. -- **`Collection`** (line 141). Guessed the second parameter is the model name. Two unexplained type parameters. -- **`@prisma/orm-postgres/runtime` versus `@prisma/orm-postgres/orm-client`** (lines 136-137). Guessed which exports come from which only by reading the import lines. - -## Places I asked "so what do I actually type?" - -1. **Counting on MongoDB** (line 5). No snippet, no import, no method name. -2. **Updating a variant-only field on MongoDB.** Line 351 says `duration` on `Tutorial` "is not in it" and that "[Field update operations] shows the workaround" — but never says what the workaround is. That is the one thing I came to this bullet for. -3. **Safe bulk update on MongoDB** (line 425): `"run the writes yourself in a MongoDB driver session"`. No code, and no hint how to get a driver session out of this ORM. The link is to a page that may or may not answer it. -4. **The `data` option name.** Every Options table says the argument is named `data` (lines 194, 266, 312, 361, 431, 465). Every example passes the fields directly, and line 246 says the migration "drops the `data` wrapper". On the tables alone I would have typed `create({ data: { ... } })` and got a type error. -5. **Do I pass `id` on PostgreSQL creates or not?** Line 184 says leave out fields with a default "including the id". Then lines 226, 230, 239, 630, 637 all pass hand-written UUIDs. I cannot tell whether the id has a default, whether passing it is allowed, or whether I am expected to generate UUIDs myself. -6. **`conflictOn` value.** Per point 5 above: any value? The matching value? I would have to test. -7. **Calling `where()` before `upsert()` on PostgreSQL** (line 602): `"where() is not used"`. Does it throw, or is it silently ignored? I do not know whether my existing chain is safe. -8. **The `select`/`include` chain before `.update()`** (lines 400-405). It appears with no comment. I cannot tell whether it shapes the returned row, restricts what is written, or is decoration I can delete. -9. **Which value wins on insert in an upsert.** Line 605 says the `update` value wins "even when the row is inserted" — but that bullet is labelled MongoDB, and the PostgreSQL example at line 628 comments `"the create side is used"`. Opposite behaviours, and Postgres's is never stated as a rule. -10. **Two different `orderBy` syntaxes** appear side by side at lines 45 and 51 (`(u) => u.email.asc()` versus `{ createdAt: 1 }`) with no note that they differ by database. -11. **Coming from ORM 7, what replaces `findUniqueOrThrow` / `findFirstOrThrow` / `count()` / `deleteMany` return shape?** Line 114 maps `findUnique` and `findFirst` onto `first()` but says nothing about the throwing variants, and does not say whether `first()` still enforces uniqueness. -12. **Which `db` am I holding?** Line 154 builds `const db = orm({...}).public` and then calls `db.Task.bugs()`. Everywhere else it is `db.orm.public.User`. Nothing says the two are the same kind of thing or whether I now keep two clients. - -## Places that explain the machinery when I only wanted the instruction - -- Line 128: `"The client's own `.orm` cannot do this: `postgres(...)` has no `collections` option"` — internal reason for a restriction. -- Line 158 comment: `"client.context carries the contract, which orm() needs to build collections."` — I only need to know I must pass it. -- Line 186: `"It does not read the stored document back."` -- Line 354: `"On MongoDB, `update()` changes one document in a single database operation."` Floating fact with no consequence stated here. -- Line 355 with line 352: `"it also returns `null` when the data you pass has no fields to change, which is what happens if you pass a function by mistake."` This is a defect explained twice, in mechanism terms. The instruction is one line: on PostgreSQL, pass an object. -- Line 424: the three-step description of `updateAll()`. -- Line 603: `"it names the unique constraint the database checks"`. -- Line 607: `"is decoded the same way as on a read"`. - -## Two things that read as errors - -- Line 606: the `upsert()` failure message says `"Use createAll() instead."` `createAll()` does not upsert. Either the message is wrong or the page is quoting it wrong. -- Line 172 quotes a MongoDB error that names `updateAll()` specifically, but the warning covers `update`, `delete`, `updateAll`, and `deleteAll`. I cannot tell whether the runtime check exists for all four. - -## Could I do what the page is for after one reading? - -Partly. - -I could confidently write: `all()`, `first()`, `create()`, `createAll()`, `createAndCount()`, `update()`, `delete()` and their `-All`/`-AndCount` forms on PostgreSQL, plus the plain MongoDB equivalents. The ORM 7 diff blocks are the most useful thing on the page and did most of that work. - -I would still not know: - -- How to count on MongoDB. -- How to update a field that only exists on a variant, on MongoDB. -- Whether my variant is the "multi-table inheritance" kind, so whether `createAndCount()` and `upsert()` work at all. -- Whether to pass `id` on PostgreSQL creates. -- What value to put in `conflictOn`, and which side wins on insert in a PostgreSQL upsert. -- How to register a `Collection` subclass without copying the example blind — specifically what `context` is, whether I now have two clients, and why my existing `db.orm` cannot be used. -- How to do a safe bulk update on MongoDB. -- What `AsyncIterableResult` is, beyond "await it or loop it". \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-client/part2.round3.md b/docs/orm-docs-audit/c21-orm-client/part2.round3.md deleted file mode 100644 index 09d481b2492..00000000000 --- a/docs/orm-docs-audit/c21-orm-client/part2.round3.md +++ /dev/null @@ -1,76 +0,0 @@ -Read once, top to bottom, as a Prisma ORM 7 user. - -## Sentences I could not restate after one reading - -**1. Line 131 — "The accessor `orm(...)` returns has the same methods as `db.orm.public`. You keep the `postgres(...)` client as well, for `connect()` and for the `context` you pass to `orm(...)`."** -What stopped me: four things I have never seen (`postgres(...)`, `connect()`, `client.context`, `runtime`) in two sentences, none of them defined. Also the first sentence's grammar ("The accessor `orm(...)` returns has") made me re-read it three times. -Plainer: "`orm({ runtime, context, collections })` returns an accessor with the same methods as `db.orm`. Keep the client you got from `postgres(...)`: you need `client.connect()` for the runtime and `client.context` for the `context` argument." - -**2. Line 130 — "`db.orm` cannot use your subclass. Build a second accessor by calling the `orm(...)` function with a `collections` object, and use that accessor for the models you subclassed."** -What stopped me: "second accessor" — second to what? And the whole rest of the page says `db.orm.public.User`, while this example makes `db` mean something different (`const db = orm({...}).public`, then `db.Task.bugs()`). Two different `db` variables on one page. I genuinely could not tell whether I now have one client or two, and whether my other queries still work. - -**3. Line 121 — "For the throwing forms, `findUniqueOrThrow` and `findFirstOrThrow` map onto `all().firstOrThrow()`, which throws an error whose `code` is `RUNTIME.NO_ROWS`."** -What stopped me: `firstOrThrow()` appears here once and nowhere else — no section, no example. And line 18 just told me a result "can be used one way only", so calling a method on `all()` looked like the thing I was warned not to do. Plainer: show the line — `await db.orm.public.User.where({ email }).all().firstOrThrow()` — and say that `firstOrThrow()` is a method on the result. - -**4. Line 419 — "On MongoDB, `updateAll()` is three database operations. First it collects the ids of the documents that match your filter. Then it updates every document that matches that filter, not only the ids it collected. Then it reads back the documents whose ids it collected."** -What stopped me: too many ideas at once, and the "not only the ids it collected" clause is a correction folded into the middle of a list. I needed line 420 to work out what line 419 meant. - -**5. Line 344 — "A field declared on a variant, such as `duration` on `Tutorial`, is a TypeScript error even though it runs correctly."** -What stopped me: "is a TypeScript error" for a *field* — I had to guess it means "putting that field in the update object makes the compiler complain, but the update works". I also could not tell if this applies to `updateAll()` and `updateAndCount()` too. - -**6. Line 347 — "On PostgreSQL it also returns `null` when what you pass has no fields to change."** -Could not resolve: does "no fields to change" mean an empty object `{}`, or an object whose values already match the row? Those are very different, and the difference decides whether a `null` return means "not found" or "nothing to do". - -**7. Line 596 — "`conflictOn` takes one unique column with a value... The value is required by the type and is then ignored, so pass any value of the right type."** -I followed it, but only after the code comments. My question afterwards: what do I do when the unique constraint is on two columns together? The page never says. - -**8. Line 602 — "That advice is the library's own. `createAll()` inserts rows and has no update half, so use it when the rows are new."** -What stopped me: the page is telling me its own error message gives bad advice, then does not tell me what to do instead. If I need upsert behaviour on a `Bug`, I finish this bullet knowing only that I cannot have it. - -## Words and phrases I had to guess - -- **"shorthand object"** (line 71, 79) — guessed: a plain `{ field: value }` object meaning equality, as opposed to the `(p) => p.x.eq(...)` callback. Never defined. -- **"terminals"** in the heading anchors `#read-terminals` and `#mutation-terminals` — guessed: internal jargon for "the method that ends the chain". The prose says "read method" and "write method", which is clear; the anchor uses a word the page never uses. -- **"child-owned relation" / "parent-owned relation"** (anchors on lines 226, 237) — guessed: which side stores the foreign key. The visible heading says it plainly; only the anchor uses the jargon. -- **"MTI variant"** (line 298) — the page does expand it, but only as "what the error message calls it". I guessed that a variant with its own `@@map` table is what "multi-table inheritance" means. -- **`@@map`** (line 297) — guessed: a schema attribute setting the table name. Not defined in this part. -- **"field operations" / "field-operations callback"** (line 343, 353) — guessed from the example `(p) => [p.content.set('Rewritten')]`: return an array of per-field set calls. The definition is promised at `#field-update-operations` and not given here. -- **`/runtime` and `/orm-client`** (line 140) — guessed: subpath imports of the package. -- **"`context`" and "`runtime`"** (line 155-162) — I could copy the code but I do not know what either is. -- **"variant stored in its own table"** — guessed it means a separate physical table, so a single INSERT cannot return a count. - -## Places I asked "so what do I actually type?" and got no answer - -1. **Counting on MongoDB** (line 5). "count with `db.query`, the pipeline builder. It is already on your client, so there is nothing extra to import." I still cannot write the line. Counting is the most ordinary thing I do; sending me to another page for it is the worst omission on the page. The PostgreSQL form is given inline — give the MongoDB one too. -2. **`firstOrThrow()`** (line 121). No example anywhere. -3. **The `@ts-expect-error` line** (line 344). "as shown under Field update operations" — I have to leave the page to see one comment-and-line snippet. -4. **Grouping MongoDB writes** (line 421). "share one `MongoClient` with the MongoDB driver and group the writes in a driver session" — no code. This is the fix for a correctness problem the page just told me I have. -5. **Building the client at all** (line 155-157). The subclass example is the only place `postgres(...)`, `connect()`, and `context` appear, and it presents them as things I already do. Every other example starts from a `db` that appeared from nowhere. -6. **Array vs object in nested relation calls.** `posts.create([{...}])` and `tag.disconnect([{...}])` take arrays; `user.connect({...})` takes an object. I guessed it follows to-many vs to-one. The page never says. -7. **Upserting a variant stored in its own table** (line 601-602). See above. -8. **Does `upsert()` count as one of the "six methods" that throw `ORM.WHERE_MISSING`?** The warning (line 175) says "all six methods" and is titled "Filter before you update or delete", but line 594 says MongoDB `upsert()` needs `where()` too. I cannot tell whether a missing `where()` on `upsert()` throws or silently upserts the wrong document. -9. **Omitting fields on PostgreSQL `create()`** (line 187). "leave out any field your contract gives a default, including the id" reads as an instruction. Must I omit them, or may I? If I pass an explicit id, does it error? - -## Places that explain the internals when I only wanted to know what to do - -- **Line 419** (the three MongoDB operations). The consequences on line 420 are what I need. The step-by-step mechanism is the implementation, and it is given first and at greater length. -- **Line 298** — telling me what the error message's wording is short for is describing the library's internal vocabulary. What I need is "you cannot use `createAndCount()` here; use `createAll()`", which line 296 already says. -- **Line 595-596, 613** — the fact that `conflictOn`'s value is ignored is a quirk I must know, but the page states it three separate times (bullet, table row, trailing sentence, plus two code comments). Once is enough. -- **Line 175** — "On PostgreSQL nothing checks at run time. TypeScript refuses to compile these methods without a `where()`, so the risk is code that escapes the type check, such as JavaScript or a cast to `any`." This is a description of how the safety check is implemented. What I need is: "on PostgreSQL you only get this protection from TypeScript." - -## Could I do what the page is for, after one reading? - -Mostly yes for plain CRUD. I could write `all()`, `first()`, `create()`, `createAll()`, `update()`, `delete()` and their `*All`/`*AndCount` forms on either database, and the ORM 7 diffs made the mapping from `findMany`/`findUnique`/`create`/`update`/`delete`/`upsert` easy. The warning about filtering before update or delete landed. The note that `deleteAll()` deletes everything before the first row reaches you is the single clearest sentence on the page. - -What I would still not know: - -- How to count rows on MongoDB. -- How to throw instead of getting `null`, beyond the name `firstOrThrow()`. -- How to construct the client at all — `postgres()`, `connect()`, `context`, `runtime` are used but never introduced, so the `Collection` subclass example is copy-paste only, and I would not know how to adapt it. -- Whether subclassing means I now have two accessors to keep straight, and what happens to my existing `db.orm.public.X` calls. -- What "field operations" are, or how to write the `@ts-expect-error` line I am told to write. -- How to upsert a variant that lives in its own table. -- Whether a missing `where()` on `upsert()` is checked. -- What `select()` and `include()` do beyond a code comment saying they "shape the row you get back". - -One concrete inconsistency worth fixing regardless of the reader question: line 601 quotes the `upsert()` error as saying `Use createAll() instead.` and line 602 concedes that advice does not fit. If the message is really that, say what the reader should do; if it is not, the quote is wrong. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-client/part2.round4.md b/docs/orm-docs-audit/c21-orm-client/part2.round4.md deleted file mode 100644 index bda8cd0f994..00000000000 --- a/docs/orm-docs-audit/c21-orm-client/part2.round4.md +++ /dev/null @@ -1,90 +0,0 @@ -Read once, straight through, as a Prisma ORM 7 user. Findings below. All line numbers are from `/Users/wmadden/Projects/prisma/web/.claude/worktrees/error-reference-follow-ups-81a8e7/wip/c21/r4/part2.mdx`. - -## Sentences I could not restate after one reading - -**Line 5–10, the MongoDB count example.** -> "On MongoDB, count with `db.query`, the [pipeline builder](...)" -> ``` -> const built = db.query.from('posts').count('total').build(); -> const [counted] = await (await db.runtime()).query(built); -> ``` -Three unexplained things in two lines. `db.query` is a builder, but `(await db.runtime()).query(...)` is a different `query` that runs things — same word, two meanings, no explanation. `db.runtime()` has never appeared; part 1 only showed `db` being made by `postgres(...)`/`mongo(...)`. And `await (await ...)` looks like a typo until I stare at it. Plainest version I'd have understood: "Counting on MongoDB takes two steps. Build the pipeline with `db.query`, then run it with the runtime: `const runtime = await db.runtime(); const [counted] = await runtime.query(built);`" - -**Line 118, the whole paragraph.** Five separate rules in one block: -> "`first()` returns `null` on no match and does not check that only one row matched. `findUniqueOrThrow` and `findFirstOrThrow` both map onto `all().firstOrThrow()`, as the last line above shows. `firstOrThrow()` is a method on the result that `all()` gives you, and calling it does not count as using the result a second time." -The middle clause fights what I read 90 lines earlier at line 25: "Each result can be used one way only." I stopped and re-read to work out whether `all().firstOrThrow()` was legal. Also: `all()` loads every row and then throws away all but the first? That's what it looks like, and the page never says whether it actually fetches everything. Split this into a bulleted list and say plainly whether `firstOrThrow()` fetches one row or all of them. - -**Line 127.** -> "`db.orm` cannot use your subclass. Call the `orm(...)` function with a `collections` object to get a second accessor with the same methods as `db.orm.public`. You now have two accessors, both of them work, and your existing `db.orm.public.X` calls are unaffected." -I could not tell what I am supposed to do going forward. Do I keep both accessors in my app forever? Do I replace every `db.orm.public.X` with `tasks.X`? The sentence reassures me nothing breaks but never tells me which one to use. - -**Line 172.** -> "On PostgreSQL you only get this protection from TypeScript." -I do not know what that means concretely. Does my code fail to compile without `where()`? Is it a red squiggle I can ignore? Is it only in strict mode? Say the literal thing: "On PostgreSQL, `updateAll()` without `where()` does not compile" — or whatever actually happens. - -**Line 295 and 597, the error messages.** -> `createAndCount() is not supported for MTI variant "Bug" on model "Task".` -"MTI" is never defined anywhere on this page. I guessed it means the multiple-table storage the surrounding prose describes, but the page uses "variant stored in its own table" in prose and "MTI variant" in the error, and never connects them. Worse, line 597 says `upsert()` is unsupported and the error tells me `Use createAll() instead.` — `createAll()` is not an upsert. Either the error text is wrong or the page copied it wrong. Line 598 then gives me the real answer (`first()` then `create()`/`update()`), which contradicts the quoted message directly above it. - -**Line 341.** -> "Putting a field declared on a variant in the update, such as `duration` on `Tutorial`, makes TypeScript complain even though the update runs correctly. Put `// @ts-expect-error` on the line directly above `.update(...)`." -Two ideas fighting: this is a known defect and the fix is to silence the compiler. I understood the words. What I could not work out is the consequence: `@ts-expect-error` itself becomes an error once the underlying problem is fixed, so my build breaks on upgrade. The page does not warn me. - -**Line 348.** -> "On PostgreSQL, `update()` takes an object of field values only. Passing a function changes nothing and returns `null`." -So a function argument fails silently and I get the same `null` I'd get from "no row matched"? I re-read this three times to be sure it says what it says. If that is real, say it as a warning, not a remark. - -**Lines 422–424.** -> "On MongoDB, `updateAll()` is not a single operation, and two things follow. A document that starts matching your filter only once the call is under way is updated but not returned to you. A document that someone else changes during the call comes back carrying their values, not yours." -I got there eventually, but it took two passes. "starts matching your filter only once the call is under way" is a lot of clause. Plainer: "If a document begins matching your filter while the call is running, it is updated but not included in the returned rows. If someone else changes a matched document while the call is running, you get their version back, not yours." - -**Line 591.** -> "Only the column name is used. The value is required by the type and is then ignored, so pass any value of the right type." -I understood each sentence and still did not believe it. The example writes `conflictOn: { label: 'ignored' }`. As a reader I would want one line saying why — is this a temporary limitation or the intended design? Right now it reads like a bug I am being told to live with. - -## Words and phrases I had to guess - -- **"read terminals" / "mutation terminals"** (anchors on lines 1 and 165). Guessed: the method that ends the chain. Never used in the visible text, so the anchor is the only place the word appears — a reader who lands on that URL gets a word the page never defines. -- **`AsyncIterableResult`** (lines 23, 36, 257, 269, 436, 538). Linked to `#asynciterableresult`, which is not in this part. Guessed: an object you can `await` or `for await`. The table on line 36 says as much, so I coped, but I never learned whether streaming actually keeps memory down or just looks different. -- **`Row`** (lines 36, 92, 202, 365). Guessed: the generated TypeScript type for that model. The page never says where it comes from or how I import it if I want to name it. -- **"contract type"** (line 129, `Collection`). Guessed: the type exported by the generated `contract.d.ts`. The import on line 140 says `from './contract.d'`, which looks wrong to me — I would normally write `'./contract'`. I would have to try it. -- **`select()` and `include()`** (lines 351, 400). Used in a full example with no definition anywhere I can see. Guessed from Prisma 7 habit: choose fields, choose relations. The callback form `include('tags', (tag) => tag.select(...).orderBy(...))` is new and unexplained. -- **`orderBy({ createdAt: 1 })`** (line 59). Guessed `1` means ascending. The PostgreSQL tab right above uses `u.email.asc()`, so the two tabs look like different products. Line 26 tells me they differ but the page never says what `1` and `-1` mean. -- **"to-many" / "to-one"** (line 196). Guessed: relation cardinality. Fine, but they appear once with no anchor. -- **"driver session"** (line 424). Guessed: MongoDB's own transaction session object. -- **`client.context`** (lines 128, 157). Guessed: some bundle of state the ORM needs. The page tells me where to get it but not what it is. - -## "So what do I actually type?" - -1. **Reading an error code.** The page names five codes — `RUNTIME.ITERATOR_CONSUMED`, `RUNTIME.NO_ROWS`, `ORM.WHERE_MISSING`, `ORM.OPERATION_UNSUPPORTED` — and never shows one line of code that catches one. Do I write `catch (e) { if (e.code === 'ORM.NO_ROWS') }`? Is there an error class to `instanceof`? Is `code` typed? Nothing. -2. **The MongoDB `updateAll()` workaround (line 424).** The whole point of the remark is that my writes are unsafe. The fix is a cross-reference and one sentence of prose. I have no code to copy. -3. **The MongoDB count (line 8).** I do not know where `db.runtime()` comes from or whether I should cache the runtime rather than awaiting it per query. -4. **Streaming vs awaiting on writes.** Line 257 says `createAll()` returns something streamable. If I `for await` over `createAll()`, have the rows already been inserted, like `deleteAll()` on line 528? The page answers this for deletes and not for creates. -5. **`firstOrThrow()` has no entry.** It has no heading, no signature, no options table, and no return-type row — just a mention inside a paragraph on line 118. Every other method here gets a full section. -6. **The custom `Collection` subclass.** I have `tasks` at line 155 and `db` from part 1. Do I now call `postgres(...)` twice, or is `client` on line 152 meant to be the same `client` part 1 showed? The example builds a whole new client, so I cannot tell whether I am replacing my setup or adding to it. -7. **`conflictOn` for a multi-column unique constraint (line 592).** The rule is stated; no example is shown. I would have to guess `conflictOn: { orgId: 0, email: '' }`. -8. **Prisma 7 mappings I did not get.** The page maps `findMany`, `findUnique`, `findFirst`, `create`, `update`, `delete`, `upsert`. It never maps `createMany`, `updateMany`, `deleteMany`, or `count`. Those are the four I use most after `findMany`, and `createAll` vs `createAndCount` vs `createMany`'s `skipDuplicates` is exactly the kind of thing I would search this page for. - -## Places that explain the inside when I wanted the outside - -- **Line 423:** "That happens because the update runs by filter, while the read-back runs by the ids collected before the update." This is the implementation. I wanted to know what to do about it, which is the next line, and that line only links elsewhere. -- **Line 294:** "A variant is stored in its own table when its `@@map` names a different table than its base model does. `Bug` ... maps to `bug` while `Task` maps to `task`." Useful, but it arrives as storage-layout trivia before I am told the practical rule. The practical rule — "if your variant has its own `@@map`, `createAndCount()` and `upsert()` do not work" — should come first. -- **Line 186:** "On MongoDB, `create()` returns the values you passed plus the `_id` the server assigned, not the stored document. On PostgreSQL the returned row is read from the database." I understand why this matters (database defaults won't show up on Mongo) but the page makes me infer that consequence myself. -- **Line 128:** "`orm(...)` takes two things from the client that `postgres(...)` created ... `client.connect()` gives you the `runtime` argument, and `client.context` is the `context` argument." This is wiring narration. The code example on lines 152–159 already shows it more clearly than the prose does. - -## Could I do what the page is for, after one reading? - -Partly. For the plain cases — yes. I could write `all()`, `first()`, `create()`, `update()`, `delete()`, and their `All`/`AndCount` variants on PostgreSQL, and translate my Prisma 7 `findMany`/`create`/`update`/`delete` calls with confidence. The diff blocks are the most useful thing on the page and I would have gone straight to them. - -What I would still not know: - -- How to catch and check any of the errors the page keeps naming. -- What `AsyncIterableResult` really costs me — whether `for await` streams from the database or just iterates an array it already fetched. -- Whether `all().firstOrThrow()` fetches one row or the whole table. I would avoid it for that reason alone. -- What `MTI` means, and whether the `upsert()` error message telling me to "Use createAll() instead" is a typo I should ignore. -- What `select()` and `include()` do, or how to write the `include` callback for anything other than the one example. -- How to translate `createMany`, `updateMany`, `deleteMany`, or `count` from my existing code. -- How to make MongoDB `updateAll()` safe. I would leave the page knowing my code has a race condition and not knowing how to fix it. -- Whether I should restructure my client setup to use a custom `Collection`, or leave it alone. - -The biggest single blocker is the error handling. The page mentions error codes eight times and never once shows me the `catch`. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-client/part3.round1.md b/docs/orm-docs-audit/c21-orm-client/part3.round1.md deleted file mode 100644 index 2020a74e98d..00000000000 --- a/docs/orm-docs-audit/c21-orm-client/part3.round1.md +++ /dev/null @@ -1,82 +0,0 @@ -Read once, top to bottom, as a Prisma 7 user looking something up. - -## Sentences I could not restate after one reading - -1. **"It is a Postgres-adapter-registered operation attached to any textual-trait field, not a core comparison method."** (line 219) - Stopped by "Postgres-adapter-registered operation", "textual-trait field", and "core comparison method" — three invented-sounding terms in one sentence, none defined anywhere on this part of the page. I cannot tell whether this changes anything I type. Plainest version I'd have understood: "`ilike()` is available on text fields when you use the PostgreSQL adapter." And then say whether that's the same condition as `like()` or a different one, because right now `like` is described as being "on text fields" and `ilike` on "textual-trait field" — two different phrases for what I have to assume is the same set. - -2. **"They build AST nodes directly and are independent of any model accessor."** (line 268) - "AST nodes" is compiler vocabulary I do not need to read a filter. "Independent of any model accessor" — I cannot resolve "model accessor"; the term is not introduced. What I think this is trying to tell me is: "You import them; they are not methods on a field." That's already said by "standalone functions imported from...", so the sentence adds nothing I can act on. - -3. **"A to-one relation is filtered from the child side with the same `some(...)` shape (the compiled SQL is a correlated subquery regardless of cardinality)."** (line 311) - Too many ideas, and the parenthesis is about SQL generation, not about me. "From the child side" took a second read — I think it means: when the model I'm querying has one related parent (Post → User), I still use `some()`. Plainest version: "Use `some()` for to-one relations too — `post.user.some(...)`." Also: the example that follows passes an **object** `some({ email: 'alice@example.com' })`, while every other `some()` example passes a **callback**. The page never says both forms work. That is the single thing I most wanted stated plainly. - -4. **"`every()` is vacuously true for a parent with zero related rows."** (line 310) - I know what "vacuously true" means, but the sentence makes me translate. Plainest: "A user with no posts matches `every()`." - -5. **"On PostgreSQL, `all()` takes no arguments and returns a constant-true predicate, useful wherever a predicate is structurally required."** (line 270) - "Structurally required" is doing a lot of work and I can't cash it out. Required where? The page gives no example of `all()` at all. Worse, `.all()` is used on nearly every line of this page as the method that runs the query and returns an array — so the same name means two unrelated things and the page doesn't acknowledge the collision. - -6. **"PostgreSQL and MongoDB use different filter surfaces."** (line 206) - "Filter surfaces" — guessed it means "different APIs for filtering". Say that. - -7. **"Filters build the predicate inside `where()` (and relation refinements)."** (line 206) - "Relation refinements" is not defined on this part of the page and I can't map it to anything below — the relation section calls them "relation filters". If they're the same thing, use one name. - -8. **"MongoDB. Runtime behavior is verified; the type surface does not declare embedded paths, so the object-shorthand form needs a cast."** (line 402) - "Runtime behavior is verified" reads like a note from your test suite to yourselves. "Type surface" — guessed it means the TypeScript types. Plainest: "This works at runtime, but the TypeScript types don't know about nested paths, so the object form needs a cast." - -9. **"each returning comparison methods (`eq`, `neq`, ...)"** (line 172) - "Returning comparison methods" — an object with those methods, I assume, not literally returning methods. The example makes it clear; the sentence doesn't. - -## Words/phrases I had to guess - -- **`db.orm.public.Order`** — guessed `public` is the Postgres schema name. Never stated in this part. -- **`db.orm.users`** (lowercase, no `public`) for MongoDB — guessed the Mongo path is `db.orm.` with no schema level, and that the casing difference (`users` vs `User`) is intentional. Pure guess. -- **"model accessor"**, **"field accessor"** — guessed both mean the `u` in `where((u) => ...)` / the `db.orm.public.User` object. They may not mean the same thing. -- **"textual-trait field"** — guessed "a string column". -- **`GroupedCollection`** — guessed it's a lazy builder, not a result, because you must call `.aggregate()`. -- **`h.fn(field).cmp(value)`** in the `having()` Options table — guessed `fn` and `cmp` are placeholders, not real names. If a reader copies that line it does not compile. -- **`MongoFieldFilter.of`** — listed as one of the eleven factories and then never mentioned again. I have no guess. What does it do and what do I pass it? -- **`agg`, `h`** callback params — fine, but nothing tells me their types if I want to extract a helper function. -- **"Each key must support equality"** (shorthand object) — guessed "don't put a relation or a JSON column in there". Which fields fail? Not said. - -## "So what do I actually type?" — unanswered - -- **`MongoFieldFilter.of(...)`** — named as existing, zero signature, zero example. -- **`MongoExistsExpr`** — "There is a separate `MongoExistsExpr` class for `$exists`" (line 360). No import path, no method names, no example. I now know it exists and still can't use it. -- **`all()` (the combinator)** — no example anywhere. No signature beyond "takes no arguments". -- **`groupBy()` with more than one field** — the Options row says `...fields` / "The grouping key(s)", every example passes one. Is it `groupBy('a', 'b')` or `groupBy(['a','b'])`? The spread notation implies the former, but after coming from v7's `by: ['customerId']` I wanted it shown once. -- **Filtering rows before grouping** — `aggregate()` has a `.where(...).aggregate(...)` example. `groupBy()` never does. Can I write `.where(...).groupBy(...).having(...).aggregate(...)`? Presumably, but the page doesn't show the full chain. -- **Ordering/limiting grouped results** — "top 5 customers by spend" is the reason I'd open this section. Nothing about `orderBy` or `limit` on a `GroupedCollection`. -- **`having()` referring to an aggregate alias** — in both examples the same `sum('amount')` is written twice, once in `having` and once in `aggregate`. Can I reference the alias instead? Not said, so I'd assume no and keep duplicating. -- **Counting a specific column** — `count()` "needs no field argument". In v7 I used `_count: { bio: true }` to count non-nulls. Can I? The page implies not but never says. -- **What to use on MongoDB instead of `aggregate()`/`groupBy()`** — "calling them throws `TypeError`" and that's the end. No "drop to the driver", no link. -- **Combining Mongo combinators** — can `MongoOrExpr.of([...])` contain another `MongoOrExpr`? Can I call `.and()` on a `MongoOrExpr`, or only on a `MongoFieldFilter`? The remark says `.and()`/`.not()` are "instance methods on any `MongoFieldFilter`", which reads like `MongoOrExpr` does *not* have them. An AND-of-ORs is the most ordinary filter there is and I can't tell how to write it. -- **`not()` on Postgres combined with relation filters** — `not(p.posts.some(...))` vs `none(...)`: both plausible, neither confirmed. -- **The Mongo dot-notation object form mixing keys** — `{ 'address.city': 'SF' } as unknown as Record`. If I cast the whole object, do my normal typed keys still get checked? Obviously not. So why would anyone use this form? The page shows it without saying "prefer `MongoFieldFilter.eq`" as a recommendation, even though its own remark implies exactly that. -- **`in()` with an empty array** — matches nothing, or throws? Unstated, and it's the classic bug. -- **`like()` escaping** — `%` and `_` in user input. Nothing. - -## Where the page explains internals when I wanted instructions - -- "They build **AST nodes** directly" (line 268). -- "It compiles to a SQL `HAVING` clause" (line 172) — harmless but not actionable. -- "compile to **correlated `EXISTS` / `NOT EXISTS` subqueries**" and "the compiled SQL is a correlated subquery **regardless of cardinality**" (lines 308, 311). The second one is pure implementation trivia inside a sentence that was supposed to tell me what to type. -- "It is a **Postgres-adapter-registered operation**" (line 219). -- "At runtime there is no such restriction: **the object key is used verbatim as the Mongo field path**" (line 403). The "verbatim" mechanism is fine as a one-liner, but it's explaining why the cast works rather than telling me which form to use. -- "**`MongoFieldFilter.ne` is `undefined`**" and "**Accessing `.or` is `undefined`** and calling it throws `TypeError`" (lines 358, 299). Telling me the property is `undefined` is describing your object's shape. "There is no `ne` — it's `neq`" is the whole message. -- "There is **no `$nor` combinator anywhere in the library**" (line 299) — repeated in the remark and the note box, and it's a statement about your codebase's contents. - -## Could I look up a method and use it correctly after one reading? - -Partly. The Postgres scalar comparisons and the flat `aggregate()` I could use immediately — the tables are good, the examples are complete and runnable, and the v7 diffs are the most useful thing on the page. - -What I still couldn't do: - -- Write any grouped query more complex than the two examples: no multi-field grouping, no filter-then-group, no sorting the groups, no reusing an alias in `having`. -- Write an AND-of-ORs on MongoDB. I don't know if `MongoOrExpr` has `.and()`. -- Use `MongoFieldFilter.of` or `MongoExistsExpr` at all, despite both being named. -- Use the combinator `all()`, and I'd probably misread `.all()` at the end of a chain as the same function the first time I hit it in someone else's code. -- Decide confidently whether `some()` takes a callback or an object, because the to-one example silently switches forms. -- Know what `public` and the lowercase Mongo collection names in `db.orm....` are. That may be in the omitted sections, but every code sample on this part depends on it. diff --git a/docs/orm-docs-audit/c21-orm-client/part3.round2.md b/docs/orm-docs-audit/c21-orm-client/part3.round2.md deleted file mode 100644 index 2dd578344f6..00000000000 --- a/docs/orm-docs-audit/c21-orm-client/part3.round2.md +++ /dev/null @@ -1,85 +0,0 @@ -Read once, top to bottom, as a Prisma ORM 7 user. - -## Sentences I could not restate after one reading - -**Line 55** — "It also gives you `countBigInt`, `sumBigInt`, and `avgDecimal`, which answer with a `bigint` or an exact decimal instead of a `number`." -Stopped by: "answer with" (I think you mean "return"), and "an exact decimal" — is that a JS `Decimal` class? Whose? Do I have to import it? Also three names mapped onto two return types, so I cannot tell which is which. Plainer: "`agg.countBigInt()` returns a `bigint`. `agg.sumBigInt('amount')` returns a `bigint`. `agg.avgDecimal('amount')` returns a `Decimal` (imported from X)." - -**Line 129** — "`orderBy(...)` on a grouped collection sorts the groups by a grouping field. You cannot sort them by an aggregate value here. For that, use the SQL query builder, described in [Advanced queries]." -Stopped by: "here" — here meaning in `orderBy`, or in grouped queries generally? And "the SQL query builder" is a thing I have never heard of; it is named as if I already know it. Plainer: "You can only sort groups by a field you grouped on. To sort by a computed total, drop down to raw SQL (link)." - -**Line 234** — "Filters build the condition inside `where()`, and inside relation filters." -Stopped by: this defines filters using the word "filters". It also implies two contexts without showing the second yet. - -**Line 237** — "MongoDB uses static factories on `MongoFieldFilter` imported from `@prisma/orm-mongo/query-ast/execution`, plus `.and()`/`.not()` instance methods and the standalone `MongoOrExpr.of(...)`." -Stopped by: too many ideas in one sentence (import path, static factories, instance methods, a third helper), plus "static factories" is library-author vocabulary. The import path `query-ast/execution` reads like an internal module I was not supposed to see. - -**Line 245** — "`eq`/`neq`/`in`/`notIn` on any field that can be compared for equality; `gt`/`lt`/`gte`/`lte` on any field that can be ordered" -Stopped by: I cannot resolve "can be compared for equality" or "can be ordered" to actual Prisma types. Is `Json` orderable? Is `Bytes` equality-comparable? I have to guess and find out at compile time. Plainer: give the list of types, or say "if the method is missing from autocomplete, the type does not support it." - -**Line 364** — "A to-one relation is filtered the same way, from either side: `p.user.some(...)`." -Stopped by: two things. `some` means "at least one of many" everywhere else on the page, so using it for a single related row is surprising and the sentence does not explain why. And "from either side" — either side of what? The relation? So `u.posts.some()` and `p.user.some()` are both fine, which I already assumed. This sentence is doing work I could not follow. - -**Line 197** — "Each of those returns an object with `eq`, `neq`, `gt`, `lt`, `gte`, and `lte`." -Stopped by: "returns an object with" describes the library's internal shape when I only need "then call a comparison on it: `h.sum('amount').gt(1000)`". - -## Words and phrases I had to guess - -- **"pipeline builder"** (line 3) — guessed: some other API that exposes MongoDB's native aggregation pipeline. The page never shows a single line of it, so this is a pure guess. -- **"static factories"** (line 237) — guessed: methods you call on the class itself, `MongoFieldFilter.eq(...)`, not on an instance. -- **"selector"** (line 53) — guessed: the callback that names which aggregates to compute. -- **"output alias"** (line 53) — guessed: the object key becomes the property name in the result. -- **`GroupedCollection`** (line 127) — guessed: an unresolved, chainable thing that is not a promise yet. The page never says it is not awaitable, so I guessed that too. -- **`Uuid`** (schema, lines 13, 23) — guessed: a new scalar type replacing `String @db.Uuid`. -- **"exact decimal"** (line 55) — guessed: arbitrary-precision decimal, not a float. -- **`MongoAndExpr`** (line 298) — named once, never constructed, never shown. Guessed it exists but I would not know how to make one. -- **"the SQL query builder"** (line 129) — guessed: raw-SQL escape hatch. -- **`.desc()`** (line 178) — guessed: descending sort. Never defined on this page. -- **"to-many relation"** (line 359) — I know this from ORM 7, but the page uses it before defining it. - -## Where I asked "so what do I actually type?" and got no answer - -1. **`countBigInt` / `sumBigInt` / `avgDecimal`** (line 55). No example, no signature. Does `countBigInt` take a field? Do I need to import a `Decimal` type to hold the result? Nothing. -2. **MongoDB aggregation.** Line 3 tells me `aggregate()` and `groupBy()` throw a `TypeError` and sends me away. The whole Mongo half of "how do I count my documents" is a link. One three-line example would have answered it. -3. **MongoDB comparison methods.** PostgreSQL gets a full table (lines 251–257): `gt`, `like`, `in`, `isNull`, all of it. MongoDB gets exactly one method, `MongoFieldFilter.eq`, used in every example. How do I write "amount greater than 100" on Mongo? How do I write "name in this list"? The page does not say the methods exist or do not exist. -4. **`having()` with more than one condition.** Every example has exactly one comparison. Can I AND two? Does `and(...)` from `@prisma/orm-postgres/orm-client` work inside `having`? No answer. -5. **`having((h) => h.count(...))` with a field.** `aggregate` has both `count()` and `count('quantity')`, but line 197 lists only `count()` for `having`. Is `h.count('quantity')` legal? I cannot tell whether the omission is deliberate. -6. **Chain order.** Line 130 says `limit`/`offset` require `orderBy` first. But the example at line 177 goes `groupBy → orderBy → limit → aggregate`, while line 168 goes `where → groupBy → having → aggregate`. Where does `having` go relative to `orderBy`? Where does `limit` go relative to `having`? I would guess and recompile. -7. **Is `and()` variadic?** Line 309 shows `and(a, b)`. I need `and(a, b, c)` constantly. The page shows two arguments and says nothing. -8. **Chaining three Mongo filters.** `.and(other)` takes one argument. For three conditions do I write `a.and(b).and(c)`? Probably, but the page never shows it. -9. **Awaiting a `GroupedCollection`.** Line 142 says "resolved via `aggregate()`". So is `await ...groupBy('customerId')` an error, or does it return the groups without aggregates? Not said. -10. **`p.embedding.isNull()`** (line 287). `embedding` is not in either schema on this page. I cannot see its type, so I cannot tell what `isNull` means for it. -11. **Grouping-field types in the result.** Line 153 says each row is `{ customerId, orderCount, totalAmount }`. Are the grouping fields typed, or is this `Record`? Matters immediately in TypeScript. - -## Where the page explains internals when I only wanted to know what to do - -- **Line 47** — "This is standard SQL: aggregating zero rows yields `NULL`. Their TypeScript return type carries `| null` for this reason." The rule ("`sum` can be `null`; handle it") is enough. The SQL-standard justification and the reasoning about why the type is what it is are backstory. -- **Line 359** — "they compile to SQL `EXISTS` subqueries." I do not need to know the generated SQL to filter users by their posts. -- **Lines 44, 125, 196** — "Calling `aggregate()` on a Mongo collection throws `TypeError`", repeated three times. Telling me the exception class is telling me how the library is built. "Not available on MongoDB" is the fact I need, and once is enough. -- **Line 237** — the import path `@prisma/orm-mongo/query-ast/execution`. "query-ast" is the library's internal architecture surfacing in something I have to type. -- **Line 197** — "returns an object with `eq`, `neq`, ..." as described above. -- **Lines 59–61, 138–142, 208–212** — the "Return type" tables. "Object of aggregate results" and "A grouped collection, resolved via `aggregate()`" restate the example rather than telling me anything I can act on. - -## Two structural problems - -**The `having()` example appears before `having()` is documented.** The heading at line 165 says "Filter rows, then group (PostgreSQL)", but the code at line 168 contains `.having((h) => h.count().gt(1))`. I hit an undefined method under a heading that does not mention it, and the explanation is 25 lines further down. Either drop `having` from that example or move the section. - -**The `all` name collision.** Line 297 flags it honestly: "This `all()` is not the `.all()` you call at the end of a chain to run the query." I still had to read line 320, `where(() => all()).all()`, twice. The note does not rescue the design; it documents that the design will confuse me. - -## Could I do what the page is for, after one reading? - -**On PostgreSQL, mostly yes.** I could write `aggregate`, `groupBy`, `having`, the scalar comparisons, `and`/`or`/`not`, and `some`/`every`/`none`. The examples are concrete and I can copy them. - -**On MongoDB, no.** I would be stuck as soon as I needed anything other than equality. The page gives me `MongoFieldFilter.eq` and nothing else, gives me no aggregation at all, and points me off the page for the part I actually came for. - -What I would still not know after one reading: - -- Any MongoDB comparison other than `eq`, and whether they exist. -- How to aggregate or group on MongoDB. -- How to call `countBigInt`, `sumBigInt`, or `avgDecimal`, or what type `avgDecimal` gives me. -- The legal order of `where`, `groupBy`, `having`, `orderBy`, `limit`, `aggregate` in a chain. -- Whether `and()` takes more than two conditions. -- Whether `having()` can hold more than one condition. -- Which field types support which comparison methods. -- How to sort groups by a computed total (I know I cannot do it here; I do not know how to do it at all). -- What `MongoAndExpr` is for or how to build one. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-client/part3.round3.md b/docs/orm-docs-audit/c21-orm-client/part3.round3.md deleted file mode 100644 index 748cdd351ca..00000000000 --- a/docs/orm-docs-audit/c21-orm-client/part3.round3.md +++ /dev/null @@ -1,75 +0,0 @@ -## Sentences I could not restate after one reading - -**Line 3** — "PostgreSQL supports aggregation over a result set, both flat (`aggregate()`) and grouped (`groupBy().aggregate()`), with `having()` to filter groups. Neither method exists on a MongoDB collection." - -"Neither method" stopped me. Three methods were just named — `aggregate()`, `groupBy()`, `having()` — so "neither" (which means two) does not resolve. Plainer: "None of these methods exist on a MongoDB collection." - -**Line 3** — "use the pipeline builder, which is this library's API for MongoDB's own aggregation pipeline." - -"this library" — which library? The page has named `@prisma/orm-postgres` and `@prisma/orm-mongo`; neither is "this library." Plainer: "use the pipeline builder, Prisma's wrapper around MongoDB's aggregation pipeline." - -**Line 55** — "It also gives you three exact variants, for numbers too large or too precise to hold in a JavaScript `number`." - -Too many ideas in one sentence and "exact variants" is an invented label. I had to read the next three sentences to work out that "exact" means "does not lose precision." Plainer: "Three more methods avoid JavaScript's `number` precision limits." - -**Line 55** — "You import nothing to use these." - -I could restate it, but I could not work out why it is here. Nothing earlier suggested I would need an import for a method on a callback argument. It answers a question the page never raised. - -**Line 243** — "Which methods a field has depends on its type, and your editor's autocomplete is the answer: if it does not offer a method on that field, the type does not support it." - -Three ideas in one sentence, and the middle one tells me the page is not going to answer. Plainer: "The methods available on a field depend on the field's type. Autocomplete shows which ones apply." - -**Line 364** — "It works from both sides of a relation, so `u.posts.some(...)` and `p.user.some(...)` are both valid." - -"works from both sides" is a slick phrase, not a description. I think it means "the same method name is used whether the relation is to-many or to-one," which the previous sentence already said. I could not tell whether it was making a new point. - -## Words and phrases I had to guess - -- **"flat (`aggregate()`)"** (line 3) — guessed: aggregating over the whole result set as one group, as opposed to per group. -- **"exact variants"** (line 55) — guessed: they return `bigint` or a string instead of `number`, so no precision is lost. -- **"resolves to"** (lines 45, 47) — guessed: the promise resolves, i.e. this is the value you get after `await`. -- **"calling a method on the `MongoFieldFilter` class itself"** (line 235) — guessed: a static method, called on the class, not on an instance. -- **"a type that can be compared for equality" / "a type that can be ordered"** (line 243) — guessed at the boundary, and I still do not know it. Is `Uuid` orderable? Is `Json` equality-comparable? Is `DateTime`? The page never lists the types. -- **`GroupedCollection`, `MongoAndExpr`, `MongoOrExpr`** — guessed these are class names I mostly do not touch, except `MongoOrExpr` and `MongoAndExpr`, which I do call `.of()` on. -- **`@prisma/orm-mongo/query-ast/execution`** (line 326) — a path with "query-ast" and "execution" in it reads like an internal module. I guessed it really is the supported public import, because the page uses it in an example. - -## "So what do I actually type?" — unanswered - -1. **Sorting groups by an aggregate value.** Line 129: "To do that, write the query with the SQL query builder (`db.sql`)." This is the most common thing anyone wants from `groupBy` — top customers by total spend. The page names the escape hatch and gives no code, not even one line. -2. **The three exact methods have no example.** `countBigInt()`, `sumBigInt()`, `avgDecimal()` are described in prose at line 55 and appear in no code block, no Options table, and no Return type table. -3. **Aggregating a `BigInt` or `Decimal` column.** Line 47 only covers `sum('amount')` on `Int`. What does plain `sum()` do on a `Decimal` column — throw, or silently lose precision? If it throws, that is the one case where I need `avgDecimal`, and the page does not say so. -4. **Building `MongoAndExpr` directly.** Line 298 tells me `MongoAndExpr.of([a, b, c])` exists. The only Mongo import line on the page (line 326) imports `MongoFieldFilter` and `MongoOrExpr`. I do not know where `MongoAndExpr` comes from. -5. **The Mongo import path is not in the prose.** Line 234 tells me exactly what to import for PostgreSQL. The matching Mongo bullet at line 235 tells me none of it; I have to scroll to a code block on line 326 to find the module. -6. **Chaining two `.where()` calls.** The page tells me explicitly that repeated `.having(...)` is ANDed (line 197). It never says whether repeated `.where(...)` is. I would have to guess. -7. **Mixing the shorthand object and a callback in one `where()`.** Both forms are documented. Whether I can pass both, or combine them, is not stated. -8. **Is the callback form available on MongoDB?** Every Mongo example uses `MongoFieldFilter`. Line 235 implies the callback form is PostgreSQL-only, but never says it. -9. **`every()` and `none()` on a to-one relation.** Line 364 says `some()` works on to-one. It says nothing about the other two, so I do not know if `p.user.none(...)` compiles. -10. **The ORM 7 replacement for `is` / `isNot`.** I used `{ user: { is: {...} } }` for two years. The page gives me a migration diff for `aggregate()` and for `groupBy()`, so I expected one here. There is none, and `is`/`isNot` are never mentioned. I had to infer that `p.user.some({...})` is the replacement. -11. **`aggregate()` is itself the terminal call.** Everywhere else a query ends in `.all()` or `.first()`. Line 68 awaits `aggregate()` directly. The page never states that `aggregate()` runs the query; I worked it out from the `await` in the examples. - -## A direct contradiction - -Line 243: if a field's type cannot be compared for equality, it does not have `eq` — "if it does not offer a method on that field, the type does not support it." - -Line 392: "a field whose type cannot be compared for equality throws an error whose `code` is `ORM.FILTER_UNSUPPORTED`. Filter that field with a comparison method instead." - -If the type has no equality comparison method, which comparison method am I supposed to use instead? The advice on line 392 points at something line 243 says does not exist. One of these is wrong, or line 392 means something else and does not say what. - -## Internals where I only wanted instructions - -- Line 142, Return type table: "`GroupedCollection`". I cannot import it, name it, or type a variable with it from anything the page shows. The useful part is the two lines of prose next to it ("Not a promise. Call `.aggregate(...)`"). -- Line 298: "The result is a `MongoAndExpr`" — relevant only because `.of()` is then mentioned, but it is introduced as a fact about the return type rather than as something to do. -- Line 350: "There is no `$nor` combinator in the library." `$nor` is MongoDB's own operator name. This tells me about the library's coverage; it does not tell me what to write if I wanted NOR. - -## Could I do what the page is for, after one reading? - -For the common PostgreSQL cases, yes. I could write `aggregate()`, `groupBy(...).aggregate(...)`, `having(...)`, scalar comparisons, `and`/`or`/`not`, and relation filters from the examples without going anywhere else. The "For Prisma ORM 7 users" diffs on lines 114 and 184 did more for me than anything else on the page. - -What I would still not know: - -- How to sort groups by a computed total — the single query I would have written first. -- Which field types support which comparison methods, so I would find out by hitting a compile error or a runtime `ORM.FILTER_UNSUPPORTED`. -- What to do with a `Decimal` or `BigInt` column, or when the three exact methods are required rather than optional. -- How to write anything on MongoDB beyond copying the four example lines. I have `MongoFieldFilter` (list is in a part I do not have), `MongoOrExpr.of`, `.and()`, `.not()`, one import path, and a named class I cannot import. -- Whether my ORM 7 `is` / `isNot` relation filters map onto `some()`, and whether stacked `where()` calls behave like the stacked `having()` calls the page does document. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-client/part3.round4.md b/docs/orm-docs-audit/c21-orm-client/part3.round4.md deleted file mode 100644 index 241ea5664c2..00000000000 --- a/docs/orm-docs-audit/c21-orm-client/part3.round4.md +++ /dev/null @@ -1,81 +0,0 @@ -Read once, top to bottom, as a Prisma ORM 7 user. - -## Sentences I could not restate after one reading - -**Line 3** — "PostgreSQL supports aggregation over a result set." -This reads as a fact about the database, which I already knew, so I could not work out what it was telling me. I think you mean "On PostgreSQL, a collection has aggregation methods." Plainest wording: "These methods exist only on PostgreSQL collections." - -**Line 48** — "`sum()` and `avg()` are offered on `Decimal` and `BigInt` columns. Over a `Decimal` column they return the exact value as a string." -"Offered" stopped me. The bullet above already showed `sum('amount')` on an `Int` column, so "offered on Decimal and BigInt" cannot mean "only on". And "they return the exact value as a string" — does that include `avg()`? If `avg()` on a `Decimal` already returns a string, why does `avgDecimal()` exist (line 56)? Three ideas in two sentences and they contradict the next paragraph. Plainest wording: state the return type per column type in a small table: column type → `sum()` returns → `avg()` returns. - -**Line 48** — "Over a `BigInt` column `sum()` returns a `number` and throws an error whose `code` is `RUNTIME.DECODE_FAILED` if the total is past `Number.MAX_SAFE_INTEGER`." -Too many ideas in one sentence, and it never says *when* it throws — at query time? on property access? Plainest wording: split into two sentences, and say the error comes from running the query. - -**Line 135** — "`groupBy(...)` returns a `GroupedCollection`, which you never import or name yourself." -I could not work out why I was being told this. It answers a question I did not ask. If the point is "don't try to type this type", say "You never write this type in your own code." - -**Line 137** — "To do that, write the query with the SQL query builder (`db.sql`)." -"SQL query builder" appears here for the first time with no definition, and the example below it (line 176) uses four things this page never explains: `db.sql.public.order`, `(f, fns)`, `.build()`, and `db.runtime().query(...)`. I could not restate what any of them do. - -**Line 227** — "Stacked `where()` calls are combined with AND, and you can use the shorthand object in one call and the callback in the next." -"Stacked" is a guess. Plainest wording: "If you call `where()` more than once, the conditions are ANDed. One call can take the object form and the next the callback form." - -**Line 238** — the whole bullet. -This is one paragraph carrying seven separate rules. The parts that stopped me: "need a type that can be compared for equality" and "need a type that can be ordered" are abstract categories the page never lists. I get two examples (`Uuid`, `DateTime` yes; `Json` no) and nothing else, so I cannot predict whether `Boolean`, `Decimal`, `Bytes`, or an enum is ordered. Plainest wording: a table of field type → available methods. - -**Line 392** — "Naming such a field throws an error whose `code` is `ORM.FILTER_UNSUPPORTED`." -"Naming such a field" made me stop — I think it means "putting that field as a key in the object". And "throws" tells me it is a runtime failure, but the `groupBy` section said a missing `orderBy` is a *TypeScript* error, so now I do not know which failures I catch at compile time and which blow up in production. Plainest wording: "Using that field as a key compiles, but fails at runtime with `ORM.FILTER_UNSUPPORTED`." - -**Line 356** — "There `some()` reads as 'the related row matches' and `none()` as 'it does not'." -I could restate this, but only after re-reading, because `some`/`every`/`none` are plural words and a to-one relation has one row. It also does not say what `every()` means on a to-one relation, which it just told me is available. - -## Words and phrases I had to guess - -- **"result set"** (lines 3, 40, 47) — guessed: the rows my filters match, not rows already fetched into memory. -- **"collection"** in "None of these methods exist on a MongoDB collection" — I was told "collection" means the query object, but MongoDB also calls its tables collections, and line 230 says "the accessor is the collection name". I guessed the query-object meaning here. -- **"offered on"** (line 48) — guessed: "also work on". -- **"pipeline builder"** (line 3) — guessed: a separate chaining API. I did not follow the link. -- **"selector"** and **"predicate"** as the Name column in the Options tables (lines 54, 207) — guessed these are just labels for the positional callback, not keys I type. The tables look exactly like options-object tables, so I first read them as `aggregate({ selector: ... })`. -- **"page the groups"** (line 138) — guessed: pagination. -- **"Column-level comparison methods"** (line 233) — guessed: comparisons on one field at a time. "Column-level" is not a term I know. -- **"to-many relation"** / **"to-one relation"** (lines 351, 356) — guessed from the examples; the page defines to-many but not to-one until it is already used. -- **`@prisma/orm-mongo/query-ast/execution`** — "query-ast" and "execution" look like internal package paths. I guessed I copy it verbatim and do not think about it. -- **`MongoAndExpr` / `MongoOrExpr` / `MongoFieldFilter`** — guessed "Expr" means expression. The page never expands it. -- **`h`, `agg`, `f`, `fns`, `o`, `u`, `p`, `t`, `group`** — guessed all of these are arbitrary parameter names I choose. - -## Places where I asked "so what do I actually type?" - -1. **`db.sql.public.order` (line 176) is lowercase, but `db.orm.public.Order` is capitalized.** The schema has `@@map("order")`. The page never says that `db.sql` uses the mapped table name while `db.orm` uses the model name. I would have typed `db.sql.public.Order` and got an error I could not explain. -2. **`db.runtime().query(topCustomers)` (line 182).** What is `runtime()`? Do I need a different import? Is this inside a transaction? This is the one escape hatch the page offers for a real need (sorting groups by an aggregate), and it is the least explained code on the page. -3. **`(f, fns)` in the `db.sql` example.** Two callback arguments appear with no word about what the second one is. -4. **`.build()` then `await db.runtime().query(...)`** — two steps, no explanation of why the query is not awaited directly like every other example on the page. -5. **`import { MongoAndExpr }`.** Line 293 tells me to use `MongoAndExpr.of([a, b, c])` but no example imports or uses it. I would have to infer the import line from the neighbouring one. -6. **`having()` placement.** Line 136 says `having()`, `orderBy()`, `limit()`, `offset()` can come in any order, and line 198 says `having()` comes "before `aggregate()`". Both are fine, but the only `having()` example has no `orderBy()`, so I do not know if `.orderBy(...).having(...)` compiles. -7. **Aggregates plus `orderBy`/`limit`.** Does `aggregate()` on a non-grouped collection honour a `limit()` earlier in the chain, or does it always aggregate everything the `where()` matched? The page never says. -8. **`count()` on a to-many relation.** Nothing here says how to count a group without `groupBy`, or how to aggregate related rows. I would have gone looking. -9. **The `Task` model in line 280** (`t.description.isNull()`) is not in either schema I was shown. Minor, but I could not check the field type against the claim. - -## Places explaining internals when I only wanted to know what to do - -- **Line 135**, `GroupedCollection` "which you never import or name yourself" — internal type naming. The useful half is "you cannot `await` it; call `.aggregate(...)`." -- **Line 150**, the Return type table saying `GroupedCollection` / "Not a promise." This repeats line 135 and still tells me about a type I never write. -- **Line 213**, the same table again for `having()`: "The same grouped collection with this condition added." That is a description of the builder's internal state, not of what I do. -- **Line 292**, "`.and(other)` and `.not()` are **methods on a condition you already built**" — the bolded contrast between "standalone functions" and "methods on a condition" is API-design commentary. What I need is the two code shapes side by side, which line 300 and line 318 already give. -- **Line 230**, "a filter you build by calling a static method on the `MongoFieldFilter` class". "Static method on the class" is implementation vocabulary. `MongoFieldFilter.eq('name', 'Alice')` shows me everything I need. - -## Could I do what the page is for, after one reading? - -Partly. - -I could write a simple `aggregate()`, a `groupBy().aggregate()`, a `having()`, and PostgreSQL `where()` filters with `and`/`or`/`not` and relation filters. Those sections have enough examples that I could copy and adapt. - -What I would still not know: - -- **Which comparison methods my field has.** For anything that is not `String`, `Int`, `Uuid`, `DateTime`, or `Json`, I would have to poke at autocomplete. The page tells me to rely on autocomplete instead of telling me the rule. -- **What `sum()` and `avg()` actually return on a `Decimal` column**, and therefore whether I need `avgDecimal()`. Lines 48 and 56 point in different directions. -- **How to sort groups by an aggregate.** This is a common thing to want, the page names it as the reason to drop to `db.sql`, and then the escape hatch is four unexplained API calls. I would have to go find the `db.sql` docs before I could write it. -- **Which mistakes the compiler catches.** `limit()` without `orderBy()` is called a TypeScript error; a `Json` key in a shorthand object is a runtime throw. I have no rule for predicting which is which. -- **How to aggregate on MongoDB.** Told to go elsewhere, which is fair, but it means half the page does not apply to a MongoDB reader and the split is only announced once, in line 3. -- **Whether `every()` works on a to-one relation**, and what it would mean. - -The largest single problem is the `db.sql` example at line 176. It is the answer to a real question the page raises itself, and it is the one block I could not type from. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-client/part4.round1.md b/docs/orm-docs-audit/c21-orm-client/part4.round1.md deleted file mode 100644 index 9292df72760..00000000000 --- a/docs/orm-docs-audit/c21-orm-client/part4.round1.md +++ /dev/null @@ -1,65 +0,0 @@ -Read once, top to bottom, as a Prisma 7 user who has never seen ORM 8. - -## Sentences I could not restate after one reading - -**1.** Line 20: *"Each operation targets a field and produces a Mongo update operator."* -Stopped by: this tells me what happens inside the library, not what I get back. Does `u.bio.set('x')` return something I have to do anything with? I think the answer is "no, just put it in the array" — but the sentence made me stop and wonder. Plainer: delete it, or say "each call returns an update instruction; collect them in the array you return from the callback." - -**2.** Line 32: *"The field accessor is typed against the collection's **base** model, even after `variant(...)`. Accessing a variant-only field (such as `Tutorial.duration`) inside a callback is a compile error, though it works correctly at runtime because the accessor is a `Proxy` that resolves any field name."* -Three things at once, and the conclusion is missing. It tells me my code will not compile and also that it works. So what do I actually do — `@ts-expect-error`? `as any`? Give up? The page immediately shows me the exact broken case as a worked example (`t.duration.inc(10)`, lines 47-55) without a single word saying "this example does not type-check." I would have copied it, hit a red squiggle, and assumed I'd made a mistake. Plainer: "TypeScript only knows the base model's fields, so `t.duration.inc(10)` is a type error even though it runs correctly. Work around it with X." - -**3.** Line 68: *"These are library gaps, not contract limitations."* -Stopped by "contract." I do not know what a contract is here. I cannot tell what the distinction buys me as a reader — either way the method does not exist. Plainer: delete the sentence. - -**4.** Line 78: *"they are **unverified** on the current test contract, which has no array (`many: true`) field to target."* -Stopped by "current test contract." This is a fact about Prisma's internal test suite, not about my code. It also makes "unverified" sound like a property of the API rather than a property of your testing. Plainer: "We have not tested these. Try them against a real array field before relying on them." - -**5.** Line 95: *"Awaiting a result and then iterating it with `for await` (or the reverse) is the failure mode."* -"is the failure mode" is a phrase I had to re-read. It means "that is the thing that throws." Say that. - -## Words and phrases I had to guess - -- **"field accessor"** — guessed: the `u` / `t` object handed to the `update()` callback, and `u.bio` is the per-field thing. The page never defines it before using it four times, and defines it only obliquely via the Proxy remark. -- **"variant('Tutorial')"** — guessed: some subtype/discriminator of the `posts` collection. Possibly explained in an omitted section, but line 32 leans on it heavily. -- **"contract"** / **"test contract"** — guessed: your internal test schema. Still not sure. -- **"dual interface"** (line 85) — guessed: "you can use it two ways." -- **"`RUNTIME.ITERATOR_CONSUMED`"** — guessed: an error code string on a thrown error object. I do not know what property it is on, or what class the error is. -- **"`many: true`"** (line 78) — guessed: the schema syntax for an array field. Appears here with no other mention. -- **"base model"** (line 32) — guessed: the parent of a variant. - -## "So what do I actually type?" — unanswered - -- **`updateAndCount()` and `upsert()` callbacks.** Line 20 says field operations work in all three. Every example is `update()`. `upsert()` in particular — where does the callback go relative to the create branch? Not shown. -- **Why the callback returns an array.** Every example returns a one-element array, `[u.bio.unset()]`. The page never says I can put several operations in it, never shows two, and never says whether order matters. I inferred the point of the array from its shape alone. -- **What `update()` returns.** Every example writes `const updated = await ...` and then never uses `updated`. Is it the row? A count? The example names imply a row; nothing says so. -- **The workaround for the variant type error** (see above). This is the single biggest gap on the page. -- **The import for MongoDB users.** Line 85 gives `@prisma/orm-postgres/components/runtime`; line 108 says the behavior is shared by both. If I am on MongoDB, do I import `AsyncIterableResult` from the Postgres package? From a Mongo package? Not said. -- **How to catch `RUNTIME.ITERATOR_CONSUMED`.** I get an error code and a message substring. I do not get: what to `catch`, whether there is a typed error class, or whether I am meant to match on `.code`. -- **Two different client shapes, unreconciled.** Field-op examples use `db.orm.users` and `db.orm.posts`. The result-type example uses `db.orm.public.User.all()`. Same page, different accessors, different casing, no word about why. I would not know which one to type. -- **`aggregate((agg) => ({ ... }))`.** Line 112-113 tells me the result shape but never shows one real aggregate expression, so I do not know how `total`, `orderCount`, `totalAmount` were produced. I cannot write the call from this section. -- **`push`, `pull`, `addToSet`, `pop`.** Listed twice as existing, never shown once. I do not know their arguments — does `pull` take a value or a filter? Does `pop` take `1`/`-1` like Mongo, or nothing? -- **Line 71: *"Do not use these operations; they are not supported."*** The preceding sentence is about `rename`. For a second I read "these" as the eight operations in the same sentence's first clause. - -## Places explaining the internals when I only wanted to know what to do - -- Line 32 and line 71: the `Proxy` explanation, twice. I do not care that it is a Proxy. I care that (a) the type error is yours not mine, and (b) a typo'd operation fails at call time with `TypeError` instead of at compile time. Line 71 does at least land on the consequence; line 32 does not. -- Line 20: *"produces a Mongo update operator."* -- Line 68: *"The library never generates `$min`, `$max`, `$rename`, or `$currentDate`."* — I already know they do not exist from the previous sentence. -- Line 78: *"which has no array (`many: true`) field to target"* — your test suite's shape. -- Line 94: *"it returns the cached array, with no re-query and no throw."* — "no re-query" is implementation. "Safe, returns the same array" is what I need. - -## Could I look up a method and use it correctly after one reading? - -For `set`, `unset`, `inc`, `mul` on a non-variant field: yes. Those four are clear, and the missing-field behavior for `inc` and `mul` is genuinely useful and well stated. - -For everything else, no. Specifically I would still not know: - -- What to type to update a variant-only field without a compile error — and the page's own example is the broken case, unmarked. -- Which client shape is real: `db.orm.users` or `db.orm.public.User`. -- The signatures of `push`, `pull`, `addToSet`, `pop`. -- How to call `aggregate()` at all — only what comes back. -- Where a MongoDB user imports `AsyncIterableResult` from. -- How to catch the consumed-iterator error in code. -- Whether I can return more than one operation from the callback. - -The `AsyncIterableResult` section is the strongest part of the page: the rule is stated, the safe case and the throwing case are both shown in one snippet, and the `count`/`sum`/`avg` null behavior on line 114 is exactly the kind of fact I would come to a reference for. diff --git a/docs/orm-docs-audit/c21-orm-client/part4.round2.md b/docs/orm-docs-audit/c21-orm-client/part4.round2.md deleted file mode 100644 index 611d51daf1c..00000000000 --- a/docs/orm-docs-audit/c21-orm-client/part4.round2.md +++ /dev/null @@ -1,98 +0,0 @@ -Read once, top to bottom, as a Prisma ORM 7 user. Findings below. - -## Sentences I could not restate after one reading - -**Line 3** — "Static factories that build MongoDB filter expressions." -Stopped by: "static factories" and "filter expressions." I know what a static method is, but I do not know what a "filter expression" is in this product, or why I would build one instead of passing a plain object to `.where()`. The page never says what problem these solve. Plainest wording I would have understood: "Helpers for filters you cannot write as a plain object, such as comparisons, `in`, and paths into embedded objects." - -**Line 7** — "Imported from `@prisma/orm-mongo/query-ast/execution`." -Stopped by: `query-ast`. That is your internal module layout. I have to type it, so I accept it, but nothing tells me what it means or whether there are other things in it I should know about. - -**Line 10** — "`isNull` / `isNotNull` are shorthand for equals-null and not-equals-null, not a `$exists` check." -Stopped by: this assumes I already know that in MongoDB a field set to `null` and a field that is absent are different things, and that equals-null matches both. The sentence tells me the two are not the same but not what the difference does to my results. Plainer: "`isNull` matches documents where the field is `null` and documents where the field is missing. To test only whether the field is present, use `MongoExistsExpr`." - -**Line 12** — "The factories `exists`, `regex`, `elemMatch`, `all`, and `size` do **not** exist on `MongoFieldFilter`." -Stopped by: it lists `exists` as missing one line after line 11 told me to use `MongoExistsExpr.exists`. On one reading those two lines fight each other. Also I do not know what `elemMatch` is, so "does not exist" tells me nothing I can act on. - -**Line 20** — "`gt` / `lt` / `gte` / `lte` `(field, value)` | Field + ordered value | Ordered comparisons." -Stopped by: "ordered value" and "ordered comparisons" say the same vague thing twice and neither says what types are allowed. The row is also mangled: the argument list sits outside the code span for three of the four names. Plainer: "A value that can be compared, such as a number, a date, or a string." - -**Line 65** — "`MongoWhereFilter` is typed against the model's own top-level field names. `{ 'address.city': ... }` is therefore a compile error without a cast." -Stopped by: `MongoWhereFilter` appears here for the first time and is never defined. I guessed it is the type of the plain object I pass to `.where()`. Two ideas in one sentence: what the type allows, and what breaks because of it. - -**Line 86** — the whole paragraph. Too many ideas in one block: -- "The callback receives one argument, written `u` or `t` below" — I stopped to work out whether `u` and `t` mean different things. They do not; they are just variable names in different examples. Saying it this way made me look for a distinction. -- "The argument is also a function, so `u('address.city').set('San Francisco')` reaches a field inside an embedded object." — one value being both an object with properties and a callable function is surprising. The page states it and moves on. I would have understood: "Call the argument as a function when the field is inside an embedded object: `u('address.city').set(...)`. Use the property form for top-level fields: `u.bio.set(...)`." -- "Each call returns an update instruction; collect them in the array you return." — "update instruction" is your internal name for the thing. As a reader I only need "return an array of the calls you made." - -**Line 88** — "`updateAll()` returns the changed documents." -Stopped by: line 172 later says `updateAll()` returns an `AsyncIterableResult`. On first reading these look like two different answers. Line 88 should say what the result type is, or say nothing. - -**Line 100** — "The callback argument is typed against the collection's base model, even after `variant(...)`. TypeScript only knows the base model's fields, so `t.duration.inc(10)` is a type error even though it runs correctly. Put `// @ts-expect-error` on that line to accept it." -Stopped by: this is an explanation of a limitation in your type generation, told from the inside. I only wanted to know what to type. Also "on that line" is ambiguous — the error is inside the callback but the example puts the comment above `.update(`. I could not have guessed that placement from the sentence. - -**Line 135** — "That callback cannot use a dot path, and `upsert()` rejects one." -Stopped by: "rejects" — at compile time or at runtime? If at runtime, what error do I catch? The page gives an error code elsewhere (`RUNTIME.ITERATOR_CONSUMED`) so I expected one here. - -**Line 156** — "`addToSet()` and `pop()` are covered only by unit tests that check the operator they build." -Stopped by: this is about your test suite, not about my code. I do not know what I am supposed to do with it. Am I being told not to trust them? - -**Line 159** — "One update cannot touch the same field twice, so the lines below are four separate writes." -Stopped by: I read the first half as a rule I need to obey, then the second half revealed it is an excuse for how the example is formatted. Plainer: "These four lines are separate calls because a single update cannot apply two operations to the same field." - -**Line 209** — "`sum()`, `avg()`, `min()`, and `max()` carry `| null`." -Stopped by: "carry `| null`" is shorthand for "their TypeScript type includes `null`." I worked it out, but it reads like notation, not a sentence. - -## Words and phrases I had to guess - -- **"static factories"** (line 3) — guessed: methods on the class you call without creating an instance. -- **"filter expressions"** (line 3) — guessed: the object the query builder actually sends to MongoDB. -- **"query-ast"** (line 7) — guessed: abstract syntax tree, your internal query representation. -- **"raw MongoDB operator string"** (line 9) — guessed: a literal like `'$regex'`, quoted, dollar sign included. The example confirmed it, the sentence did not. -- **"ordered value"** (line 20) — guessed: a number, date, or string. -- **`MongoWhereFilter`** (line 65) — guessed: the type of the plain object form of `.where()`. -- **"update instruction"** (line 86) — guessed: whatever `u.bio.set('x')` returns; an opaque value I just put in the array. -- **"the collection's base model"** (line 100) — guessed: the model named by `@@base`, from the earlier part of the page. -- **"the group key field or fields"** (line 208) — guessed: you can pass more than one field to `groupBy`, though nothing shows that. -- **"Options"** (line 14, heading) — guessed: this is a methods table, not options. The heading is wrong. - -## Places I asked "so what do I actually type?" and got no answer - -1. **How do I combine two filters?** This is the largest gap. Every example has exactly one condition. I cannot find out how to write "name is Alice AND createdAt after X" with `MongoFieldFilter`. Do I pass an array? Call `.where()` twice? Is there an `and`/`or` factory? The page does not say, and I need this in almost every real query. -2. **Can I mix a plain object and a `MongoFieldFilter` in the same `.where()`?** Never addressed. -3. **`MongoExistsExpr`** — line 11 names two methods. Does it have others? Does it combine with `MongoFieldFilter`? No `#### Options` table for it, unlike `MongoFieldFilter`. -4. **Regex options.** `of('title', '$regex', '^Hello')` is shown. How do I pass case-insensitivity (`$options: 'i'`)? The `of` signature takes one value, so I cannot see where it goes. -5. **`pull(match)`** — line 157 says it removes "every element matching a partial object." No example of the partial-object form. I do not know what to type. -6. **What happens if I omit `.where()`?** Line 88 says all four methods "require" it. Compile error or runtime throw? Not said. -7. **PostgreSQL increment.** Line 86 tells me PostgreSQL has no callback form and links to `update()`. It never says what I do instead. If I am on PostgreSQL, I have just read a long section that does not apply and got no replacement. -8. **Two casts.** Line 81 is `as unknown as Record`. Why two? A reader who tries the single cast first will fail and not know why. -9. **`@ts-expect-error` placement.** The example shows the comment above `.update(`, but line 100 says "on that line." Which line, when the call is chained across four lines? And this comment fails the build if you ever fix the type, which the page does not warn about. -10. **Catching the consumed-iterator error.** Line 197 does `error instanceof Error && 'code' in error && error.code === ...`. Is there an exported error class I could `instanceof` instead? That is the first thing I would ask, and the page does not say. -11. **Aggregate functions.** Line 207 uses `agg.count()` and `agg.sum('amount')`. There is no list of what `agg` offers; `avg`, `min`, `max` only appear incidentally in line 209. No signature for any of them. -12. **`groupBy` with more than one field.** Line 208 implies it is possible ("field or fields") but every example passes one string. -13. **Array ops in one call.** Line 159 says one update cannot touch the same field twice. Can I push to `tags` and set `bio` in one call? Presumably yes, but after that warning I would not risk it without an example. - -## Places that explain the internals when I only wanted to know what to do - -- **Line 8** — "Exactly eleven factories exist... There is no `ne`; use `neq`." The count is an inventory note for whoever audited the API. I only need the list and the correction. -- **Line 12** — reciting five factories that do not exist, three of which (`elemMatch`, `all`, `size`) I had not heard of. -- **Line 64** — "the TypeScript types do not know nested paths, so the object form needs a cast." This is why your types are limited. The instruction is the last clause: use `MongoFieldFilter.eq`. Lead with it. -- **Line 100** — an explanation of how `variant()` interacts with generated types. What I need is the one line about `@ts-expect-error`. -- **Line 156** — your unit-test coverage. -- **Line 158** — "MongoDB rejects the write with its own error, not a Prisma ORM error." Telling me whose error it is, without showing me the error, does not help me handle it. -- **Line 152** — the heading anchor is `[#array-operations-mongodb-unverified]`. The word "unverified" is an internal status note and it will show up in the page URL. A reader who lands on that link will wonder what is unverified. - -## Could I do what the page is for, after one reading? - -Partly. I could write a single-condition MongoDB filter (`MongoFieldFilter.eq`, `gt`, `in`, `isNull`) and I could write a single-field update (`set`, `inc`, `push`). Those parts have complete examples I can copy. - -What I still would not know: - -- How to write a filter with more than one condition. This alone blocks most real work. -- What `MongoFieldFilter` is for at all, versus just passing `.where({ name: 'Alice' })`. The page never compares them, so I do not know when to reach for it. -- How to do any of this on PostgreSQL. Roughly half the page is marked "MongoDB" and the PostgreSQL path is a link. -- What `agg` supports, and how to group by more than one field. -- Whether `addToSet` and `pop` are safe to use, because line 156 raised a doubt and did not resolve it. -- What to catch when `upsert()` "rejects" a dot path, or when MongoDB rejects an array operation on a non-array field. - -One more structural note: the table under `#### Options` is a list of methods, and the same heading pattern (`Remarks` / `Options` / `Examples`) is applied to sections that are not API members at all, such as "Dot-notation into an embedded object." Reading top to bottom, I expected `Options` to mean configuration and kept being wrong. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-client/part4.round3.md b/docs/orm-docs-audit/c21-orm-client/part4.round3.md deleted file mode 100644 index ac2ec5c9a91..00000000000 --- a/docs/orm-docs-audit/c21-orm-client/part4.round3.md +++ /dev/null @@ -1,86 +0,0 @@ -I read the file once, straight through, as a Prisma ORM 7 user. Findings below. - -# Sentences I could not restate after one reading - -**Line 9:** "`of(field, operator, value)` takes the MongoDB operator written out as a quoted string, such as `'$regex'`." -"Written out" stopped me. Written out as opposed to what? There is no other form of the operator shown anywhere, so the contrast is with something I have never seen. Plainer: "Pass the MongoDB operator as a string, including the `$`, for example `'$regex'`." - -**Line 21 (table):** "`isNull(field)` / `isNotNull(field)` | Field | Matches a `null` value and a missing field / neither of those." -"Neither of those" forced me to reconstruct the sentence twice. Plainer: "`isNull` matches a `null` value or a missing field. `isNotNull` matches everything else." - -**Line 19 (table):** "Field + a value that can be compared, such as a number, a date, or a string" -I could not tell whether this is a real type constraint I will hit, or just prose. If TypeScript rejects some types here, say which. If not, say "a number, date, or string". - -**Line 64:** "The plain object form, `.where({ 'address.city': 'San Francisco' })`, does the same thing when it runs, but it does not compile without a cast." -Two ideas jammed together — runtime behaviour and compile behaviour — before I know why either matters. Plainer: "You can also write `.where({ 'address.city': 'San Francisco' })`. It works, but TypeScript rejects it unless you add a cast, because `.where()` only accepts your model's top-level field names." - -**Line 85:** the whole opening paragraph of "Field update operations." -This is six instructions in one block: which methods take a callback, that the callback takes one argument, that the argument name is free, that top-level fields are properties, that embedded fields use a function call, that you return an array, and that PostgreSQL is different. On one reading I retained "callback, array, MongoDB only" and lost the rest. It needs to be a short paragraph plus a two-line list. - -**Line 87:** "All four methods require `.where()`." -I could not restate this because I do not believe I understood it. `upsert()` is one of the four. In Prisma 7 an upsert is keyed by a unique `where`, so maybe this is the same thing — but the page never says what `.where()` means for `upsert()`, and `upsert()` gets no example at all. - -**Line 98:** "If the type error ever goes away, TypeScript reports the unused comment instead." -I understood the words, but not why I am being told. It reads like a warning about a future breakage I cannot act on today. What do I do when that happens — delete the comment? Say that. - -**Line 147:** "The callback argument implements `set`, `unset`, `inc`, `mul`, `push`, `pull`, `addToSet`, and `pop` only." -This directly contradicts line 93, four paragraphs earlier: "`set()`, `unset()`, `inc()`, and `mul()` are available." On first reading I concluded the page disagreed with itself and had to scroll back to check. Line 93 needs to say "available" means "in this subsection", or list all eight. - -**Line 182:** "There is no error class to test with `instanceof`: import `isRuntimeError` from the same module and check `error.code`." -"The same module" is ambiguous. The previous paragraph named two modules (`@prisma/orm-postgres/components/runtime` and `@prisma/orm-mongo/components/runtime`). I had to look at the code sample to resolve it. Name the module in the sentence. - -**Line 206:** "[Grouped aggregates](#grouped-aggregates) documents `aggregate()`, `groupBy()` with more than one field, and every function `agg` gives you." -"Every function `agg` gives you" assumes I already know `agg` is the callback argument. Line 207 introduces it one line later. The order is backwards. - -# Words and phrases I had to guess - -- **"factories"** (lines 8, 11, 25). I guessed: static methods on `MongoFieldFilter` that build a filter object. The page never says what they return or whether I can hold one in a variable. -- **"It passes the operator string to MongoDB unchanged"** (line 9). I guessed: no validation, so a typo like `'$regexp'` fails at the database, not at compile time. Worth stating outright, because it changes how I debug. -- **"embedded value object"** (line 59). I guessed: a nested object stored inside the document, not a separate collection. A Prisma 7 user knows "embedded documents" from Mongo; "value object" is a different vocabulary and I was not sure it meant the same thing. -- **"the callback argument"** (lines 147, 154). I guessed this means the thing named `u` or `t`. It is called "one argument" at line 85, "the argument" at 85, "the callback argument" at 147. Three names for one thing. -- **"applied together, in one write"** (line 105). I guessed: atomic, one round trip to MongoDB. -- **"an error from MongoDB rather than a Prisma ORM error code"** (line 156). I guessed: the thrown thing has no `.code` I can match on, so `isRuntimeError` will not help. Not stated. -- **`pop(1)` / `pop(-1)`** (line 154). I guessed nothing — the page told me what each does. But I had to accept the numbers as magic; there is no hint they are MongoDB's own `$pop` values. - -# Places I asked "so what do I actually type?" - -1. **The function-call form for embedded fields in an update.** Line 85 says `u('address.city').set('San Francisco')`, and then no example in the entire section uses it. Every one of the eight code samples uses the property form. I wanted one sample showing a dotted update end to end. -2. **`upsert()`.** Line 133 gives a fragment inside prose: `upsert({ create: { ... }, update: (u) => [u.bio.unset()] })`. `create` is literally `{ ... }`. I do not know what goes in it, and there is no `.where()` on that fragment even though line 87 says all four methods require one. -3. **How to update an embedded field in an `upsert()`.** Line 133 tells me the dot path throws `ORM.OPERATION_UNSUPPORTED`. It never says what to do instead. That is the one place I would actually be stuck. -4. **What to catch when an array operation hits a non-array field.** Line 156 says the error comes "from MongoDB rather than a Prisma ORM error code". So what is in my `catch` block? No code, no error shape. -5. **`createAll()` and `deleteAll()`.** Line 170 lists them as returning `AsyncIterableResult`. Neither appears anywhere else in this part, so I do not know what they take. -6. **Does `updateAll()` exist on PostgreSQL?** Line 87 sits inside a MongoDB section but states `updateAll()` return behaviour as a general fact. Line 170 lists `updateAll()` with no database qualifier. I could not tell. -7. **`ORM.WHERE_MISSING` and `ORM.OPERATION_UNSUPPORTED`.** Both are named with no example of catching them, while `RUNTIME.ITERATOR_CONSUMED` gets a full `try/catch`. Inconsistent. -8. **Line 188:** `const result = db.orm.public.User.all();` — no `await`. Everywhere else in the file, `all()` is awaited inline. I could not tell if dropping `await` here is required for the example to work or just incidental. - -# Places explaining internals when I wanted instructions - -- **Line 64:** "The object you pass to `.where()` is typed against the model's own top-level field names, and a dotted key is not one of them." This is the type system's reasoning. I wanted: "use `MongoFieldFilter.eq()` for dotted paths." -- **Line 76:** "The cast goes through `unknown` because TypeScript refuses a direct cast between two types that do not overlap." A TypeScript lesson. The code sample already shows the cast; the explanation adds nothing I can act on. -- **Line 98:** "TypeScript still knows only the base model's fields, so `t.duration.inc(10)` is a type error even though it runs correctly." The "even though it runs correctly" part is the only bit I needed. The rest is how the type generator works. -- **Line 97:** "Applied to a missing field, MongoDB sets it to `0` regardless of the multiplier." Reasonable to state — but it is described as MongoDB's behaviour, which left me unsure whether Prisma might change it. - -# Other things that stopped me - -**Line 150:** the heading anchor is `[#array-operations-mongodb-unverified]`. The word "unverified" will be in the published URL and the copy-link button. As a reader I would take that as a signal the section is not trustworthy. This looks like an internal marker that escaped. - -**Line 144 and 147 say the same thing twice.** The Remarks bullet and the warning box both list `min`, `max`, `rename`, `currentDate`. The warning adds "Do not use `min()`, `max()`, `rename()`, or `currentDate()`" — a third repetition inside itself. - -**Line 157:** "The example schema has no array field, so these lines assume `tags String[]` on `User`." So the four code samples immediately below do not run against the schema the rest of the page uses. I would have copied them and got a failure. The note is above the code, which helps, but the samples should be marked. - -# Could I do what the page is for, after one reading? - -Partly. - -**I could:** write MongoDB filters with `MongoFieldFilter`, including `of()` for uncovered operators, and filter into an embedded object with a dot path. Update top-level fields with `set`, `unset`, `inc`, `mul`. Push and pull array elements. Consume an `AsyncIterableResult` both ways and know not to switch. - -**I would still not know:** - -- What to type for an `upsert()` — no complete example, and no answer for the embedded-field case it explicitly forbids. -- How to write the `u('address.city')` function-call form in real code, because no example uses it. -- Whether `updateAll()`, `createAll()`, and `deleteAll()` exist on PostgreSQL. -- Whether `.where()` on `upsert()` means the same thing it means on `update()`. -- What the full operation list is, because two sections give different lists without reconciling them. -- What to catch when an array operation fails on a non-array field. - -The filter half of this page is solid and I would ship code from it. The update half assumes I will read the opening paragraph three times, and it withholds the one example — `upsert()` — I would most likely need to look up. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-client/part4.round4.md b/docs/orm-docs-audit/c21-orm-client/part4.round4.md deleted file mode 100644 index 6978db99533..00000000000 --- a/docs/orm-docs-audit/c21-orm-client/part4.round4.md +++ /dev/null @@ -1,86 +0,0 @@ -# Reader review — `wip/c21/r4/part4.mdx` - -I have used Prisma ORM 7 for two years. Here is what happened on one read. - -## Sentences I could not restate after one reading - -**1.** "You can also write `.where({ 'address.city': 'San Francisco' })`. It works, but TypeScript rejects it unless you add a cast, because `.where()` only accepts your model's top-level field names." - -"It works, but TypeScript rejects it" reads as a contradiction until you work out that "works" means "at runtime". Plainer: "That object form runs correctly, but it does not type-check: `.where()` only knows your model's top-level field names, so you must cast." - -**2.** "After `variant(...)`, `t.duration.inc(10)` is a type error even though it runs correctly. Put `// @ts-expect-error` on the line directly above the `.update(...)` line, as the example below shows. If that type error ever goes away, TypeScript reports the comment as unused, and you delete the comment then." - -Three ideas in one bullet and the last sentence is backwards. I could not tell whether this is a known bug I should report, a permanent design, or something I did wrong. The example also contradicts the instruction: the comment sits above the `.update(...)` line in the example, but the prose in the second example block says "each line that touches it needs `// @ts-expect-error`" — those are different rules. - -**3.** The whole paragraph under the heading `##### unset() (MongoDB)`: - -"`updateAndCount()` takes the callback in the same place. `upsert()` takes it as its `update` key, beside `create`, and [`upsert()`](#upsert) shows a full call. That callback cannot use a dot path. Given one, `upsert()` throws an error whose `code` is `ORM.OPERATION_UNSUPPORTED`." - -Nothing here is about `unset()`. It is filed under the wrong heading. And "That callback" has two candidates in the preceding sentence (`updateAndCount`'s and `upsert`'s); I had to guess it means `upsert`'s only. - -**4.** "Applied to any other field, the write fails with an error raised by MongoDB, not by Prisma ORM. In a `catch` block, `isRuntimeError(error)` is `false` for it and there is no `error.code` to match, so read the message." - -`isRuntimeError` has not been introduced at this point in the page — it is first explained 25 lines later. And "read the message" tells me to read something the page never shows. Read it and match on what? - -**5.** "`pop(1)` removes the last element, and `pop(-1)` removes the first. Those are MongoDB's own `$pop` values." - -Fine until the second sentence, which is trivia about where the numbers come from. It does not tell me anything I can act on. - -**6.** "// all() returns the result object straight away and `await` is what runs the query, so this example holds it in a variable." - -I had to read this twice. It is the only place the page tells me `all()` is lazy — a fact that changes how I write code — and it is buried in a code comment. - -## Words and phrases I had to guess - -- **`updateAndCount()`** — never defined anywhere on this page. It appears first in the "Field update operations" intro as if I already know it. I guessed it is like `updateAll()` but returns a count instead of the rows. The page later half-confirms that, but never says whether it exists on PostgreSQL. -- **"Field + a value of any type"** in the `gt`/`lt` table row, versus plain "Field + value" for `eq`. I guessed the difference is meaningless. If it is meaningless, make both rows say the same thing. If it means "no type-checking against the field's declared type", say that. -- **"the callback's argument"** — used about ten times. I guessed it means the `u` / `t` object. Naming it once ("the update builder, `u` in these examples") and then using that name would have saved me. -- **`variant('Tutorial')`** vs the base `Post` — I could follow this from the prompt context, but the sentence "`duration` is declared on `Tutorial`, not on the base `Post` model" is the only place this page explains why a type error is expected, and it sits under one heading while the rule sits under another. -- **"mutation terminals"** — the link `[Write methods](#mutation-terminals)`. The visible text says "Write methods"; the URL says something else. I guessed they are the same thing. Internal vocabulary leaking into a URL. -- **`[#array-operations-mongodb-unverified]`** — the literal word "unverified" in the heading id. I guessed this section has not been tested and I should not trust it. If that is true, say it. If it is not true, this should never have shipped. -- **`_id: '6650f1c2a1b2c3d4e5f60002'`** — a plain string. In Prisma 7 I passed ObjectId-ish values. I guessed a hex string is accepted and converted. The page never says. - -## Places I asked "so what do I actually type?" - -**1. Combining two `MongoFieldFilter`s.** The opening paragraph says "To join two conditions with AND or OR, see Combinators." It never shows whether I can just pass two filters to `.where()`, or whether `.where(a).where(b)` chains as AND. That is the first thing I would try. - -**2. Atomic increment on PostgreSQL.** "PostgreSQL has no callback form: pass an object to `update()` instead." So how do I increment a counter on PostgreSQL without a read-modify-write race? Prisma 7 had `{ increment: 1 }`. The page does not say whether that still exists, or points me nowhere. - -**3. Array operations on PostgreSQL.** Same gap. `push`, `pull`, `addToSet`, `pop` are all marked MongoDB. Postgres has arrays. The page is silent. - -**4. The array examples do not run.** "The example schema has no array field, so these four lines assume `tags String[]` on `User` and will not run against the schema on this page." So I cannot copy anything. Add `tags String[]` to the example schema, or show the contract line beside the code. - -**5. The cast.** `as unknown as Record` throws away the typing of every other key in that object. The page does not warn me, and does not say whether this is the recommended route or the one I should avoid in favour of `MongoFieldFilter.eq`. - -**6. Reading the raw MongoDB error.** Told to "read the message", shown no message and no example `catch`. - -**7. Does `.where({})` satisfy the "all four methods require `.where()`" rule?** If I want to update every document, what do I type? The page says only that the absence of `.where()` throws `ORM.WHERE_MISSING`. - -**8. Reusing an `AsyncIterableResult`.** I know awaiting twice is safe and switching modes throws. I do not know whether looping twice is safe. That is the case I would actually hit. - -## Places explaining internals when I only wanted the instruction - -- "`of` does no checking of its own, so a misspelled operator such as `'$regexp'` fails at the database, not in TypeScript." The useful half is "typos are not caught until the query runs". Where the check does not happen is internals. -- "Those are MongoDB's own `$pop` values." -- "an error raised by MongoDB, not by Prisma ORM" — I care that `error.code` is absent, not whose code threw. -- "There is no error class to test with `instanceof`." Then just tell me what to do: import `isRuntimeError` and check `error.code`. -- "`sum()`, `avg()`, `min()`, and `max()` resolve to `null`, not `0`, over an empty result set, so their TypeScript type includes `null`." The trailing clause restates the first clause in compiler terms. - -## Formatting bug - -Line 105 ("These examples use the MongoDB accessors from…") is immediately followed by the `##### set() (MongoDB)` heading with no blank line between them. - -## Could I do what the page is for, after one read? - -Partly. - -**Yes:** I could write MongoDB filters with `eq`/`gt`/`in`/`isNull`, reach into an embedded object with a dot path, run `set`/`unset`/`inc`/`mul` on MongoDB, and read a `groupBy().aggregate()` result shape correctly. - -**Still would not know:** - -- How to do an atomic increment or an array push on PostgreSQL, or whether it is possible at all. -- Whether `updateAndCount()` exists outside MongoDB, and what it actually returns beyond "a number". -- How to AND or OR two `MongoFieldFilter`s without leaving the page. -- What the raw MongoDB error looks like when I `pull` on a non-array, so I cannot write the `catch`. -- Whether the `// @ts-expect-error` workaround is a bug I should expect to be fixed, and whether I need one comment per line or one per call. -- Whether the array-operations section is trustworthy, given the word "unverified" is in its own heading id and its examples admittedly do not run. \ No newline at end of file From f6f037fc9298d3a63dfb9961266ef969e2ccc851 Mon Sep 17 00:00:00 2001 From: willbot Date: Sun, 13 Sep 2026 07:31:36 +0200 Subject: [PATCH 10/11] docs(orm8): orm-client reference, carry the rc.10 model and result types section through the rewrite #8261 added "Model and result types" to the old page. Rebased onto main and rewrote that section against this page's schema and wording: the page's own contract import, the example User's fields, no SQLite, and the rc.10 note that a contract emitted earlier has no Models until you emit again. Facts unchanged from the rc.10 release notes and shape.ts. Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- .../content/docs/orm/reference/orm-client.mdx | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/apps/docs/content/docs/orm/reference/orm-client.mdx b/apps/docs/content/docs/orm/reference/orm-client.mdx index 6c8052e1abb..e8e3fc26037 100644 --- a/apps/docs/content/docs/orm/reference/orm-client.mdx +++ b/apps/docs/content/docs/orm/reference/orm-client.mdx @@ -2009,6 +2009,34 @@ await db.orm.users.where({ _id: '6650f1c2a1b2c3d4e5f60001' }).update((u) => [u.t ## Result types +### Model and result types + +`contract.d.ts` exports a type for every model, under `Models`. On PostgreSQL the schema is part of the name: `Models.public_User`. The same file exports a `models` constant, so `typeof models.public.User` is the same type. A model type lists every scalar field and every relation, and no query returns exactly that, so three types derive what a query does return: + +- `Scalars` is the model without its relations. It is what `all()` and `first()` return when you do not call `select()` or `include()`. +- `Shape` is a row with the fields and relations you choose. In `Spec`, `"+"` names the scalar fields and relations to keep, `"-"` names scalar fields to drop, and any other key is a relation with its own nested spec. If `"+"` names only relations, every scalar field stays. A relation named in `"+"` comes with all of its scalar fields and none of its own relations. +- `ResultType` is one row of a query you have already written. + +Import `Scalars` and `Shape` from `@prisma/orm-postgres/family-contract/types`, or `@prisma/orm-mongo/family-contract/types` on MongoDB. Import `ResultType` from `@prisma/orm-postgres/components/runtime`. A contract emitted before `8.0.0-rc.10` has no `Models`; run `npx prisma contract emit` again after upgrading. + +```typescript +import type { Models } from './contract.d'; +import type { Scalars, Shape } from '@prisma/orm-postgres/family-contract/types'; +import type { ResultType } from '@prisma/orm-postgres/components/runtime'; + +type UserRow = Scalars; +// every scalar field of User, no posts and no tasks + +type UserResponse = Shape; +// every scalar field of User except email, plus posts as an array of { id, title } + +const usersWithPosts = db.orm.public.User.include('posts'); +type UserWithPosts = ResultType; +// the same type as Shape +``` + +Use `Shape` to declare a function's return type once, and TypeScript checks the body at the `return`. A wrong field name, a relation under `"-"`, or a scalar field in `"+"` next to a `"-"` is a compile error on that key. For Prisma ORM 7 users, these replace `Prisma.User` and `Prisma.UserGetPayload<...>`. + ### `AsyncIterableResult` The methods `all()` / `createAll()` / `updateAll()` / `deleteAll()` return an `AsyncIterableResult`. You can use it two ways: From 1cfaeb7809ec26806d4fcd5889f8bc8d05b03a7f Mon Sep 17 00:00:00 2001 From: willbot Date: Sun, 13 Sep 2026 07:33:04 +0200 Subject: [PATCH 11/11] docs: point the SQL query builder page at the anchor the ORM client page has The link added in #8262 named #named-model-types; the heading's anchor is #model-and-result-types, so the docs link check fails on main. Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- apps/docs/content/docs/orm/reference/sql-query-builder.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d4db84881fd..f2a15bddde7 100644 --- a/apps/docs/content/docs/orm/reference/sql-query-builder.mdx +++ b/apps/docs/content/docs/orm/reference/sql-query-builder.mdx @@ -997,7 +997,7 @@ Recover a plan's row type at the type level. - Import `ResultType` from `@prisma/orm-postgres/components/runtime`. It's a family-agnostic utility, not specific to the SQL builder. - `ResultType` is the **per-row** shape, not an array. `await runtime.query(plan)` resolves to `Row[]`, but `ResultType` is `Row`. -- Since `8.0.0-rc.10` it also works on ORM queries: `ResultType`. See [Named model types](/orm/reference/orm-client#named-model-types). +- Since `8.0.0-rc.10` it also works on ORM queries: `ResultType`. See [Model and result types](/orm/reference/orm-client#model-and-result-types). #### Examples