From 938b7405932b4cbacf49e22dcbcf20ed141b2829 Mon Sep 17 00:00:00 2001 From: willbot Date: Sun, 13 Sep 2026 08:15:18 +0200 Subject: [PATCH 01/14] docs(orm8): C21 conventions for the orm/reference section, and the rc.10 relation-nullability rule The coming-from-7 page gains one row: since 8.0.0-rc.10, contract emit reports PSL_RELATION_NULLABILITY_MISMATCH when a relation field and its foreign-key columns disagree on optionality. Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- .../docs/orm/coming-from-prisma-orm-7.mdx | 1 + .../c21-orm-reference/conventions.md | 24 +++++++++++++++++++ .../c21-orm-reference/page-conventions.md | 12 ++++++++++ 3 files changed, 37 insertions(+) create mode 100644 docs/orm-docs-audit/c21-orm-reference/conventions.md create mode 100644 docs/orm-docs-audit/c21-orm-reference/page-conventions.md diff --git a/apps/docs/content/docs/orm/coming-from-prisma-orm-7.mdx b/apps/docs/content/docs/orm/coming-from-prisma-orm-7.mdx index 4b6afd5b8ae..b725d890162 100644 --- a/apps/docs/content/docs/orm/coming-from-prisma-orm-7.mdx +++ b/apps/docs/content/docs/orm/coming-from-prisma-orm-7.mdx @@ -56,6 +56,7 @@ The biggest visible change is in field types. Where Prisma ORM 7 wrote a Prisma | `@@schema("billing")` | `namespace billing { model ... }` | a `namespace` block is a PostgreSQL schema; wrap the models in it. They become `db.orm.billing.Invoice` and so on | | implicit many-to-many (`Post[]` on `Tag` and `Tag[]` on `Post`, with no model for the join table) | the same two list fields, plus a model for the join table | you write the join table as a model yourself. Example below | | `@id`, `@unique`, `@default`, `@relation`, `@map`, `@@map`, `@@index`, `@@id`, `@@unique` | unchanged | | +| a required relation field over an optional foreign key, or the reverse | rejected | since `8.0.0-rc.10`, `contract emit` reports `PSL_RELATION_NULLABILITY_MISMATCH`. Give the relation field and its `@relation(fields: [...])` columns the same `?`, or none | :::note[DateTime values and Node.js versions] diff --git a/docs/orm-docs-audit/c21-orm-reference/conventions.md b/docs/orm-docs-audit/c21-orm-reference/conventions.md new file mode 100644 index 00000000000..3cc56bc1c2f --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-reference/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-reference/page-conventions.md b/docs/orm-docs-audit/c21-orm-reference/page-conventions.md new file mode 100644 index 00000000000..651ca34beff --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-reference/page-conventions.md @@ -0,0 +1,12 @@ +# Conventions for the orm/reference pages other than orm-client (in addition to conventions.md) + +- These are reference pages. Keep the per-method layout (Remarks, Options, Return type, Examples, PostgreSQL/MongoDB tabs where present). No tutorial prose; a "how do I" question gets one sentence and a link to the fundamentals page, unless the answer is a fact about the method. +- The finished orm-client page (`apps/docs/content/docs/orm/reference/orm-client.mdx` on main) is the model for tone and structure. Reuse its wording for shared ideas: "collection" (the query object you chain on), "the built query" instead of "plan", "an error whose `code` is `X`", "does not exist on MongoDB" instead of "throws TypeError", "TypeScript rejects it, nothing checks at run time". +- Do not renumber, rename, or reorder headings. If a heading must change because it contains a banned term, pin the old anchor with `[#old-anchor]`. +- Banned on these pages in addition to the list: terminal (write "the call that runs the query", or name it: `.build()`, `.all()`), capability-gated / "the adapter reports the X capability" (say "PostgreSQL only" or "available on PostgreSQL; not on SQLite"), codec (say "how the value is stored and read", or name the type id such as `pg/int4@1`), codec inferer, facet, factory, "wired to", execution context, envelope (say "the driver's own result object"), materialize, surface, "source-read from an internal design document" (say "planned, not shipped"), namespace-qualified (show `db.sql.public.user`, and say `public` is the PostgreSQL schema), "auto-parameterized" (say "sent as a query parameter, never pasted into the SQL"), field accessor (say "the `f` argument, one property per column"). +- `db.sql.public.user` is keyed by table name (the model name with a lowercase first letter unless `@@map`), which differs from `db.orm.public.User`. Say this once per page where `db.sql` first appears. +- `db.runtime()` is synchronous on PostgreSQL and returns a promise on MongoDB: `(await db.runtime()).query(built)`. State this where `runtime()` first appears on a page. +- Every raw or builder query ends with `.build()` and is then passed to `runtime.query(...)` for rows or `runtime.execute(...)` for a count. +- Type ids such as `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 source of truth is prisma/orm at 8.0.0-rc.10 (`cfccb09be2`), checked out at `wip/prisma-src/`. Where the page names an rc.9 behaviour that rc.10 changed, rc.10 wins. +- The error-reference page is generated from prisma/orm by `apps/docs/scripts/generate-error-reference.mjs` and is out of scope; its wording is fixed upstream. From 5f69da09266c7a291959579434093a9fd0ed1a99 Mon Sep 17 00:00:00 2001 From: willbot Date: Sun, 13 Sep 2026 08:28:36 +0200 Subject: [PATCH 02/14] docs(orm8): orm/reference pages, banned terms replaced and facts checked against rc.10 Banned-term pass on sql-query-builder, pipeline-builder, and transactions-and-runtime (raw-queries and index had none); renamed headings keep their old anchors. Then one checker per page against prisma/orm at 8.0.0-rc.10 (cfccb09be2). Corrections that change what the pages say: orderBy's nulls option never reaches the SQL; returning() also works on SQLite; the runtime comes from db.runtime(), and connect() is optional; the execute option scope only labels a query for middleware; pipeline results are decoded only through eight stages, so after lookup() or group() the whole row is raw; from() takes the roots-map name; several "not exercised" caveats were wrong because Prisma ORM's own tests do run them; MongoDB raw findOneAndUpdate rejects sort and returnDocument at the type level; bare interpolated values in raw SQL work in rc.10. The pages no longer claim their examples are transcribed from a test suite. Reports are in docs/orm-docs-audit/c21-orm-reference/, to be removed before merge. Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- .../docs/orm/reference/pipeline-builder.mdx | 116 +++++++-------- .../docs/orm/reference/raw-queries.mdx | 10 +- .../docs/orm/reference/sql-query-builder.mdx | 88 +++++------ .../reference/transactions-and-runtime.mdx | 138 +++++++++--------- .../factcheck1.pipeline-builder.md | 70 +++++++++ .../factcheck1.raw-queries.md | 52 +++++++ .../factcheck1.sql-query-builder.md | 71 +++++++++ .../factcheck1.transactions-and-runtime.md | 78 ++++++++++ 8 files changed, 447 insertions(+), 176 deletions(-) create mode 100644 docs/orm-docs-audit/c21-orm-reference/factcheck1.pipeline-builder.md create mode 100644 docs/orm-docs-audit/c21-orm-reference/factcheck1.raw-queries.md create mode 100644 docs/orm-docs-audit/c21-orm-reference/factcheck1.sql-query-builder.md create mode 100644 docs/orm-docs-audit/c21-orm-reference/factcheck1.transactions-and-runtime.md diff --git a/apps/docs/content/docs/orm/reference/pipeline-builder.mdx b/apps/docs/content/docs/orm/reference/pipeline-builder.mdx index 66c5a0665ff..455d371fd1a 100644 --- a/apps/docs/content/docs/orm/reference/pipeline-builder.mdx +++ b/apps/docs/content/docs/orm/reference/pipeline-builder.mdx @@ -1,18 +1,18 @@ --- title: Pipeline builder reference -description: 'Reference for the Prisma ORM MongoDB pipeline builder''s stages, accumulators, expression helpers, and write terminals.' +description: 'Reference for the Prisma ORM MongoDB pipeline builder''s stages, accumulators, expression helpers, and write methods.' url: /orm/reference/pipeline-builder metaTitle: Prisma ORM MongoDB pipeline builder reference -metaDescription: 'Reference for the Prisma ORM MongoDB pipeline builder''s stages, accumulators, expression helpers, and write terminals.' +metaDescription: 'Reference for the Prisma ORM MongoDB pipeline builder''s stages, accumulators, expression helpers, and write methods.' --- -The pipeline builder gives you a typed way to build MongoDB aggregation pipelines. You reach it through `db.query`, chain aggregation stages onto a starting collection, and resolve the pipeline with a terminal. It is the MongoDB counterpart to the [SQL query builder](/orm/reference/sql-query-builder): a lower-level surface for the queries the [ORM client](/orm/reference/orm-client) doesn't express. +The pipeline builder gives you a typed way to build MongoDB aggregation pipelines. You reach it through `db.query`, chain aggregation stages onto a starting collection, and finish the chain with the call that builds the query. It is the MongoDB counterpart to the [SQL query builder](/orm/reference/sql-query-builder): a lower-level API for the queries the [ORM client](/orm/reference/orm-client) doesn't express. The pipeline builder is aggregation-only by design. There is no `find` and no `distinct`; to fetch a single document, filter and then `limit(1)`. Everything the builder produces compiles to a MongoDB aggregation pipeline. Use the ORM client (`db.orm`) for everyday reads and writes across models and relations. Drop down to the pipeline builder when you need a stage the ORM client doesn't expose: grouping, `$lookup` joins with post-processing, computed projections, multi-stage transformations, or aggregation-time writes with `$out` / `$merge`. -This page documents every stage, accumulator, expression helper, and write terminal, with the behavior that the executable test suite confirmed and the caveats it surfaced. Where a method is not executable in the validation harness, the Remarks say so. +This page documents every stage, accumulator, expression helper, and write method, with the caveats that apply to each. Where Prisma ORM's own tests do not run a method against a live MongoDB, the Remarks say so. :::note[This is the MongoDB pipeline builder] The pipeline builder targets MongoDB only. PostgreSQL has no pipeline builder: use the [SQL query builder](/orm/reference/sql-query-builder) for PostgreSQL joins and aggregates, and the [ORM client](/orm/reference/orm-client) for everyday queries on either database. @@ -20,7 +20,7 @@ The pipeline builder targets MongoDB only. PostgreSQL has no pipeline builder: u ## 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 MongoDB database. `Post` is a discriminated model (`Article` and `Tutorial` variants share the `posts` collection, discriminated by `kind`). +All examples on this page run against the following schema. `Post` is a discriminated model (`Article` and `Tutorial` variants share the `posts` collection, discriminated by `kind`).
@@ -83,7 +83,7 @@ model Tutorial { ## Entry points -Every pipeline starts with `db.query.from(root)` and ends with a terminal. Between them you chain stages. +Every pipeline starts with `db.query.from(root)` and ends with a call that turns the chain into a query you can run. Between them you chain stages. ### `from()` @@ -92,8 +92,8 @@ Enter the pipeline builder on a collection. #### Remarks - MongoDB only. `db.query` is the pipeline-builder entry point on the client, the same object the standalone `mongoQuery(...)` returns. -- `from(root)` takes a contract **root name**: the lowercase plural collection name from the contract's `roots` map (`'posts'`, `'users'`), the same names the ORM client uses (`db.orm.users`), not the PSL model names. -- Passing an unknown root throws synchronously with the structured code `ORM.MODEL_UNKNOWN` and the message `Unknown root: "". Valid roots: ...`. +- `from(root)` takes a contract **root name**: the collection name from the contract's `roots` map (`'posts'`, `'users'`), the same names the ORM client uses (`db.orm.users`), not the model names. The collection name is the model's `@@map` value, or the model name with a lowercase first letter when there is no `@@map`. +- Passing an unknown root throws right away, with an error whose `code` is `ORM.MODEL_UNKNOWN` and whose message is `Unknown root: "". Valid roots: ...`. - `from()` returns a builder you chain stages onto. The builder moves through three states as you chain: a starting collection, a filtered collection after `match()`, and a pipeline chain after any other stage. Each state exposes the methods that are valid at that point (for example, the root-level writes `insertOne` / `insertMany` are only available before you add stages). #### Options @@ -106,7 +106,7 @@ Enter the pipeline builder on a collection. | Return type | Example | Description | |---|---|---| -| Pipeline builder | `db.query.from('posts')` | A builder you chain stages and a terminal onto. | +| Pipeline builder | `db.query.from('posts')` | A builder you chain stages onto, then finish with `build()`. | #### Examples @@ -124,8 +124,8 @@ A pipeline is inert until you run it. `build()` (or its alias `aggregate()`) tur #### Remarks - `runtime.query(plan)` is the executor. It accepts any plan, including one built by the pipeline builder. On MongoDB `db.runtime()` returns a promise, so the call reads `(await db.runtime()).query(plan)`; hold the runtime in a variable if you run several plans. It returns an `AsyncIterableResult`: `await` it for an array of documents, or `for await` over it to take one at a time. There is no `db.execute` on the MongoDB client. -- **Read results are codec-decoded.** Unlike [raw queries](/orm/reference/raw-queries), a pipeline built through `db.query` carries a result shape, so top-level `_id` fields come back as decoded hex strings, and other fields are decoded to their contract types. -- A sub-document pulled in by `lookup()` is **not** decoded the same way: its `_id` comes back as a raw driver `ObjectId`. Compare it with `String(...)`. This matches the ORM client's `include()` behavior. +- **Read results are decoded, up to a point.** Unlike [raw queries](/orm/reference/raw-queries), a pipeline built through `db.query` carries a result shape, so `_id` comes back as a decoded hex string and other fields come back as their contract types. Only `match()`, `sort()`, `limit()`, `skip()`, `sample()`, `project()`, `addFields()`, and `vectorSearch()` carry that shape forward. +- Every other stage, `lookup()` and `group()` included, drops the result shape. From that point the whole row comes back as the driver gives it: a looked-up sub-document's `_id`, and the top-level `_id` beside it, are raw driver `ObjectId` values. Compare them with `String(...)`. #### Examples @@ -138,7 +138,7 @@ const posts = await (await db.runtime()).query(plan); ## Pipeline stages -Stages transform the documents flowing through the pipeline. Chain them in order; each stage's output feeds the next. Field-accessor callbacks (`(f) => ...`) reference the current document's fields. +Stages transform the documents flowing through the pipeline. Chain them in order; each stage's output feeds the next. The `(f) => ...` callbacks reference the current document's fields, one property per field. ### `match()` @@ -146,9 +146,9 @@ Filter documents by a predicate. #### Remarks -- `match()` takes a callback that receives a field accessor and returns a filter expression (`(f) => f.kind.eq('tutorial')`). -- `match()` filter values are **not codec-encoded**. The value you pass is compared as-is against the stored value. For most scalar fields this is what you want, but it breaks down for `_id` (see the warning below). -- Leaf fields expose these filter operators: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`, `exists`, and `type`. `eq` and `expr(...)`-wrapped comparisons are exercised by our executable test suite; the remaining operators are documented from the builder's source (`field-accessor.ts`) and follow the matching MongoDB query operators (`$ne`, `$gt`, `$in`, `$exists`, `$type`, and so on). +- `match()` takes a callback whose `f` argument has one property per field, and returns a filter expression (`(f) => f.kind.eq('tutorial')`). +- `match()` filter values are **not encoded to the stored type**. The value you pass is compared as-is against the stored value. For most scalar fields this is what you want, but it breaks down for `_id` (see the warning below). +- Leaf fields expose these filter operators: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`, `exists`, and `type`. `eq` and `expr(...)`-wrapped comparisons run in Prisma ORM's own tests; the remaining operators follow the matching MongoDB query operators (`$ne`, `$gt`, `$in`, `$exists`, `$type`, and so on). - For an aggregation-expression predicate (comparing computed values rather than a field against a constant), wrap it in `expr(...)`: `match((f) => expr(fn.gt(fn.year(f.createdAt), fn.literal(2023))))`. See [Expression helpers](#expression-helpers). #### Options @@ -161,7 +161,7 @@ Filter documents by a predicate. | Return type | Example | Description | |---|---|---| -| Filtered builder | `db.query.from('posts').match(...)` | A builder narrowed by the filter, chainable into more stages, write terminals, or a read terminal. | +| Filtered builder | `db.query.from('posts').match(...)` | A builder narrowed by the filter, chainable into more stages, write methods, or `build()`. | #### Examples @@ -188,7 +188,7 @@ const recent = await (await db.runtime()).query(plan); ``` :::warning[`_id` equality filters do not work through the pipeline builder] -Filtering by `_id` equality inside `match()` **never matches any document**, and this is verified empirically. Neither a hex string (`f._id.eq('507f...')`) nor a real driver `ObjectId` matches: the pipeline builder's value resolver walks the value as a plain object, destructuring an `ObjectId` into its internal `buffer` bytes, so the filter that reaches MongoDB never carries a real `ObjectId`. Note that the hex-string form **compiles without any cast** — round-tripping a decoded `_id` back into `match()` typechecks cleanly and silently returns zero rows. Only the driver-`ObjectId` form requires a cast to attempt. +Filtering by `_id` equality inside `match()` **never matches any document**. Neither a hex string (`f._id.eq('507f...')`) nor a real driver `ObjectId` matches: the pipeline builder's value resolver walks the value as a plain object, destructuring an `ObjectId` into its internal `buffer` bytes, so the filter that reaches MongoDB never carries a real `ObjectId`. Note that the hex-string form **compiles without any cast**. Round-tripping a decoded `_id` back into `match()` typechecks cleanly and silently returns zero rows. Only the driver-`ObjectId` form requires a cast to attempt. There is currently no supported way to filter by `_id` equality through the typed pipeline builder. The escape hatch is [`rawCommand()`](#rawcommand), which places a real `ObjectId` directly in a raw pipeline document and matches correctly, or the [ORM client](/orm/reference/orm-client), which handles `_id` encoding for you (`db.orm.posts.where({ _id })`). ::: @@ -426,7 +426,7 @@ Unroll an array field into one document per element. #### Remarks - `unwind(field, { preserveNullAndEmptyArrays? })` maps to MongoDB's `$unwind`. It requires an **array-typed** field to unroll. -- This stage is documented from its signature only: the example schema has no array field to unwind, so `unwind()` is not exercised by the validation harness. Verify it against a real array field in your own schema before relying on it. +- The example schema has no array field, so there is no example here. One input document becomes one output document per element of the named array. #### Options @@ -447,10 +447,10 @@ Group documents by a key and compute per-group aggregates. #### Remarks -- `group()` takes a callback that receives **only** the field accessor (`(f) => ...`) and returns a spec object. The spec's `_id` sets the grouping key; every other key must be an **accumulator**. +- `group()` takes a callback that receives **only** the `f` argument, one property per field (`(f) => ...`), and returns a spec object. The spec's `_id` sets the grouping key; every other key must be an **accumulator**. - Accumulators come from the imported `acc` namespace, **not** a second callback argument: `import { acc } from '@prisma/orm-mongo/query-builder'`, then `acc.count()`, `acc.push(f.title)`. The callback signature is single-argument; there is no `(f, acc) => ...` form. (Some older internal material shows a two-argument callback. That is incorrect.) - A `_id: null` key groups the whole collection into a single bucket. -- A non-accumulator value for a non-`_id` key is a compile error. Forced past the type system, `group()` throws at build time: `... must use an accumulator (e.g. acc.sum(), acc.count()) ...`. +- A non-accumulator value for a non-`_id` key throws as soon as you call `group()`, with an error whose `code` is `ORM.ARGUMENT_INVALID`: `group() field "" must use an accumulator (e.g. acc.sum(), acc.count()). Got "" expression.` A `null` value for a key other than `_id` throws the same way. #### Options @@ -612,19 +612,18 @@ Keep or prune documents (and sub-documents) based on an expression. #### Remarks - `redact()` maps to MongoDB's `$redact`, whose expression must evaluate to one of the system variables `$$KEEP`, `$$DESCEND`, or `$$PRUNE`. -- This is a sharp edge: `fn.literal('KEEP')` does **not** work, because `fn.literal(...)` compiles to a plain string via `$literal`, which `$redact` rejects at the server. There is no dedicated expression helper for a system-variable reference. Emitting `$$KEEP` through the typed builder requires an internal trick (a field-accessor path that itself starts with `$`), which is not a supported public pattern. -- For anything beyond a trivial redact, prefer [`rawCommand()`](#rawcommand), where you can write the `$redact` stage with `$$KEEP` / `$$PRUNE` directly. +- There is no dedicated expression helper for a system variable, and `fn.literal(...)` cannot stand in: `fn.literal('KEEP')` compiles to the plain string `KEEP` and `fn.literal('$$KEEP')` is wrapped in `$literal`, and the server rejects both. +- Use `f.rawPath('$KEEP')` instead. `rawPath` builds a field path without checking it against the model, and a field path is compiled by prefixing a `$`, so `'$KEEP'` reaches the server as `$$KEEP`. Branch with `fn.cond(...)`, with `f.rawPath('$KEEP')` and `f.rawPath('$PRUNE')` as the two arms. For anything more involved, prefer [`rawCommand()`](#rawcommand), where you can write the `$redact` stage with `$$KEEP` / `$$PRUNE` directly. ### Option-object stages -Several stages take a single MongoDB stage options object rather than a typed field-accessor callback. These map closely to the underlying MongoDB stage documents. +Several stages take a single MongoDB stage options object rather than a typed `(f) => ...` callback. These map closely to the underlying MongoDB stage documents. #### Remarks -- The following stages take an options object: `unionWith`, `bucket`, `bucketAuto`, `geoNear`, `facet`, `graphLookup`, `setWindowFields`, `densify`, and `fill`. -- Of these, `unionWith`, `bucket`, and `graphLookup` are exercised by our executable test suite (examples below). `bucketAuto`, `geoNear`, `facet`, `setWindowFields`, `densify`, and `fill` are documented from the builder's source signatures and are **not exercised by the validation harness**. -- **The expression fields inside these options are opaque.** Fields like `bucket.groupBy`, `graphLookup.startWith`, `setWindowFields.partitionBy`, and `geoNear.near` are raw aggregation expressions (`MongoAggExpr`), not reachable through the typed field-accessor callback the classic stages use. You build these values with expression helpers and pass the underlying node (`fn.year(ref).node`), which is less type-safe than the callback stages. This is a known gap in the typed surface for these stages. -- `unionWith` is the simplest: it takes a collection name and concatenates that collection's documents onto the pipeline output. +- `bucket`, `bucketAuto`, `geoNear`, `graphLookup`, `setWindowFields`, `densify`, and `fill` take an options object. Two in this group differ: `unionWith` takes a collection name and an optional array of raw stages, and `facet` takes an object that maps each output field name to an array of raw stages. +- `geoNear`, `densify`, and `fill` are **not run against a live database** by Prisma ORM's own tests; only their signatures are checked. The rest are. +- **The expression fields inside these options are opaque.** Fields like `bucket.groupBy`, `graphLookup.startWith`, `setWindowFields.partitionBy`, and `geoNear.near` are raw aggregation expressions (`MongoAggExpr`), not reachable through the typed `(f) => ...` callback the classic stages use. You build these values with expression helpers and pass the underlying node (`fn.year(ref).node`), or name a field directly with `MongoAggFieldRef.of('price')`. That is less type-safe than the callback stages, and a known gap in the typed API. Those classes, and the stage classes `unionWith` and `facet` take (`MongoMatchStage`, `MongoCountStage`, and the rest), come from `@prisma/orm-mongo/query-ast/execution`. #### Examples @@ -636,17 +635,15 @@ const combined = await (await db.runtime()).query(plan); // posts followed by users ``` -`bucket()` and `graphLookup()` are executable but require hand-built expression nodes for their opaque fields (`bucket.groupBy`, `graphLookup.startWith`). Build those nodes with expression helpers and pass `.node`. Because the value bypasses the typed accessor, verify the stage output against your own data before relying on it. - ### Atlas-only stages `search()`, `searchMeta()`, and `vectorSearch()` build MongoDB Atlas Search stages (`$search`, `$searchMeta`, `$vectorSearch`). #### Remarks -- These stages **require MongoDB Atlas** (they need Atlas Search's `mongot` process) and are **not validated by our executable test suite**, which runs against an in-memory MongoDB with no Atlas Search backend. Only their plan shape is verified; the stages are never executed. -- `search(spec)` and `searchMeta(spec)` take an Atlas Search operator spec (for example `{ text: { query: 'hello', path: 'title' } }`). -- `vectorSearch(spec)` takes an Atlas Vector Search spec (`{ index, path, queryVector, numCandidates, limit }`). +- These stages **require MongoDB Atlas** (they need Atlas Search's `mongot` process) and Prisma ORM's own tests **never run them**, because those tests use an in-memory MongoDB with no Atlas Search. Only the built query's shape is checked. Nothing in the builder refuses them off Atlas: the stage is built and sent, and a MongoDB with no Atlas Search backend is what rejects it. +- `search(spec)` and `searchMeta(spec)` take an Atlas Search operator spec (for example `{ text: { query: 'hello', path: 'title' } }`), and an optional index name as a second argument. +- `vectorSearch(spec)` takes an Atlas Vector Search spec (`{ index, path, queryVector, numCandidates, limit }`, plus an optional `filter`). - Verify these against a real Atlas deployment before relying on them. #### Return type @@ -663,7 +660,7 @@ Accumulators compute per-group values inside a [`group()`](#group) stage. Import import { acc } from '@prisma/orm-mongo/query-builder'; ``` -The builder exposes all nineteen MongoDB accumulators: +The builder exposes nineteen accumulators: | Accumulator | Signature | Description | |---|---|---| @@ -687,7 +684,7 @@ The builder exposes all nineteen MongoDB accumulators: | `acc.stdDevPop(expr)` | `stdDevPop(expression)` | Population standard deviation. | | `acc.stdDevSamp(expr)` | `stdDevSamp(expression)` | Sample standard deviation. | -The examples below are the common accumulators, all executed against a live database. The `N`-variants (`firstN` shown), `top`/`bottom`, and the standard-deviation accumulators share the same call shapes; `firstN` is exercised here as a representative of the family. +The examples below are the common accumulators, all executed against a live database. The `N`-variants (`firstN` shown), `top`/`bottom`, and the standard-deviation accumulators share the same call shapes; `firstN` is exercised here as a representative of the family. In every signature, `sortBy` is a plain `{ field: 1 | -1 }` object and `n` is an expression, so pass `fn.literal(2)` rather than `2`. #### Examples @@ -774,7 +771,7 @@ Expression helpers (`fn.*`) build the computed values used inside stages like `a import { fn } from '@prisma/orm-mongo/query-builder'; ``` -The `fn.*` namespace mirrors MongoDB's aggregation expression operators: each helper is the camelCase name of the operator without its `$` prefix (`$toUpper` becomes `fn.toUpper`, `$dateToString` becomes `fn.dateToString`). The helpers below are grouped by category, with the ones exercised by the test suite; the full set follows the same naming pattern. +The `fn.*` namespace covers 73 of MongoDB's aggregation expression operators. It is not all of them: there is no `fn.and`, `fn.or`, `fn.not`, `fn.switch`, `fn.ifNull`, `fn.map`, `fn.reduce`, or `fn.filter`, and arithmetic stops at `add`, `subtract`, `multiply`, and `divide`. Each helper is usually the camelCase name of the operator without its `$` prefix (`$toUpper` becomes `fn.toUpper`, `$dateToString` becomes `fn.dateToString`). Five are renamed to avoid clashes: `fn.firstElem` and `fn.lastElem` for `$first` and `$last`, `fn.isIn` for `$in`, `fn.typeOf` for `$type`, and `fn.toString_` for `$toString`. The helpers below are grouped by category, with the ones exercised by the test suite. | Category | Helpers (examples) | Notes | |---|---|---| @@ -789,9 +786,10 @@ The `fn.*` namespace mirrors MongoDB's aggregation expression operators: each he #### Remarks +- Some helpers take a named-arguments object rather than positional arguments: `fn.dateToString({ date, format })`, `fn.trim({ input })`, `fn.regexMatch({ input, regex })`, `fn.convert({ input, to })`, and the rest of the date, regex, and replace helpers. - **`fn.cond()`'s condition is a raw expression, not a filter.** Pass a comparison helper's `.node`: `fn.cond(fn.eq(a, b).node, thenExpr, elseExpr)`. Passing the `expr(...)` form (which is for `match()` filters) throws `TypeError: visitor.expr is not a function`. - The `expr()` wrapper is only for `match()` filters. Inside `match()`, `expr(fn.gt(...))` is correct; inside `fn.cond()` it is not. -- **`fn.toObjectId()` cannot be composed over `fn.literal()`.** `fn.toObjectId(fn.literal(id))` throws (`Cannot read properties of undefined (reading 'codecId')`), because `fn.literal()` carries no field codec. Give `fn.toObjectId()` a real field-accessor expression instead. +- **`fn.toObjectId()` cannot be composed over `fn.literal()`.** `fn.toObjectId(fn.literal(id))` throws (`Cannot read properties of undefined (reading 'codecId')`), because `fn.literal()` has no field type information. Give `fn.toObjectId()` an expression taken from the `f` argument instead. #### Examples @@ -860,13 +858,12 @@ Append an arbitrary raw pipeline stage the typed methods don't cover. #### Remarks -- Documented from the builder's source (`builder.ts`); **not exercised by the validation harness**. -- `pipe(stage)` appends a raw `MongoPipelineStage` node and collapses the row shape to an opaque document shape. `pipe(stage)` lets you declare the resulting shape yourself. +- `pipe(stage)` appends a raw stage node, such as `new MongoMatchStage(...)` from `@prisma/orm-mongo/query-ast/execution`. It keeps the row type you already had; `pipe(stage)` lets you declare a new one. Whatever the row type says, the rows are no longer decoded, because a stage the builder does not recognise drops the result shape. - Prefer a typed stage when one exists; `pipe()` is the in-chain escape hatch, and [`rawCommand()`](#rawcommand) is the whole-command one. -## Read terminals +## Read methods [#read-terminals] -A read terminal turns the chain into an executable plan. Pass the plan to `(await db.runtime()).query(plan)` to run it. +`build()` turns the chain into a plan you can run. Pass the plan to `(await db.runtime()).query(plan)` to run it. ### `build()` and `aggregate()` @@ -881,7 +878,7 @@ Compile the pipeline into a plan. | Return type | Example | Description | |---|---|---| -| Query plan | `db.query.from('posts').build()` | A plan you run with `(await db.runtime()).query(...)`. | +| Built query | `db.query.from('posts').build()` | A plan you run with `(await db.runtime()).query(...)`. | #### Examples @@ -897,13 +894,14 @@ const rows = await (await db.runtime()).query(aggregated); ## Write methods -The pipeline builder can write, not just read. Write methods are available at three points: on the root collection, after a `match()` filter, and as pipeline write terminals. +The pipeline builder can write, not just read. Write methods are available at three points: on the root collection, after a `match()` filter, and as the last call in a pipeline. Updater callbacks (the `(f) => [...]` argument to the update methods) must return an **array** of operations. This is a firm rule: -- A bare (non-array) operation throws at build time: `Error: Unreachable: items.length > 0 but first is undefined`. This is an internal consistency guard, not a friendly validation message, so returning `[f.bio.set('x')]` rather than `f.bio.set('x')` matters. (Some older internal material shows the bare form. That is incorrect.) -- An empty array throws: `Updater returned no operations. Return at least one update from the callback ...`. -- You cannot mix operator-form ops and pipeline-form (`f.stage.*`) ops in one updater; doing so throws `Cannot mix ...`. Each updater is one form or the other. +- A bare (non-array) operation throws as soon as you call the write method: `Error: Unreachable: items.length > 0 but first is undefined`. This is an internal consistency guard, not a friendly validation message, so returning `[f.bio.set('x')]` rather than `f.bio.set('x')` matters. (Some older internal material shows the bare form. That is incorrect.) +- An empty array throws an error whose `code` is `ORM.MUTATION_DATA_MISSING`: `Updater returned no operations. Return at least one update from the callback ...`. +- You cannot mix operator-form ops and pipeline-form (`f.stage.*`) ops in one updater. TypeScript rejects the mixed array, and at run time it throws an error whose `code` is `ORM.ARGUMENT_INVALID`: `Cannot mix ...`. Each updater is one form or the other. +- Applying the same operator to the same field twice in one updater throws an error whose `code` is `ORM.ARGUMENT_INVALID`: `Update spec collision: ...`. ### Root-level writes @@ -912,7 +910,8 @@ Updater callbacks (the `(f) => [...]` argument to the update methods) must retur #### Remarks - These return a plan; run it with `(await db.runtime()).query(plan)`. -- Write results are the driver's own result envelopes (`{ insertedId }`, `{ insertedIds }`, `{ deletedCount }`, `{ modifiedCount }`, `{ upsertedId }`), not decoded documents. +- Write results are result objects, not decoded documents: `insertOne` gives `{ insertedId }`, `insertMany` gives `{ insertedIds, insertedCount }`, the update methods and `upsertOne` give `{ matchedCount, modifiedCount, upsertedCount, upsertedId }`, and the delete methods give `{ deletedCount }`. +- The document you pass to `insertOne` / `insertMany` is a plain record. The builder does not check it against the model, and its values are stored as you pass them, the same way [`match()`](#match) filter values are. `insertMany([])` throws an error whose `code` is `ORM.MUTATION_DATA_MISSING`. #### Examples @@ -934,7 +933,7 @@ const manyPlan = db.query.from('users').insertMany([ { name: 'Dave', email: 'dave@example.com', bio: null, role: 'reader', address: null }, ]); const [insertManyResult] = await (await db.runtime()).query(manyPlan); -// { insertedIds: { '0': , '1': } } +// { insertedIds: [, ], insertedCount: 2 } ``` ##### `updateAll()` (array-of-ops updater) @@ -973,7 +972,8 @@ After a `match()` filter, `updateMany`, `updateOne`, `deleteMany`, `deleteOne`, - The `match()` filter supplies the write's filter; you do not repeat it. - After `match()`, `upsertOne()` takes the updater callback only (the filter comes from `match()`). -- `updateOne` / `deleteOne` affect at most one matching document; `updateMany` / `deleteMany` affect all matches. +- `updateOne` / `deleteOne` affect at most one matching document; `updateMany` / `deleteMany` affect all matches. Which document `updateOne` / `deleteOne` picks is up to the driver. Use `findOneAndUpdate()` after a `sort()` when the choice matters. +- `findOneAndUpdate()` and `findOneAndDelete()` return the document exactly as the driver gives it, undecoded, so its `_id` is a raw `ObjectId` even though the TypeScript type says the decoded shape. Both disappear from the type after a stage MongoDB's `findAndModify` command cannot carry, such as `skip()`; forced past the type system they throw an error whose `code` is `ORM.OPERATION_UNSUPPORTED`. #### Examples @@ -1018,7 +1018,7 @@ const [result] = await (await db.runtime()).query(plan); ##### `findOneAndUpdate()` and `returnDocument` -`findOneAndUpdate()` returns the matched document. Its second argument controls which image you get back: `returnDocument: 'before'` returns the pre-update document, `'after'` returns the post-update document. The default is `'after'`. +`findOneAndUpdate()` returns the matched document. Its second argument takes `returnDocument`, which controls which version you get back: `'before'` returns the document before the update, `'after'` returns it after. The default is `'after'`. The same argument takes `upsert`, which defaults to `false` and inserts a document when nothing matches. ```typescript const afterPlan = db.query @@ -1057,8 +1057,8 @@ Inside an updater callback, each operation targets a field. There are two mutual #### Remarks -- **Operator form**: field-level operators through the field accessor, for example `f.bio.set(value)`. Per-field operators include `set`, `unset`, `rename`, `inc`, `mul`, `min`, `max`, `push`, `addToSet`, `pop`, `pull`, `pullAll`, `currentDate`, and `setOnInsert`. -- **Pipeline form**: aggregation-pipeline update stages through `f.stage.*`, for example `f.stage.set({ bio: f.name.node })` (which emits an `$addFields`-style stage). Pipeline-form ops let an update reference other fields of the same document. +- **Operator form**: field-level operators on the `f` argument, for example `f.bio.set(value)`. Per-field operators include `set`, `unset`, `rename`, `inc`, `mul`, `min`, `max`, `push`, `addToSet`, `pop`, `pull`, `pullAll`, `currentDate`, and `setOnInsert`. +- **Pipeline form**: aggregation-pipeline update stages through `f.stage.*`, for example `f.stage.set({ bio: f.name.node })` (which emits an `$addFields`-style stage). The four emitters are `set`, `unset`, `replaceRoot`, and `replaceWith`. Pipeline-form ops let an update reference other fields of the same document. - The two forms cannot be mixed in a single updater (see the write-methods intro). An updater is entirely operator-form or entirely pipeline-form. #### Examples @@ -1084,19 +1084,19 @@ await (await db.runtime()).query(plan); // each author's bio is set to that author's own name ``` -### Pipeline write terminals: `out()` and `merge()` +### Pipeline write methods: `out()` and `merge()` [#pipeline-write-terminals-out-and-merge] `out()` and `merge()` write the pipeline's output into a collection (`$out` / `$merge`). #### Remarks -- `out(collection)` materializes the pipeline output into a destination collection, replacing its contents. -- `merge({ into })` streams the pipeline output into a target collection, merging with existing documents. -- Both are terminals: they return a plan you run with `(await db.runtime()).query(plan)`, and produce no rows of their own. +- `out(collection)` writes the pipeline output into a destination collection, replacing its contents. A second argument names a different database. +- `merge({ into })` streams the pipeline output into a target collection, merging with existing documents. `into` is a collection name or `{ db, coll }`. The options object also takes `on` (the field or fields to match on), `whenMatched`, and `whenNotMatched`. +- Both end the chain: they return a plan you run with `(await db.runtime()).query(plan)`, and produce no rows of their own. #### Examples -##### Materialize with `out()` +##### Write the output with `out()` ```typescript const plan = db.query.from('users').out('users_snapshot'); @@ -1117,9 +1117,9 @@ Run a raw MongoDB aggregate command through the pipeline builder, bypassing the #### Remarks -- `db.query.rawCommand(command)` packages a raw command (for example `new RawAggregateCommand(collection, pipeline)`) into a plan. The rows come back as `unknown`; you type them yourself. +- `db.query.rawCommand(command)` packages a command node (for example `new RawAggregateCommand(collection, pipeline)`) into a plan, without translating it. The rows come back as `unknown`; you type them yourself. - Because it bypasses the typed AST, `rawCommand()` is the escape hatch for anything the typed builder can't express, including `_id` equality filters (see the [`match()`](#match) warning) and `$redact` with `$$KEEP` / `$$PRUNE`. -- For the full raw MongoDB surface (raw collection methods, untyped writes, and the undecoded-results behavior), see [Raw queries](/orm/reference/raw-queries). +- For the full raw MongoDB API (raw collection methods, untyped writes, and the undecoded-results behavior), see [Raw queries](/orm/reference/raw-queries). #### Examples diff --git a/apps/docs/content/docs/orm/reference/raw-queries.mdx b/apps/docs/content/docs/orm/reference/raw-queries.mdx index db0554ed5eb..961d503e381 100644 --- a/apps/docs/content/docs/orm/reference/raw-queries.mdx +++ b/apps/docs/content/docs/orm/reference/raw-queries.mdx @@ -22,7 +22,7 @@ A **raw fragment** is a piece of SQL spliced into a builder query: you write it A **whole raw statement** is a complete SQL statement written with `db.raw.sql`, which you terminate with `.returnsRow(spec).build()` for a plan `runtime.query(...)` streams rows from, or `.affectedCount().build()` for a plan `runtime.execute(...)` reports statistics for. Both sizes end in a plan, so both go through the same middleware and the same runtime. -The examples in this section run against the `user` / `post` schema from the SQL query builder page. See its [example schema](/orm/reference/sql-query-builder#example-schema) for the full model definitions. As on that page, create the client with `postgres(...)`, connect it for a runtime, and reach tables through `db.sql.public`: +The examples in this section run against the `user` / `post` schema from the SQL query builder page. See its [example schema](/orm/reference/sql-query-builder#example-schema) for the full model definitions. As on that page, create the client with `postgres(...)`, connect it for a runtime, and reach tables through `db.sql.public`, which is keyed by table name (the model name with a lowercase first letter unless the model sets `@@map`): ```ts import postgres from '@prisma/orm-postgres/runtime'; @@ -158,7 +158,7 @@ Use `.affectedCount()` for a statement that returns no rows. It takes no spec, a ```ts const plan = db.raw.sql` UPDATE "post" - SET "viewCount" = "viewCount" + 1 + SET priority = ${'urgent'} WHERE "userId" IN (SELECT id FROM "user" WHERE kind = ${kind}) ` .affectedCount() @@ -211,7 +211,7 @@ TypeScript already rejects an unsupported value at compile time. If you need to `db.raw.collection(rootName)` returns a raw collection with nine methods for running MongoDB commands directly against a collection. The root name is the contract's lowercase plural root (`'users'`, `'posts'`), the same name the ORM client uses (`db.orm.users`). -Each method returns a buildable command: call `.build()` to get a plan, then run it with `(await db.runtime()).query(plan)`. Because raw filters carry native BSON values, the examples below construct ids with the driver's `ObjectId` class: +Each method returns a buildable command: call `.build()` to get a plan, then run it with `(await db.runtime()).query(plan)`. On MongoDB `db.runtime()` returns a promise, so you await it; on PostgreSQL it is synchronous. Because raw filters carry native BSON values, the examples below construct ids with the driver's `ObjectId` class: ```ts import { ObjectId } from 'mongodb'; @@ -310,12 +310,12 @@ Atomically update a matching document and return an image of it. The third argum #### Remarks -- The MongoDB command AST carries `sort` and `returnDocument` fields, but the public `findOneAndUpdate()` wrapper does **not** expose them. Passing them in the options object has no effect: they are silently dropped, not a type or runtime error. A caller who needs either must build the raw command directly, bypassing this collection wrapper. +- The MongoDB command AST carries `sort` and `returnDocument` fields, but the public `findOneAndUpdate()` wrapper does **not** expose them. Its options argument is `{ upsert?: boolean }` and nothing else, so TypeScript rejects an options literal that names either key, and a value that reaches the call untyped is dropped without a run-time error. A caller who needs either must build the raw command directly with `new RawFindOneAndUpdateCommand(...)` and run it through `db.query.rawCommand(...)`, bypassing this collection wrapper. - Because `returnDocument` is never forwarded, the native driver's own default applies, which is `'before'` (the pre-update image). This has a surprising consequence for the classic upsert-counter pattern, shown below. ##### Upsert counter pattern -Because the wrapper always uses the driver's `'before'` default, the **first** call to an upsert counter returns an **empty** result: the upsert creates a brand-new document, so there is no pre-image to return, and the driver yields `null`. Read the counter value on the second and later calls, each of which returns the pre-image of its own increment. +Because the wrapper always uses the driver's `'before'` default, the **first** call to an upsert counter returns an **empty** result: the upsert creates a brand-new document, so there is no pre-image to return, the driver returns `null`, and the query yields no row. Read the counter value on the second and later calls, each of which returns the pre-image of its own increment. ```ts const filter = { _id: 'pageViews' }; 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 f2a15bddde7..860394b2d3e 100644 --- a/apps/docs/content/docs/orm/reference/sql-query-builder.mdx +++ b/apps/docs/content/docs/orm/reference/sql-query-builder.mdx @@ -6,9 +6,9 @@ metaTitle: Prisma ORM SQL query builder reference metaDescription: 'Reference for the Prisma ORM SQL query builder''s select, mutation, and grouped query methods.' --- -The SQL query builder gives you table-level, SQL-shaped methods for building typed queries: `select()`, `innerJoin()`, `groupBy()`, and the rest. It's the lower-level escape hatch that sits alongside the [ORM client](/orm/reference/orm-client). Reach for it when you need a join, aggregate, or SQL feature the ORM client doesn't expose, or when you want direct control over the generated SQL. +The SQL query builder gives you table-level, SQL-shaped methods for building typed queries: `select()`, `innerJoin()`, `groupBy()`, and the rest. It's the lower-level escape hatch that is available alongside the [ORM client](/orm/reference/orm-client). Reach for it when you need a join, aggregate, or SQL feature the ORM client doesn't expose, or when you want direct control over the generated SQL. -This page documents every method, its options, and its return type. Where a method is capability-gated or has a runtime caveat, the Remarks call that out. For a task-oriented guide to when and how to reach for the builder, see [Advanced queries](/orm/fundamentals/advanced-queries). +This page documents every method, its options, and its return type. Where a method works only on some databases, or has a caveat that only shows up when the query runs, the Remarks call that out. For a task-oriented guide to when and how to reach for the builder, see [Advanced queries](/orm/fundamentals/advanced-queries). :::note[This is the SQL-family builder] The SQL query builder targets SQL databases; PostgreSQL and SQLite are supported today. MongoDB has no SQL builder: use the [ORM client](/orm/reference/orm-client) for MongoDB queries, and the [pipeline builder](/orm/reference/pipeline-builder) for aggregation pipelines. @@ -16,7 +16,7 @@ The SQL query builder targets SQL databases; PostgreSQL and SQLite are supported ## 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 PostgreSQL database. The select and mutation examples use this `user` / `post` / `post_tag` / `tag` schema; the grouped-query examples use a separate `customer` / `order` schema, shown under [Grouped queries](#grouped-queries). +All examples on this page run against the following schema. The select and mutation examples use this `user` / `post` / `post_tag` / `tag` schema; the grouped-query examples use a separate `customer` / `order` schema, shown under [Grouped queries](#grouped-queries).
@@ -128,11 +128,11 @@ model Feature { ## Entry points -You build queries from the client's `sql` facet, which carries your contract's execution context. Create a Postgres client with `postgres(...)`, then reach tables through `db.sql.public`. Table accessors use the contract's **mapped table names** (snake_case), so a `User` model mapped to `user` is reached as `db.sql.public.user`, and a `Post`/`Tag` junction mapped to `post_tag` is reached as `db.sql.public.post_tag`. `public` is the default PostgreSQL schema namespace. +You build queries from the client's `sql` property, which knows your contract and how your queries are run. Create a Postgres client with `postgres(...)`, then reach tables through `db.sql.public`. Table accessors are keyed by table name, not model name. A table name is the model name with a lowercase first letter unless the model sets `@@map`, so the `User` model mapped to `user` is reached as `db.sql.public.user`, and the model for the `Post`/`Tag` join table, mapped to `post_tag`, is reached as `db.sql.public.post_tag`. Column names are the field names unless a field sets `@map`. `public` is the PostgreSQL schema, the namespace your tables live in unless you set another one. -### The `db.sql` facet +### The `db.sql` builder [#the-dbsql-facet] -Create the client with `postgres(...)`, connect it to get a runtime, and build queries off `db.sql.public`. A `select()`, `insert()`, `update()`, or `delete()` call starts a query; you finish it with [`build()`](#build) and run the resulting plan with `runtime.query(...)`. +Create the client with `postgres(...)`, take a runtime from `db.runtime()`, and build queries off `db.sql.public`. On PostgreSQL `db.runtime()` is synchronous, so there is nothing to await. A `select()`, `insert()`, `update()`, or `delete()` call starts a query; you finish it with [`build()`](#build) and run the built query with `runtime.query(...)`. ```ts import postgres from '@prisma/orm-postgres/runtime'; @@ -140,25 +140,25 @@ import type { Contract } from './contract.d'; import contractJson from './contract.json' with { type: 'json' }; const db = postgres({ contractJson, url: process.env.DATABASE_URL }); -const runtime = await db.connect(); +const runtime = db.runtime(); const plan = db.sql.public.user.select('id', 'email').build(); const users = await runtime.query(plan); ``` -The facet is built from the `sql()` factory: `db.sql` holds the result of an internal `sql({ context, rawCodecInferer })` call, wired to the client's execution context and the adapter's codec inferer. You call `sql(...)` directly only when you're building your own database wrapper instead of using the `postgres()` client's facet. Pass your execution `context` (from your database client) and a `rawCodecInferer` (a fallback codec for raw expressions that have no column to infer one from): +`db.sql` holds the result of an internal `sql({ context, rawCodecInferer })` call, connected to the client's way of running queries and to the adapter's rule for how values are stored and read. You call `sql(...)` directly only when you're building your own database wrapper instead of using the builder on the `postgres()` client. Pass your `context` (from your database client) and a `rawCodecInferer` (a fallback rule for how to store and read raw expressions that have no column to take that from): ```ts import { sql } from '@prisma/orm-postgres/builder/runtime'; -const publicSql = sql({ context, rawCodecInferer: { inferCodec: () => 'pg/text' } }).public; +const publicSql = sql({ context, rawCodecInferer: { inferCodec: () => 'pg/text@1' } }).public; ``` -This page reaches tables through the client facet and refers to the root as `db.sql.public`. +This page reaches tables through the client's builder and refers to the root as `db.sql.public`. ### Table access -Every table on `db.sql.public` exposes the query-building methods. A `select()`, `insert()`, `update()`, or `delete()` call starts a query; you finish it with [`build()`](#build) and run the resulting plan with `runtime.query(...)`. +Every table on `db.sql.public` exposes the query-building methods. A `select()`, `insert()`, `update()`, or `delete()` call starts a query; you finish it with [`build()`](#build) and run the built query with `runtime.query(...)`. ```ts db.sql.public.user.select('id', 'email'); // start a SELECT @@ -184,7 +184,7 @@ const highPriorityPosts = db.sql.public.post These methods build and refine a `SELECT`. They chain, and you resolve the query with [`build()`](#build) followed by `runtime.query(...)`. -The `where()`, `select()`, `orderBy()`, `update()`, and other callback forms receive two arguments: a field accessor `f` (columns keyed by name, or namespace-qualified after a join, such as `f.post.id`) and a function bag `fns` (the [expression helpers](#expressions-and-functions)). +The `where()`, `select()`, `orderBy()`, `update()`, and other callback forms receive two arguments: the `f` argument, one property per column (after a join, columns are grouped under their table name, such as `f.post.id`), and a function bag `fns` (the [expression helpers](#expressions-and-functions)). ### `select()` @@ -196,7 +196,7 @@ Project a row down to a chosen set of columns or computed expressions. | Form | Signature | Description | |---|---|---| -| Column names | `select('col', 'col2', ...)` | Keep the named columns. Each name resolves against the current scope; an unknown name throws `Column "x" not found in scope`. | +| Column names | `select('col', 'col2', ...)` | Keep the named columns. Each name resolves against the current scope; an unknown name throws an error whose `code` is `ORM.COLUMN_UNKNOWN`. | | Aliased expression | `select(alias, (f, fns) => expr)` | Add one computed column under `alias`. | | Object of expressions | `select((f, fns) => ({ alias: expr, ... }))` | Add several computed columns at once. The row type is inferred from the object's shape. | @@ -252,7 +252,7 @@ Restrict a query to rows matching an expression. | Name | Type | Required | Description | |---|---|---|---| -| `predicate` | `(f, fns) => Expression` | Yes | A boolean expression built from the field accessor and function bag. Combine comparisons with `fns.and(...)` and `fns.or(...)`, nested freely. | +| `predicate` | `(f, fns) => Expression` | Yes | A boolean expression built from `f` and `fns`. Combine comparisons with `fns.and(...)` and `fns.or(...)`, nested freely. | #### Return type @@ -279,7 +279,7 @@ const rows = await runtime.query(plan); ### `innerJoin()` -Combine matching rows from two tables. After a join, address columns with their table namespace (`f.post.id`, `f.user.email`) to avoid ambiguity. +Combine matching rows from two tables. After a join, address columns by their table name (`f.post.id`, `f.user.email`) to avoid ambiguity. #### Options @@ -292,7 +292,7 @@ Combine matching rows from two tables. After a join, address columns with their | Return type | Example | Description | |---|---|---| -| `SelectQuery` | `db.sql.public.post.innerJoin(db.sql.public.user, ...)` | A query over the joined tables, with both tables' columns in scope under their namespaces. | +| `SelectQuery` | `db.sql.public.post.innerJoin(db.sql.public.user, ...)` | A query over the joined tables, with both tables' columns in scope under their table names. | #### Examples @@ -336,10 +336,10 @@ Correlate a per-row subquery against the outer row: for each outer row, the join #### Remarks -- **Availability:** `lateralJoin()` requires the adapter to report the `sql.lateral` capability. Prisma ORM's PostgreSQL adapter reports it, so the method is available on PostgreSQL. Adapters that don't report `sql.lateral` won't expose the method. +- **Availability:** PostgreSQL only. On an adapter without lateral joins, such as SQLite, TypeScript types `lateralJoin` as `never`, so the call does not compile; calling it anyway throws an error whose `code` is `ORM.CAPABILITY_MISSING`. `outerLateralJoin()` is the left-join form: same arguments, same availability, and the subquery's columns are nullable. - The callback receives a `lateral` builder. Start its subquery with `lateral.from(otherTable)`, then chain the usual `SELECT` methods. The subquery can filter on the outer row's columns (`f.user.id`). - Return the query chain **directly** from the callback. Do **not** call `.as(...)` on it: `lateralJoin()`'s own first argument already names the derived table, and `.as(...)` returns the wrong shape (a bare join source), which throws `subquery.getRowFields is not a function`. -- When the outer table and the lateral table share a column name (for example both have `id` and `createdAt`), the merged scope **drops** the ambiguous names. Reach them through their table namespace instead (`f.post.id`, `f.post.createdAt`); a bare `select('id')` or `orderBy((f) => f.createdAt, ...)` throws because the ambiguous name isn't in scope. +- When the outer table and the lateral table share a column name (for example both have `id` and `createdAt`), the merged scope **drops** the ambiguous names. Reach them through their table name instead (`f.post.id`, `f.post.createdAt`); a bare `select('id')` or `orderBy((f) => f.createdAt, ...)` throws because the ambiguous name isn't in scope. #### Options @@ -387,7 +387,7 @@ Sort the result set by a column, a computed expression, or with explicit directi |---|---|---|---| | `key` | Column name (`string`) or `(f, fns) => Expression` | Yes | The column or computed value to sort by. | | `options.direction` | `'asc' \| 'desc'` | No | Sort direction. | -| `options.nulls` | `'first' \| 'last'` | No | Where nulls sort relative to non-null values. | +| `options.nulls` | `'first' \| 'last'` | No | TypeScript accepts it, but nothing reads it: no `NULLS FIRST` or `NULLS LAST` reaches the SQL, so the database's own null ordering applies. | #### Return type @@ -419,7 +419,7 @@ const rows = await runtime.query(plan); ```ts const plan = db.sql.public.post .select('id', 'embedding') - .orderBy('embedding', { direction: 'asc', nulls: 'first' }) + .orderBy('embedding', { direction: 'asc', nulls: 'first' }) // nulls adds nothing to the SQL .build(); const rows = await runtime.query(plan); ``` @@ -450,7 +450,7 @@ Keep the first row per distinct key, according to the query's `orderBy()` (`DIST #### Remarks -- **Availability:** `distinctOn()` requires the adapter to report the `postgres.distinctOn` capability. `DISTINCT ON` is a PostgreSQL-only SQL extension, so it lives under the `postgres` capability namespace rather than the shared `sql` one. It's available on Prisma ORM's PostgreSQL adapter. +- **Availability:** PostgreSQL only. `DISTINCT ON` is a PostgreSQL extension to SQL. On SQLite TypeScript types `distinctOn` as `never`, so the call does not compile; calling it anyway throws an error whose `code` is `ORM.CAPABILITY_MISSING`. - Pair it with an `orderBy()` on the same key(s) to control which row is kept per key. This ordering requirement is not type-enforced; supply it yourself for a meaningful result. #### Options @@ -488,8 +488,8 @@ Cap the number of returned rows and skip rows in the ordered result set. | Method | Argument | Description | |---|---|---| -| `limit(n)` | `number` | Return at most `n` rows. | -| `offset(n)` | `number` | Skip the first `n` rows. | +| `limit(n)` | `number`, or a numeric expression | Return at most `n` rows. | +| `offset(n)` | `number`, or a numeric expression | Skip the first `n` rows. | #### Return type @@ -546,7 +546,7 @@ const rows = await runtime.query(plan); ## Grouped queries -Grouping starts with [`groupBy()`](#groupby), which turns a query into a `GroupedQuery`. A grouped query supports `having()` for group-level filtering, the aggregate functions, and the same `orderBy()` / `limit()` / `offset()` / `distinct()` surface as a `SelectQuery`. +Grouping starts with [`groupBy()`](#groupby), which turns a query into a `GroupedQuery`. A grouped query supports `having()` for group-level filtering, the aggregate functions, and the same `orderBy()`, `limit()`, `offset()`, and `distinct()` methods as a `SelectQuery`. The examples below use a separate `customer` / `order` schema, where `order.amount` and `order.quantity` are numeric columns: @@ -626,7 +626,7 @@ const plan = db.sql.public.order .build(); const rows = await runtime.query(plan); // rows === [{ yearPlaced: '2024', orderCount: 10 }] -// yearPlaced is a string: a raw expression is not decoded through a codec, and EXTRACT returns numeric. +// yearPlaced is a string: Prisma ORM does not convert what a raw expression returns, and EXTRACT returns numeric. ``` ### `having()` @@ -725,7 +725,7 @@ Insert one or more rows in a single statement. | Name | Type | Required | Description | |---|---|---|---| -| `rows` | Array of row objects | Yes | The rows to insert. Values are auto-parameterized; their codecs are inferred from the target columns. | +| `rows` | Array of row objects | Yes | The rows to insert. Each value is sent as a query parameter, never pasted into the SQL; how it is stored and read comes from the target column. | #### Return type @@ -760,7 +760,7 @@ Return columns from the rows affected by an `insert()`, `update()`, or `delete() #### Remarks -- **Availability:** `returning()` requires the adapter to report the `sql.returning` capability. Prisma ORM's PostgreSQL adapter reports it. Adapters that don't (for example a SQL target without `RETURNING`) won't expose the method. +- **Availability:** PostgreSQL and SQLite. Both support SQL's `RETURNING`. On an adapter for a database without it, TypeScript types `returning` as `never`, so the call does not compile; calling it anyway throws an error whose `code` is `ORM.CAPABILITY_MISSING`. #### Options @@ -856,12 +856,12 @@ const rows = await runtime.query(plan); ### `param()` -Force an explicit codec on a raw value that has no column context to infer one from, for example a literal interpolated into `fns.raw`. +Say explicitly how a raw value is stored and read when it has no column to take that from, for example a literal interpolated into `fns.raw`. #### Remarks - Import `param` from `@prisma/orm-postgres/relational-core/expression`. -- You rarely need `param()`. Values passed to `insert()`, `update()`, and comparison functions such as `fns.eq(f.col, value)` are already auto-parameterized, with codecs inferred from the target column. `param()` is the manual escape hatch for a bound value with no adjacent column to derive a codec from. +- You rarely need `param()`. Values passed to `insert()`, `update()`, and comparison functions such as `fns.eq(f.col, value)` are already sent as query parameters, never pasted into the SQL, and how each is stored and read comes from the target column. `param()` is the manual escape hatch for a bound value with no neighbouring column to take that from. - There is **no** `build({ params })` form. Parameter values are embedded into the query where they're supplied. See [`build()`](#build). #### Options @@ -869,7 +869,7 @@ Force an explicit codec on a raw value that has no column context to infer one f | Name | Type | Required | Description | |---|---|---|---| | `value` | `T` | Yes | The value to bind. | -| `opts.codecId` | `string` | Yes | The codec id to encode the value with, for example `'pg/text@1'`. | +| `opts.codecId` | `string` | Yes | The type id to encode the value with, for example `'pg/text@1'`. | #### Return type @@ -895,7 +895,7 @@ const rows = await runtime.query(plan); ## Expressions and functions -Callback forms receive a function bag `fns` alongside the field accessor. `fns` holds the built-in expression helpers you use to build comparisons, boolean combinators, aggregates, and raw SQL. +Callback forms receive a function bag `fns` alongside the `f` argument. `fns` holds the built-in expression helpers you use to build comparisons, boolean combinators, aggregates, and raw SQL. ### Built-in functions @@ -909,18 +909,18 @@ These are fixed: | Existence | `exists`, `notExists` | | Raw SQL | `raw` | -The aggregates are not a fixed list. They come from the aggregate map in your emitted contract, which each target and extension contributes to, so which ones exist depends on your database. PostgreSQL contributes eight: +The aggregates are not a fixed list. They come from the aggregate map in the contract written by `prisma contract emit`, which each target and extension contributes to, so which ones exist depends on your database. PostgreSQL contributes eight: | Function | Returns | |---|---| | `count()` / `count(field)` | `number`. With no argument it counts rows; with a field it counts that field's non-null values | | `countBigInt()` / `countBigInt(field)` | `bigint`, the lossless form of `count` | -| `sum(field)` | `number` over an integer column, the column's own float or decimal type otherwise | +| `sum(field)` | `number` over an integer column; over any other column, the type PostgreSQL's own `sum` produces for it | | `sumBigInt(field)` | `bigint`, the lossless form of `sum` over an integer column | | `avg(field)` | `number` over an integer or float column | | `avgDecimal(field)` | An exact decimal string, the lossless form of `avg` | -| `min(field)` | The input column's own type | -| `max(field)` | The input column's own type | +| `min(field)` | The input column's own type; a `varchar` column widens to `text` | +| `max(field)` | The input column's own type; a `varchar` column widens to `text` | Any further function is registered dynamically: `ilike` is registered by the Postgres adapter for textual columns, while functions like `cosineDistance` come from an extension pack (pgvector) registered for your contract; without the matching adapter or extension pack, those functions are absent. @@ -968,21 +968,21 @@ const rows = await runtime.query(plan); ### `build()` -Compile a query into an executable plan. +Compile a query so you can run it. #### Remarks -- `build()` takes **zero arguments** on every query type: select, insert, update, delete, and grouped. Parameter values are embedded into the plan where they're supplied (inside `insert([...])`, a `where()` callback, or a [`param()`](#param) call), not passed to `build()`. There is no `build({ params })` form. +- `build()` takes **zero arguments** on every query type: select, insert, update, delete, and grouped. Parameter values are embedded into the built query where they're supplied (inside `insert([...])`, a `where()` callback, or a [`param()`](#param) call), not passed to `build()`. There is no `build({ params })` form. #### Return type | Return type | Example | Description | |---|---|---| -| Query plan | `db.sql.public.user.select('id').build()` | A plan you run with `runtime.query(...)`. Its per-row type is recoverable with [`ResultType`](#resulttype). | +| Built query | `db.sql.public.user.select('id').build()` | A query you run with `runtime.query(...)`. Its per-row type is recoverable with [`ResultType`](#resulttype). | -### Executing a plan +### Running a built query [#executing-a-plan] -Run a plan with `runtime.query(plan)`, where `runtime` is the runtime you got from connecting your database client (`const runtime = await db.connect()`). It returns an `AsyncIterableResult`: `await` it for an array of rows (`Row[]`), or `for await` over it to take one row at a time. `runtime.execute(plan)` is the separate call for writes that return no rows; it resolves to `{ affectedRows }`. +Run a built query with `runtime.query(plan)`, where `runtime` is `db.runtime()`. It returns an `AsyncIterableResult`: `await` it for an array of rows (`Row[]`), or `for await` over it to take one row at a time. `runtime.execute(plan)` is the separate call for writes that return no rows; it resolves to `{ affectedRows }`. ```ts const plan = db.sql.public.user.select('id', 'email').build(); @@ -991,17 +991,17 @@ const rows = await runtime.query(plan); // Row[] ### `ResultType` -Recover a plan's row type at the type level. +Recover a built query's row type at the type level. #### Remarks - 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`. +- `ResultType` is **one row**, 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 [Model and result types](/orm/reference/orm-client#model-and-result-types). #### Examples -##### Recover the row type from a plan +##### Recover the row type from a built query [#recover-the-row-type-from-a-plan] ```ts import type { ResultType } from '@prisma/orm-postgres/components/runtime'; @@ -1014,4 +1014,4 @@ const rows = await runtime.query(plan); // Row[] ### Streaming vs. collecting -`await runtime.query(plan)` collects every row into an array. This is the common case and what every example on this page uses. For large result sets that you'd rather process incrementally, use the ORM client's streaming terminals, documented under [`AsyncIterableResult`](/orm/reference/orm-client#asynciterableresult). +`await runtime.query(plan)` collects every row into an array. This is the common case and what every example on this page uses. For a large result set, loop the same result with `for await` to take one row at a time. Each result is read once, one way: see [`AsyncIterableResult`](/orm/reference/orm-client#asynciterableresult). Since `8.0.0-rc.10` a buffered query hands its pooled connection back before you read the rows, so a paused loop no longer blocks other queries; a cursor stream keeps its connection until it finishes. diff --git a/apps/docs/content/docs/orm/reference/transactions-and-runtime.mdx b/apps/docs/content/docs/orm/reference/transactions-and-runtime.mdx index 4faf32daef1..28052a8de3d 100644 --- a/apps/docs/content/docs/orm/reference/transactions-and-runtime.mdx +++ b/apps/docs/content/docs/orm/reference/transactions-and-runtime.mdx @@ -6,13 +6,9 @@ metaTitle: Prisma ORM transactions and runtime reference metaDescription: 'Reference for the Prisma ORM client lifecycle, transactions, prepared statements, and execution options.' --- -This page documents the client lifecycle (create, connect, close), transactions, prepared statements, and per-query execution options across PostgreSQL and MongoDB. Availability is stated per method: where the two databases differ, or where a surface exists on only one of them, the Remarks say so. +This page documents the client lifecycle (create, connect, close), transactions, prepared statements, and per-query execution options across PostgreSQL and MongoDB. Availability is stated per method: where the two databases differ, or where a method exists on only one of them, the Remarks say so. Transactions are fully supported on PostgreSQL and not shipped on MongoDB. -The transaction surface is rich on PostgreSQL and not shipped on MongoDB. Where MongoDB has no equivalent, this page says so plainly and links the workaround, rather than showing an API that does not exist. - -Every PostgreSQL example is transcribed from an executable test suite that runs against a live database. MongoDB behavior is verified the same way, including the absence probes that confirm a surface does not exist. - -For a task-oriented walkthrough, see the Fundamentals guide to [Transactions](/orm/fundamentals/transactions). For the query surfaces you run inside a transaction, see the [ORM client reference](/orm/reference/orm-client), the [SQL query builder reference](/orm/reference/sql-query-builder), and the [raw queries reference](/orm/reference/raw-queries). +For a task-oriented walkthrough, see the Fundamentals guide to [Transactions](/orm/fundamentals/transactions). For the ways to query inside a transaction, see the [ORM client reference](/orm/reference/orm-client), the [SQL query builder reference](/orm/reference/sql-query-builder), and the [raw queries reference](/orm/reference/raw-queries). ## Client lifecycle on PostgreSQL @@ -25,10 +21,10 @@ Create a PostgreSQL client. #### Remarks - Pass the contract with `contractJson` (a JSON contract you import) or `contract` (a contract value). Supply exactly one. -- Bind to a database in one of two ways: a connection string via `url`, or an existing `pg` `Pool` or `Client` instance via `pg`. When you pass a `pg` instance, you own its lifecycle; when you pass a `url`, the client creates and owns the pool. -- The internal pool defaults are `connectionTimeoutMillis: 20000` and `idleTimeoutMillis: 30000` (source-read from `postgres.ts`). Override them with `poolOptions`. +- Bind to a database in one of three ways: a connection string via `url`, an existing `pg` `Pool` or `Client` instance via `pg`, or a `binding` object that names its kind (`{ kind: 'url' | 'pgPool' | 'pgClient', ... }`). Pass exactly one. When you pass a `pg` instance, you own its lifecycle; when you pass a `url`, the client creates and owns the pool. +- When you pass a `url`, the pool it creates uses `connectionTimeoutMillis: 20000` and `idleTimeoutMillis: 30000`. Override them with `poolOptions`. `poolOptions` has no effect on a pool you supply yourself. - If your contract uses types from an extension pack, pass the pack in `extensions` (for example `extensions: [pgvector]`). -- `postgres(options)` does not open a connection. Call [`connect()`](#connect) to acquire a runtime. +- `postgres(options)` does not open a connection. The client opens one on first use, so [`connect()`](#connect) is optional: call it to open the connection up front and fail early. #### Options @@ -37,15 +33,16 @@ Create a PostgreSQL client. | `contractJson` / `contract` | JSON contract or contract value | Yes | The contract. Supply exactly one. | | `url` | `string` | One binding | A PostgreSQL connection string. | | `pg` | `Pool` or `Client` from the `pg` package | One binding | An existing `pg` instance to bind to. You own its lifecycle. | -| `poolOptions` | `{ connectionTimeoutMillis?: number; idleTimeoutMillis?: number }` | No | Overrides for the internal pool timeouts (defaults `20000` / `30000`). | +| `poolOptions` | `{ connectionTimeoutMillis?: number; idleTimeoutMillis?: number }` | No | Timeouts for the pool the client creates from `url` (defaults `20000` / `30000`). | | `extensions` | Array of extension packs | No | Runtime extension packs your contract's types need. | | `middleware` | Array of middleware | No | Middleware applied to every execution. | +| `verifyMarker` | `'onFirstUse'` or `false` | No | Whether the runtime reads the contract marker row on the first query and warns when it does not match. Defaults to `'onFirstUse'`. | #### Return type | Return type | Example | Description | |---|---|---| -| `PostgresClient` | `postgres({ contractJson, url })` | An unconnected client exposing `.orm`, `.sql`, `.enums`, `connect()`, `runtime()`, `transaction()`, `prepare()`, and `close()`. | +| `PostgresClient` | `postgres({ contractJson, url })` | An unconnected client exposing `.orm`, `.sql`, `.raw`, `.enums`, `.nativeEnums`, `.contract`, `.context`, `connect()`, `runtime()`, `transaction()`, `prepare()`, and `close()`. | #### Examples @@ -78,8 +75,10 @@ Open a connection and return a runtime. #### Remarks -- Returns a `Promise`. The runtime is what runs plans directly (`runtime.query(...)` for rows, `runtime.execute(...)` for writes that return none), opens connections (`runtime.connection()`), and prepares statements (`runtime.prepare(...)`). -- Calling `connect()` after the client has been closed rejects with a structured `DRIVER.NOT_CONNECTED` error (`Postgres client is closed`). +- Returns a `Promise`. The runtime is what runs built queries directly (`runtime.query(...)` for rows, `runtime.execute(...)` for writes that return none), opens connections (`runtime.connection()`), prepares statements (`runtime.prepare(...)`), and reports telemetry (`runtime.telemetry()`). +- `connect()` takes an optional binding (`{ url }`, `{ pg }`, or `{ binding }`), for when you did not give `postgres(...)` one. +- Calling `connect()` once the client has already connected rejects with an error whose `code` is `DRIVER.ALREADY_CONNECTED`. +- Calling `connect()` after the client has been closed rejects with an error whose `code` is `DRIVER.NOT_CONNECTED` (`Postgres client is closed`). #### Return type @@ -125,7 +124,7 @@ Close the client and release its pool. #### Remarks - Returns a `Promise`. -- After `close()`, calling `connect()` again rejects with `DRIVER.NOT_CONNECTED` (`Postgres client is closed`). +- After `close()`, `connect()` rejects and `runtime()` throws an error whose `code` is `DRIVER.NOT_CONNECTED` (`Postgres client is closed`). - When you bound the client with `pg` (your own pool or client), close that instance yourself; the client only owns pools it created from a `url`. #### Return type @@ -152,7 +151,7 @@ The client implements `Symbol.asyncDispose`, so `await using` closes it automati #### Remarks - Disposal fires at the end of the **block** the `await using` declaration lives in, not on the next line. Scope the client to the block where you need it. -- After the block exits, the client is closed the same way `close()` closes it: a later `connect()` rejects with `DRIVER.NOT_CONNECTED`. +- After the block exits, the client is closed the same way `close()` closes it: a later `connect()` rejects with an error whose `code` is `DRIVER.NOT_CONNECTED`. - `await using` requires `@types/node` (which augments `SymbolConstructor` with `asyncDispose`) or a `lib` that declares the symbol. #### Examples @@ -178,8 +177,8 @@ Create a MongoDB client. #### Remarks - Pass the contract with `contractJson` or `contract` (supply exactly one). -- Bind to a database in one of three ways: `url` (a `mongodb://` connection string, with an optional `dbName` to select or override the database, the form the examples on this page use), `uri` plus an explicit `dbName`, or `mongoClient` (an existing `MongoClient` from the `mongodb` package) plus an explicit `dbName`. -- **Client ownership** (source-read from `binding.ts` / `mongo-driver.ts`): with `url` or `uri`, Prisma ORM creates the underlying `MongoClient` and closes it on `close()`. With `mongoClient`, you supplied the client, so Prisma ORM does **not** close it: `close()` is a no-op on your client, and you close it yourself. This is what lets you share one `MongoClient` between Prisma ORM and driver-level code. +- Bind to a database in one of four ways: `url` (a `mongodb://` connection string, with an optional `dbName` to select or override the database, the form the examples on this page use), `uri` plus an explicit `dbName`, `mongoClient` (an existing `MongoClient` from the `mongodb` package) plus an explicit `dbName`, or a `binding` object that names its kind. Pass exactly one. +- **Client ownership**: with `url` or `uri`, Prisma ORM creates the underlying `MongoClient` and closes it on `close()`. With `mongoClient`, you supplied the client, so Prisma ORM does **not** close it: `close()` is a no-op on your client, and you close it yourself. This is what lets you share one `MongoClient` between Prisma ORM and driver-level code. - `mongo(options)` does not open a connection. The runtime is built lazily on first use, or explicitly via [`connect()`](#connect-1). #### Options @@ -191,6 +190,7 @@ Create a MongoDB client. | `uri` + `dbName` | `string` + `string` | One binding | A connection string plus an explicit database name. | | `mongoClient` + `dbName` | `MongoClient` + `string` | One binding | An existing `MongoClient` you own, plus the database name. | | `middleware` | Array of middleware | No | Middleware applied to every execution. | +| `mode` | `'strict'` or `'permissive'` | No | The mode middleware reads on every execution. Defaults to `'strict'`. | #### Return type @@ -230,8 +230,9 @@ Open a connection and return a runtime. #### Remarks - Returns a `Promise`. -- Calling `connect()` when the client is already connected rejects with `DRIVER.ALREADY_CONNECTED`. The first connection can happen implicitly, the moment any query builds the runtime, so a later explicit `connect()` collides the same way. PostgreSQL raises the same code from the same situation. -- After the client has been closed, `connect()` rejects with `DRIVER.NOT_CONNECTED` (`Mongo client is closed`). +- Calling `connect()` when the client is already connected rejects with an error whose `code` is `DRIVER.ALREADY_CONNECTED`. The first connection can happen implicitly, the moment any query builds the runtime, so a later explicit `connect()` collides the same way. PostgreSQL raises the same code from the same situation. +- `connect()` accepts a binding, so you can defer the choice of database: `mongo({ contractJson })` then `await db.connect({ url, dbName })`. +- After the client has been closed, `connect()` rejects with an error whose `code` is `DRIVER.NOT_CONNECTED` (`Mongo client is closed`). #### Return type @@ -245,7 +246,7 @@ Open a connection and return a runtime. ```typescript await db.connect(); -await db.connect(); // throws DRIVER.ALREADY_CONNECTED +await db.connect(); // rejects with DRIVER.ALREADY_CONNECTED ``` ### `runtime()` @@ -254,13 +255,9 @@ Get the runtime. #### Remarks -- On MongoDB, `runtime()` returns a **`Promise`**: you must `await` it. The underlying driver connection is built lazily and asynchronously on first use. +- On MongoDB, `runtime()` returns a **`Promise`**: you must `await` it. The underlying driver connection is built lazily and asynchronously on first use. PostgreSQL's `runtime()` is synchronous and returns a `Runtime` directly, so do not treat the two as the same shape: `await db.runtime()` on MongoDB, `db.runtime()` on PostgreSQL. -:::note[`runtime()` differs between databases] -PostgreSQL's `runtime()` is synchronous and returns a `Runtime` directly. MongoDB's `runtime()` is asynchronous and returns a `Promise`. Do not treat them as the same shape: `await db.runtime()` on MongoDB, `db.runtime()` on PostgreSQL. -::: - -- The `MongoRuntime` surface is `query`, `execute`, and `close` only. It has no `connection()`, `prepare()`, or `telemetry()` (see [Transactions (MongoDB)](#transactions-mongodb) and [Execution options and results](#execution-options-and-results)). +- `MongoRuntime` has `query`, `execute`, and `close` only. It has no `connection()`, `prepare()`, or `telemetry()` (see [Transactions (MongoDB)](#transactions-mongodb) and [Execution options and results](#execution-options-and-results)). #### Return type @@ -278,27 +275,27 @@ const runtime = await db.runtime(); ### `runtime.query()` -Run a plan through the runtime. +Run a built query through the runtime. #### Remarks -- `(await db.runtime()).query(plan)` runs any `MongoQueryPlan`, including a plan built by the [pipeline builder](/orm/reference/pipeline-builder) (`db.query`). The client itself has no `execute()` method. +- `(await db.runtime()).query(built)` runs any built MongoDB query, including one built by the [pipeline builder](/orm/reference/pipeline-builder) (`db.query`). The client itself has no `execute()` method. - Returns an [`AsyncIterableResult`](#asynciterableresult): `await` it for an array, or `for await` to stream rows. -- `runtime.execute(plan)` is the sibling call for writes that return no rows; it resolves the statement's statistics. +- `runtime.execute(built)` is the sibling call for writes that return no rows; it resolves the statement's statistics. #### Return type | Return type | Example | Description | |---|---|---| -| `AsyncIterableResult` | `await (await db.runtime()).query(plan)` | The plan's rows. | +| `AsyncIterableResult` | `await (await db.runtime()).query(built)` | The built query's rows. | #### Examples -##### Run a pipeline-builder plan +##### Run a query built by the pipeline builder [#run-a-pipeline-builder-plan] ```typescript -const plan = db.query.from('posts').build(); -const posts = await (await db.runtime()).query(plan); +const built = db.query.from('posts').build(); +const posts = await (await db.runtime()).query(built); ``` ### `close()` @@ -308,7 +305,7 @@ Close the client. #### Remarks - Returns a `Promise`. -- After `close()`, any further use throws `DRIVER.NOT_CONNECTED` (`Mongo client is closed`). This is verified against both `db.runtime()` and ORM access such as `db.orm.users.first()`, since both route through the same runtime. +- After `close()`, any further use rejects with an error whose `code` is `DRIVER.NOT_CONNECTED` (`Mongo client is closed`). That covers `db.runtime()` and ORM access such as `db.orm.users.first()`, since both route through the same runtime. - When you supplied a `mongoClient`, `close()` does not close your client (see [`mongo(options)`](#mongooptions)). #### Return type @@ -323,7 +320,7 @@ Close the client. ```typescript await db.close(); -await db.runtime(); // throws DRIVER.NOT_CONNECTED +await db.runtime(); // rejects with DRIVER.NOT_CONNECTED ``` ### `await using` (automatic disposal) @@ -333,7 +330,7 @@ The MongoDB client implements `Symbol.asyncDispose` too, so `await using` closes #### Remarks - Disposal fires at the end of the block, exactly as on PostgreSQL. -- After the block exits, a later `connect()` rejects with `DRIVER.NOT_CONNECTED`. +- After the block exits, a later `connect()` rejects with an error whose `code` is `DRIVER.NOT_CONNECTED`. #### Examples @@ -349,7 +346,7 @@ The MongoDB client implements `Symbol.asyncDispose` too, so `await using` closes ## Transactions (PostgreSQL) -A transaction groups several writes into one unit: they all commit together, or they all roll back. Prisma ORM offers three levels of control, from the high-level `db.transaction(...)` facade down to a manual connection you commit yourself. +A transaction groups several writes into one unit: they all commit together, or they all roll back. Prisma ORM offers three levels of control, from the high-level `db.transaction(...)` call down to a manual connection you commit yourself. ### `db.transaction(callback)` @@ -357,8 +354,9 @@ Run a callback inside a transaction. The transaction commits when the callback r #### Remarks -- Inside the callback, query through the `tx` handle, not `db`: `tx.orm` for models, `tx.sql` with `tx.query(...)` (rows) or `tx.execute(...)` (writes that return none) for SQL builder plans, and `tx.enums` for enum members (`tx.orm`, `tx.sql`, and `tx.execute` are verified by the test suite; `tx.enums` is confirmed from the client source). Every call on `tx` rides the same transaction connection; queries on `db` run outside the transaction. -- `tx.sql` is a full SQL builder, so it is namespace-qualified like `db.sql`: use `tx.sql.public.`. +- Inside the callback, query through the `tx` handle, not `db`: `tx.orm` for models, `tx.sql` with `tx.query(...)` (rows) or `tx.execute(...)` (writes that return none) for queries built with the SQL builder, and `tx.enums` and `tx.nativeEnums` for enum members. Every call on `tx` uses the same transaction connection; queries on `db` run outside the transaction. +- `db.transaction(...)` takes the callback and nothing else. There are no `isolationLevel`, `timeout`, or `maxWait` options, and `tx` has no `transaction()` method, so transactions do not nest. +- `tx.sql` is a full SQL builder, keyed the same way as `db.sql`: use `tx.sql.public.
`, where `public` is the PostgreSQL schema. - Reads inside the transaction see the transaction's own uncommitted writes. - The callback's return value passes through as the result of `db.transaction(...)`. - Do not let an [`AsyncIterableResult`](#asynciterableresult) escape the callback unread. A result you return and then read after the transaction has ended rejects with `RUNTIME.TRANSACTION_CLOSED` (see the Remark below). @@ -367,7 +365,7 @@ Run a callback inside a transaction. The transaction commits when the callback r | Name | Type | Required | Description | |---|---|---|---| -| `callback` | `(tx) => Promise` | Yes | The transactional work. `tx` exposes `orm`, `sql`, `query`, `execute`, and `enums`. | +| `callback` | `(tx) => Promise` | Yes | The transactional work. `tx` exposes `orm`, `sql`, `query`, `execute`, `enums`, and `nativeEnums`. | #### Return type @@ -411,7 +409,7 @@ const { createdId, found } = await db.transaction(async (tx) => { // found.id === createdId ``` -##### Run a SQL builder plan with `tx.sql` and `tx.execute` +##### Run a SQL builder query with `tx.sql` and `tx.execute` [#run-a-sql-builder-plan-with-txsql-and-txexecute] ```typescript await db.transaction(async (tx) => { @@ -422,7 +420,7 @@ await db.transaction(async (tx) => { ``` :::warning[An escaped `AsyncIterableResult` throws after the transaction ends] -An `AsyncIterableResult` is tied to the transaction connection. If you return one from the callback and read it after the transaction has committed, it rejects with `RUNTIME.TRANSACTION_CLOSED` before yielding any row. Collect the rows inside the callback (`await` the result there) and return the array instead. +An `AsyncIterableResult` is tied to the transaction connection. If you return one from the callback and read it after the transaction has committed, it rejects with an error whose `code` is `RUNTIME.TRANSACTION_CLOSED`, before yielding any row. Collect the rows inside the callback (`await` the result there) and return the array instead. ```typescript const escaped = await db.transaction(async (tx) => { @@ -452,12 +450,12 @@ The callback form does something the array form never could: one query's result ### `withTransaction(runtime, callback)` -A lower-level transaction helper you import directly, without going through the client facade. +A lower-level transaction helper you import directly, without going through the client. #### Remarks - Imported from `@prisma/orm-postgres/family-runtime`. -- The callback receives a bare transaction context whose surface is `query` and `execute` only. Unlike `db.transaction(...)`'s `tx`, it has no `.orm` or `.sql`; build plans with the client's `db.sql` (or a standalone SQL builder) and run them through `tx.query(...)` for rows or `tx.execute(...)` for writes that return none. +- The callback receives a bare transaction context whose only methods are `query` and `execute`. Unlike `db.transaction(...)`'s `tx`, it has no `.orm` or `.sql`; build queries with the client's `db.sql` (or a standalone SQL builder) and run them through `tx.query(...)` for rows or `tx.execute(...)` for writes that return none. - Commits on return, rolls back on throw, the same as `db.transaction(...)`. #### Options @@ -508,9 +506,9 @@ The lowest level: acquire a connection, open a transaction on it, commit or roll #### Remarks -- `runtime.connection()` returns a dedicated connection. `connection.transaction()` opens a transaction on it. Run plans with `transaction.query(...)` for rows or `transaction.execute(...)` for writes that return none, then call `transaction.commit()` or `transaction.rollback()`. +- `runtime.connection()` returns a dedicated connection. `connection.transaction()` opens a transaction on it. Run built queries with `transaction.query(...)` for rows or `transaction.execute(...)` for writes that return none, then call `transaction.commit()` or `transaction.rollback()`. - Always `release()` the connection when done, to return it to the pool. -- `connection.destroy(reason)` evicts that one connection from the pool instead of reusing it (source-read from `postgres-driver.ts`: it passes a truthy error to `pg`'s `PoolClient.release(err)`). It does not close the whole pool or the runtime; the runtime opens a replacement connection transparently. Use `release()` for the normal path and `destroy()` only to discard a connection you no longer trust. +- `connection.destroy(reason)` evicts that one connection from the pool instead of reusing it: Prisma ORM passes a truthy error to `pg`'s `PoolClient.release(err)`, which tells the pool to drop the client. It does not close the whole pool or the runtime; the runtime opens a replacement connection on the next query. Use `release()` for the normal path and `destroy()` only to discard a connection you no longer trust. `reason` is optional and advisory: the pool only reads whether it is set. #### Options @@ -520,7 +518,7 @@ The lowest level: acquire a connection, open a transaction on it, commit or roll | `connection.transaction()` | none | — | Returns a `Promise` of a transaction on that connection. | | `transaction.commit()` / `transaction.rollback()` | none | — | Commit or discard the transaction. | | `connection.release()` | none | — | Return the connection to the pool. | -| `connection.destroy(reason)` | `Error` | — | Evict this connection from the pool. | +| `connection.destroy(reason)` | `Error`, optional | — | Evict this connection from the pool. | #### Examples @@ -559,16 +557,16 @@ await connection.destroy(new Error('connection no longer trusted')); ## Transactions (MongoDB) :::note[MongoDB transactions are not available in Prisma ORM yet] -There is no `db.transaction(...)` on the MongoDB client, and the MongoDB runtime has no `connection()`, `prepare()`, or `telemetry()`. This is verified: probing each surface returns `undefined` and calling it throws `TypeError`. Single-document write operations are atomic on their own; multi-document transactions through Prisma ORM are not shipped. +There is no `db.transaction(...)` on the MongoDB client, and the MongoDB runtime has no `connection()`, `prepare()`, or `telemetry()`: it has `query`, `execute`, and `close` and nothing else. Single-document write operations are atomic on their own; multi-document transactions through Prisma ORM are not shipped. To run a multi-document MongoDB transaction today, use the MongoDB driver directly, sharing one `MongoClient` between Prisma ORM and your driver code. See the [Fundamentals transactions guide](/orm/fundamentals/transactions#transactions-on-mongodb) for the driver-session pattern. ::: -A transaction surface for MongoDB is planned (source-read from an internal design document, not shipped): a `db.transaction(fn)` facade with `tx.orm` and `tx.query`, backed by a driver session. As designed, it will require a MongoDB replica set and must not silently degrade to non-transactional execution on a standalone server. Treat all of this as planned design, not current behavior. Nothing in this planned surface exists in the shipped packages today, so this page shows no example code for it. +Transactions for MongoDB are planned, not shipped: a `db.transaction(fn)` method with `tx.orm` and `tx.query`, backed by a driver session. As designed, it will require a MongoDB replica set and must not silently degrade to non-transactional execution on a standalone server. Treat all of this as planned design, not current behavior. None of it exists in the shipped packages today. ## Prepared statements (PostgreSQL) -A prepared statement compiles a plan once against a declaration of its parameters, then runs it repeatedly with different values. Prepared statements are PostgreSQL only. +A prepared statement compiles a query once against a declaration of its parameters, then runs it repeatedly with different values. Prepared statements are PostgreSQL only. ### `runtime.prepare(declaration, callback)` @@ -576,17 +574,17 @@ Prepare a statement off a runtime. #### Remarks -- The declaration maps each parameter name to a codec id (for example `{ label: 'pg/text@1' }`). -- The callback receives the declared `params` and returns a plan. It takes a **single** argument (`(params) => plan`); build the plan with a SQL builder you already hold in scope (such as `db.sql`). -- A declared parameter that the callback never uses is rejected at prepare time with `RUNTIME.PREPARE_UNUSED_PARAM` (with `details.unused` listing the offending names). The rejection happens at `prepare()`, before any execution. +- The declaration maps each parameter name to a type id (for example `{ label: 'pg/text@1' }`). +- The callback receives the declared `params` and returns a built query. It takes a **single** argument (`(params) => plan`); build the query with a SQL builder you already hold in scope (such as `db.sql`). +- A declared parameter that the callback never uses is rejected at prepare time with an error whose `code` is `RUNTIME.PREPARE_UNUSED_PARAM`, with `details.unused` listing the offending names. The rejection happens at `prepare()`, before any execution. - The resulting `PreparedStatement` runs via `ps.query(target, params)`; see [`PreparedStatement.query`](#preparedstatementquerytarget-params). #### Options | Name | Type | Required | Description | |---|---|---|---| -| `declaration` | Object mapping param name to codec id | Yes | The statement's parameters. | -| `callback` | `(params) => SqlQueryPlan` | Yes | Builds the plan from the declared params. | +| `declaration` | Object mapping param name to type id | Yes | The statement's parameters. | +| `callback` | `(params) => SqlQueryPlan` | Yes | Builds the query from the declared params. | #### Return type @@ -627,19 +625,19 @@ await runtime.prepare({ label: 'pg/text@1', unused: 'pg/int4@1' }, (params) => ### `db.prepare(declaration, callback)` -Prepare a statement off the client facade. +Prepare a statement on the client itself. #### Remarks -- Same result as `runtime.prepare(...)`, but the callback receives **two** arguments, `(sql, params)`, because the client has no closed-over SQL builder for you to reach for. Use the injected `sql` to build the plan: `sql.public.
`. -- `prepare()` starts the connection itself, so calling `db.connect()` afterwards throws `DRIVER.ALREADY_CONNECTED`. Reach for the runtime with `db.runtime()` instead, which returns the one `prepare()` already built. +- Same result as `runtime.prepare(...)`, but the callback receives **two** arguments, `(sql, params)`, because the client has no closed-over SQL builder for you to reach for. Use the injected `sql` to build the query: `sql.public.
`. +- `prepare()` starts the connection itself, so calling `db.connect()` afterwards rejects with an error whose `code` is `DRIVER.ALREADY_CONNECTED`. Reach for the runtime with `db.runtime()` instead, which returns the one `prepare()` already built. #### Options | Name | Type | Required | Description | |---|---|---|---| -| `declaration` | Object mapping param name to codec id | Yes | The statement's parameters. | -| `callback` | `(sql, params) => SqlQueryPlan` | Yes | Builds the plan; `sql` is the client's SQL builder. | +| `declaration` | Object mapping param name to type id | Yes | The statement's parameters. | +| `callback` | `(sql, params) => SqlQueryPlan` | Yes | Builds the query; `sql` is the client's SQL builder. | #### Return type @@ -669,7 +667,7 @@ Run a prepared statement against a target, with the parameter values. #### Remarks -- A plan that returns rows prepares into a `PreparedStatement`, whose consumption method is `query(...)`. A plan terminated with `.affectedCount()` prepares into a `PreparedExecution` instead, consumed with `execute(target, params)`, which resolves the statement's statistics. +- A query that returns rows prepares into a `PreparedStatement`, whose consumption method is `query(...)`. A raw query ending with `.affectedCount()` prepares into a `PreparedExecution` instead, consumed with `execute(target, params)`, which resolves the statement's statistics. - The `target` is explicit: it can be a runtime, a connection, or a transaction. One prepared statement runs against any of them. - A single `PreparedStatement` prepared once runs against both a runtime target and a transaction target, seeing the transaction's writes when run inside it and the committed state when run against the runtime afterward. @@ -721,16 +719,16 @@ Per-query options for cancellation and scope. #### Remarks -- `signal` is an `AbortSignal` for per-query cancellation. A signal that is **already aborted** when you call `query(...)` short-circuits before any query runs, rejecting with `RUNTIME.ABORTED` and `details.phase` of `'stream'` (verified). Per the runtime source, the signal is also threaded through codec calls and checked between rows mid-stream. -- Aborting mid-stream (after rows have started arriving) is not separately verified in the validation harness: reliably landing an abort between two internal stream checks needs a very large result set or a fake-timer harness. The pre-aborted case above is verified. -- `scope` is `'runtime' | 'connection' | 'transaction'` (source-read from `runtime-middleware.ts`; not separately exercised by the validation harness). It selects the execution scope for the query. +- `signal` is an `AbortSignal` for per-query cancellation. A signal that is **already aborted** when you call `query(...)` short-circuits before any query runs, rejecting with an error whose `code` is `RUNTIME.ABORTED` and whose `details.phase` is `'stream'`. The error's `cause` is the signal's reason, exactly as you passed it to `controller.abort(...)`. +- The runtime also checks the signal between rows, and passes it into the calls that read each value. An abort that lands after rows have started arriving ends the stream with the same `RUNTIME.ABORTED` error, with a `details.phase` of `'stream'`, `'encode'`, or `'decode'` depending on where it landed. +- `scope` is `'runtime'`, `'connection'`, or `'transaction'`. It is the label middleware reads to tell a top-level query from one inside a dedicated connection or a transaction. The runtime sets it for you; setting it yourself changes only that label, not where the query runs. #### Options | Name | Type | Required | Description | |---|---|---|---| | `signal` | `AbortSignal` | No | Per-query cancellation signal. | -| `scope` | `'runtime' \| 'connection' \| 'transaction'` | No | The execution scope for the query. | +| `scope` | `'runtime' \| 'connection' \| 'transaction'` | No | The scope label middleware reads for this query. | #### Examples @@ -752,9 +750,9 @@ Read telemetry about the most recent query. #### Remarks -- PostgreSQL only. The MongoDB runtime has no `telemetry()` (probing it returns `undefined` and calling it throws `TypeError`). +- PostgreSQL only. `telemetry()` does not exist on the MongoDB runtime. - `telemetry()` returns `null` on a freshly-connected runtime, before any query has run. -- After a query, it returns an object of the shape `{ lane, target: 'postgres', fingerprint, outcome: 'success', durationMs? }`. It reflects only the **most recent** query, not a running history. +- After a query, it returns an object of the shape `{ lane, target: 'postgres', fingerprint, outcome, durationMs? }`, where `outcome` is `'success'` or `'runtime-error'`. It reflects only the **most recent** query, not a running history. #### Return type @@ -777,6 +775,8 @@ const after = runtime.telemetry(); ### `AsyncIterableResult` -Read terminals (`all()`, `createAll()`, and the runtime's `query(...)`) return an `AsyncIterableResult`: `await` it to collect an array, or `for await` to stream rows one at a time. +The calls that run a read query (`all()`, `createAll()`, and the runtime's `query(...)`) return an `AsyncIterableResult`: `await` it to collect an array, or `for await` to stream rows one at a time. + +On PostgreSQL, a query run through the client buffers its rows and hands its pooled connection back before you read them, so a result you hold or iterate slowly no longer keeps a connection checked out. A connection or transaction you opened yourself is unaffected: it stays yours until you release or commit it. -A result is consumed once per mode. Re-`await`ing a buffered result is safe (it returns the cached array), but switching between awaiting and iterating a consumed result throws `RUNTIME.ITERATOR_CONSUMED`. For the full single-consumption rules, shared identically by PostgreSQL and MongoDB, see [`AsyncIterableResult`](/orm/reference/orm-client#asynciterableresult) in the ORM client reference. +A result is consumed once per mode. Re-`await`ing a buffered result is safe (it returns the cached array), but switching between awaiting and iterating a consumed result throws an error whose `code` is `RUNTIME.ITERATOR_CONSUMED`. For the full single-consumption rules, shared identically by PostgreSQL and MongoDB, see [`AsyncIterableResult`](/orm/reference/orm-client#asynciterableresult) in the ORM client reference. diff --git a/docs/orm-docs-audit/c21-orm-reference/factcheck1.pipeline-builder.md b/docs/orm-docs-audit/c21-orm-reference/factcheck1.pipeline-builder.md new file mode 100644 index 00000000000..209010818fc --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-reference/factcheck1.pipeline-builder.md @@ -0,0 +1,70 @@ +Done. Report below. + +## Corrections made (page: `apps/docs/content/docs/orm/reference/pipeline-builder.mdx`) + +| Line (new) | Claim | Source that decides it | Verdict | What the page now says | +|---|---|---|---|---| +| 95 | `from()` takes "the lowercase plural collection name" | `query-builder/src/state-classes.ts:createCollectionHandle`; `contract-psl/src/interpreter.ts:1361` (`roots[collectionName]`) | Corrected | Takes the collection name from the `roots` map: the model's `@@map`, else the model name with a lowercase first letter | +| 96 | Unknown root error | `state-classes.ts` `ormError('ORM.MODEL_UNKNOWN', 'Unknown root: ...')`; `test/structured-error-codes.test.ts:53` | Corrected (wording only) | "throws right away, with an error whose `code` is `ORM.MODEL_UNKNOWN`" | +| 97 | Three builder states, root writes only before stages | `state-classes.ts` (CollectionHandle / FilteredCollection / PipelineChain) | Confirmed | — | +| 126 | `db.runtime()` returns a promise; `AsyncIterableResult`; no `db.execute` | `extensions/mongo/src/runtime/mongo.ts:38`; `framework-components/src/execution/async-iterable-result.ts:3` | Confirmed | — | +| 127–128 | "Read results are decoded"; only lookup sub-documents undecoded | `query-builder/src/pipeline-result-shape.ts:14,80`; `7-runtime/src/codecs/decoding.ts:53`; `mongo-runtime.ts:251` | **Corrected** | Decoding survives only `match`, `sort`, `limit`, `skip`, `sample`, `project`, `addFields`, `vectorSearch`. Every other stage (including `lookup()` and `group()`) drops the result shape and the **whole row**, top-level `_id` included, comes back raw | +| 150 | Filter values "not encoded to the stored type" | `2-mongo-adapter/src/resolve-value.ts:46-93` (only `MongoParamRef` with a codecId is encoded); ORM client wraps values in `MongoParamRef` (`orm/src/collection.ts:744`) | Confirmed | — | +| 151 | Filter operator list `eq ne gt gte lt lte in nin exists type` | `query-builder/src/field-accessor.ts:49-73` | Confirmed (exact match) | — | +| 190–193 | `_id` equality never matches; `ObjectId` walked as a plain object; `rawCommand` / ORM client are the escape hatches | `resolve-value.ts:73-93`; `orm/src/collection.ts:744` | Confirmed, still true at rc.10 | — | +| 202, 231, 237, 262, 287 | `sort`/`limit`/`skip`/`sample` signatures | `builder.ts:199-238` | Confirmed | — | +| 310, 344, 384–385 | `addFields`, `lookup(from().on().as())`, `project` two forms, implicit `_id` | `builder.ts:251,285,326`; `lookup-builder.ts` | Confirmed | — | +| 428–429 | `unwind()` "not exercised, verify it yourself" | `test/integration/test/mongo/query-builder.test.ts:395` runs `unwind('tags')` live | **Corrected** | Caveat removed; states the behaviour instead | +| 450–453 | `group()` single-argument callback; `acc` namespace; non-accumulator is "a compile error … throws at build time" | `builder.ts:395-430`; `types.ts:181` (the index signature allows a plain expression); `test/builder.test.ts:206` | **Corrected** | Throws when you call `group()`, `code` `ORM.ARGUMENT_INVALID`, exact message given; `null` for a non-`_id` key throws the same way. The "compile error" claim was dropped | +| 513 | `replaceRoot` rejects a bare scalar | MongoDB server message, not in the tree | **Q** | unchanged | +| 581 | `sortByCount` emits `{ _id, count }` sorted descending | `builder.ts:455`; integration test:514 | Confirmed | — | +| 612–616 | `redact()`: `fn.literal` fails; "no supported public pattern" for `$$KEEP` | `lowering.ts:37` (`fieldRef` → `'$'+path`), `:124` (`needsLiteralWrap`); `field-accessor.ts` `rawPath`; integration test:620 | **Corrected** | `fn.literal('KEEP')` emits the plain string and `fn.literal('$$KEEP')` is wrapped in `$literal`; both are rejected. The working route is `f.rawPath('$KEEP')` / `f.rawPath('$PRUNE')` inside `fn.cond(...)` | +| 624 | "The following stages take an options object: `unionWith` … `facet` …" | `builder.ts:535` (`unionWith(collection, pipeline?)`), `:588` (`facet(record)`) | **Corrected** | `unionWith` takes a collection name plus optional raw stages; `facet` takes a map of named raw sub-pipelines; the other seven take options objects | +| 625 | `bucketAuto`, `facet`, `setWindowFields` "not exercised" | integration test:447 (facet), :580 (bucketAuto), :666 (setWindowFields) | **Corrected** | Only `geoNear`, `densify`, `fill` are signature-only; the rest run live in Prisma ORM's test suite | +| 626 | Opaque expression fields | `builder.ts:546-645` (`MongoAggExpr` slots) | Confirmed; added where the classes are imported from | — | +| 643–650 | Atlas stages; `search(spec)`, `vectorSearch(spec)` | `builder.ts:646,655,664` | Corrected (additions) | `search`/`searchMeta` also take an optional index name; `vectorSearch` also takes `filter`; nothing in the builder refuses the stages off Atlas | +| 663 | "all nineteen MongoDB accumulators" | `accumulator-helpers.ts` — 19 exported, but MongoDB has more (`$mergeObjects`, `$accumulator`, `$percentile`, `$median`) | Corrected | "nineteen accumulators" | +| 665–685 | The 19 names and signatures | `accumulator-helpers.ts` | Confirmed (exact match, none missing, none extra) | Added that `sortBy` is a plain object and `n` is an expression | +| 774 | "`fn.*` mirrors MongoDB's operators; each is the camelCase name without `$`" | `expression-helpers.ts:139-519` | **Corrected** | 73 helpers, not all of MongoDB's; no `and/or/not/switch/ifNull/map/reduce/filter`; arithmetic stops at four; five are renamed (`firstElem`, `lastElem`, `isIn`, `typeOf`, `toString_`) | +| 789 | Named-argument helpers | `expression-helpers.ts:216,284,311,465` | Added | one new bullet | +| 790 | `fn.cond` condition must be `.node` | `expression-helpers.ts:180` (`condition: MongoAggExpr`); `filter-expressions.ts:217` (`visitor.expr`) | Confirmed | — | +| 792 | `fn.toObjectId(fn.literal(x))` throws `…reading 'codecId'` | `expression-helpers.ts:98` (`docUnaryExpr` reads `arg._field.codecId`), `:130` (`literal` sets `_field: undefined`) | Confirmed exactly | — | +| 860–861 | `pipe()` "not exercised"; "collapses the row shape" | integration test:713, 625 use `.pipe(...)` live; `builder.ts:679-687` (first overload keeps `Shape`) | **Corrected** | `pipe(stage)` keeps the current row type; `pipe` declares a new one; at run time the rows stop being decoded | +| 874–877 | `aggregate()` is an alias of `build()` | `builder.ts:849` (`return this.build()`) | Confirmed | — | +| 899–902 | Updater array rules and the three error strings | `update-ops.ts:199-227` | Confirmed; error codes added | Codes `ORM.MUTATION_DATA_MISSING` / `ORM.ARGUMENT_INVALID`; "as soon as you call the write method" instead of "at build time"; added the `Update spec collision` error | +| 908 | Root writes: `insertOne`, `insertMany`, `updateAll`, `deleteAll`, `upsertOne` | `state-classes.ts` `CollectionHandle` | Confirmed (exact) | — | +| 913 | Write result objects | `query-ast/src/result-types.ts`; `3-mongo-driver/src/mongo-driver.ts:117-175` | **Corrected** | Per-method shapes spelled out, including `insertedCount` and `matchedCount`/`upsertedCount` | +| 936 | `// { insertedIds: { '0': …, '1': … } }` | `mongo-driver.ts:144` (`Object.values(result.insertedIds)`) | **Corrected** | `// { insertedIds: [, ], insertedCount: 2 }` | +| 969 | Writes after `match()`: the seven methods | `state-classes.ts` `FilteredCollection` | Confirmed (exact) | — | +| 974 | `updateOne`/`deleteOne` at most one | `state-classes.ts` | Confirmed; added that the driver picks which, and that `findOneAndUpdate` after `sort()` is the ordered form | — | +| 975 | `findOneAndUpdate`/`findOneAndDelete` return values | `state-classes.ts` (plans carry no `resultShape`); `mongo-runtime.ts:251` | Added | The returned document is undecoded, so `_id` is a raw `ObjectId`; both vanish from the type after `skip()` and throw `ORM.OPERATION_UNSUPPORTED` if forced | +| 1018 | `returnDocument` default `'after'`; `returnNewDocument` invalid | `state-classes.ts` (`opts.returnDocument ?? 'after'`, excess-property check) | Confirmed; added the `upsert` option (default `false`) | — | +| 1058–1060 | Operator-form operator list (14 names) | `field-accessor.ts:74-92` | Confirmed (exact match) | — | +| 1059 | Pipeline form `f.stage.*` | `field-accessor.ts:139-146` | Confirmed; added the four emitters `set`, `unset`, `replaceRoot`, `replaceWith` | — | +| 1090–1092 | `out(collection)`, `merge({ into })` | `builder.ts:503,513` | Corrected (additions) | `out` takes an optional database name; `merge` takes `into` as a name or `{ db, coll }`, plus `on`, `whenMatched`, `whenNotMatched` | +| 1117 | `rawCommand()` takes a raw command, rows are `unknown` | `query.ts:29-59`; `test/raw-command.test.ts` | Corrected (wording) | It takes any command node, typed or raw, and does not translate it | +| 1126 | `RawAggregateCommand` import path | `9-public/@prisma/orm-mongo/package.json` exports `./query-ast/execution` | Confirmed | — | +| 9, 11, 17 | MongoDB only; no `find`/`distinct`; PostgreSQL has no `db.query` | `extensions/postgres/src/runtime/postgres.ts:50-61` (no `query`); no `find`/`distinct` on the builder | Confirmed | — | + +## Q list (unverifiable against `wip/prisma-src`) + +1. **"Every example is transcribed from an executable test suite that runs against a live MongoDB database" and the whole example schema (`User` / `Post` / `Article` / `Tutorial`).** The source's own pipeline-builder integration test uses a different fixture, `Product` / `Order` (`test/integration/test/mongo/query-builder.test.ts:36-130`). The page's harness is external to this checkout, so I cannot confirm or refute the provenance. What I could check does hold: root names are collection names, and a discriminated variant's root collapses into the base model's root (`contract-psl/src/interpreter.ts:487-493`), so `from('posts')` is the right call for that schema. Searches: `grep -rn "Tutorial\|Article\|@@discriminator" test/integration packages --include='*.ts'`, `ls test/integration/test/mongo/`. +2. **The "not exercised by the validation harness" and "verified empirically" statements** generally. These describe the docs team's own harness. I left them where the source also has no live test (`geoNear`, `densify`, `fill`, the Atlas stages) and removed them where the source does run the code live. +3. **`replaceRoot()`'s error text `'newRoot' expression must evaluate to an object`.** That string comes from the MongoDB server. Searches: `grep -rn "newRoot" packages --include='*.ts'`. +4. **`sortByCount()` "order of tied counts is not guaranteed".** MongoDB behaviour; the source test says the same in a comment (`query-builder.test.ts:524`) but does not prove it. + +## Things the source has that the page does not document + +Reported, not added, to keep the page at its original length: + +- `f('address.city')`, the callable form of the `f` argument for dot-paths into value objects, and `f.rawPath(...)` beyond the `redact()` use (`field-accessor.ts:160-210`). +- The no-argument `updateMany()` / `updateOne()` forms on a pipeline chain. The source says they are unreachable from user code (`builder.ts:711` comment), so leaving them out is right. +- `ObjectExpression`, the reduced operator set on a value-object path: `exists`, `eq(null)`, `ne(null)`, `set`, `unset` (`field-accessor.ts:104-113`). + +## Page-conventions last bullet + +It says the error-reference page is generated and out of scope. My slice does not touch it. + +## Line count + +Before: 1147. After: 1147. `check-plain.sh`: clean. `pnpm lint:links`: 0 errors. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/factcheck1.raw-queries.md b/docs/orm-docs-audit/c21-orm-reference/factcheck1.raw-queries.md new file mode 100644 index 00000000000..554601ffdcd --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-reference/factcheck1.raw-queries.md @@ -0,0 +1,52 @@ +Fact check complete. The page is 356 lines before and after. + +## Claims checked + +| Line | Claim | Deciding source | Verdict | +|---|---|---|---| +| 11 | Two escape hatches: `fns.raw`, `db.raw.sql`, `db.raw.collection(...)`, `db.query.rawCommand(...)` | `sql-builder/src/runtime/functions.ts:227`; `sql-builder/src/types/raw-query.ts:164`; `mongo-raw.ts:6`; `query-builder/src/query.ts:30` | Confirmed | +| 14 | Only a whole `db.raw.sql` statement converts results; `.returnsRow` converts; `fns.raw`'s `.returns()` is TypeScript only | `2-sql/5-runtime/src/codecs/decoding.ts:77-100` (row spec supplies codecs) vs `sql-builder/src/runtime/builder-base.ts:332` (projection codec comes from `returnType.codec`, which a raw expression has not got) | Confirmed | +| 14 | MongoDB raw results are native BSON; write methods return driver result objects | `mongo-runtime.ts:251` (no `resultShape`, row passes through); `mongo-driver.ts:118-198` | Confirmed | +| 21 | `fns.raw` usable in `select()`, `where()`, `orderBy()`, `update()` | `builder-base.ts:325,342,387,416`; `mutation-impl.ts:330` | Confirmed | +| 23 | `.returnsRow(spec).build()` → `runtime.query`; `.affectedCount().build()` → `runtime.execute` | `relational-core/src/expression.ts:460-484` | Confirmed | +| 25 | `db.sql.public` keyed by table name | conventions requirement | **Corrected**: added "which is keyed by table name (the model name with a lowercase first letter unless the model sets `@@map`)" | +| 28-33 | `postgres({ contractJson, url })`, `await db.connect()` | `3-extensions/postgres/src/runtime/postgres.ts:65,102,155` | Confirmed | +| 38-50 | `fns.raw` tagged template, `.returns(codecId)` compile-time only | `expression.ts:430-449`; integration test `test/integration/test/sql-builder/raw-sql.integration.test.ts:169` | Confirmed | +| 54 | A `.returns('pg/bool@1')` fragment satisfies `where()` alone | `sql-builder/src/expression.ts:12,38-41` (`BooleanCodecType`) | Confirmed | +| 67 | Interpolated `Expression` splices its AST, stays parameterized | `expression.ts:336-361` (`resolveInterpolation` → `value.buildAst()`) | Confirmed | +| 84 | Bare values work; `number`→`pg/int4@1`/`pg/int8number@1`/`pg/float8@1`, `bigint`→`pg/int8@1`, `string`→`pg/text@1`, `boolean`→`pg/bool@1`, `Uint8Array`→`pg/bytea@1` | `3-targets/6-adapters/postgres/src/core/adapter.ts:100-140`; integration `raw-query.integration.test.ts:118` uses bare `${1}` | Confirmed — **bare-scalar interpolation is not broken in rc.10**, so the earlier audit finding no longer holds and the page's current text is right | +| 84/87 | `param(value, { codecId })` from `@prisma/orm-postgres/relational-core/expression` | `relational-core/src/expression.ts:132`; `exports/expression.ts`; `@prisma/orm-postgres/package.json:127` | Confirmed | +| 100 | Object form of `.returns()` declares nullable | `expression.ts:434-449` | Confirmed | +| 118 | `db.raw` has one key, `sql` | `sql-builder/src/types/raw-query.ts:164` | Confirmed | +| 123 | `pg/timestamptz-temporal@1` is a real type id | codec ids in `3-targets/6-adapters/postgres/src` | Confirmed | +| 131-153 | `.returnsRow(spec)` entries: contract column or explicit type id; `user.columns.id` exists; `pg/int8@1` decodes to `bigint` | `types/raw-query.ts:29-63,115`; `table-proxy-impl.ts:95`; `raw-query.integration.test.ts:195` (`invitee_count: 2n`) | Confirmed | +| 156-168 | `.affectedCount()` takes no spec; `runtime.execute` returns `affectedRows` | `expression.ts:481`; `raw-query.integration.test.ts:140-148` | Confirmed | +| 162 | `SET "viewCount" = "viewCount" + 1` | The named example schema (sql-query-builder page) has no `viewCount` column on `Post` | **Corrected**: now `SET priority = ${'urgent'}` (`priority` is a real column of the example `Post` model, and the enum's storage type is `pg/text@1`, which a bare string binds to) | +| 171-196 | Row-returning raw queries compose into CTEs; `.returns.X` inherits a declaration | `expression.ts:378-386`; `raw-query.integration.test.ts:151-200` | Confirmed | +| 198 | `.affectedCount()` result cannot be interpolated | `expression.ts:304` (`RawAffectedCountQuery` has only `build()`); `RawSqlInterpolation` at `:334` | Confirmed | +| 202-208 | Accepted interpolations; `Date` throws `RUNTIME.RAW_SQL_UNSUPPORTED_INTERPOLATION` synchronously; TypeScript rejects it first | `ast/types.ts:44` (`RawSqlLiteral`); `expression.ts:356-360`; thrown from `templateParts` at tag-call time | Confirmed | +| 205 | Error message text | `expression.ts:358-359` and `adapter.ts:135-138`, identical strings | Confirmed | +| 212 | `db.raw.collection(rootName)`, nine methods, root name is the contract root | `raw-collection.ts:20-40`; `mongo-raw.ts:16-35` | Confirmed | +| 214 | `.build()` then `(await db.runtime()).query(plan)` | `raw-collection.ts:46-50`; `3-extensions/mongo/src/runtime/mongo.ts:33,38` | **Corrected**: added that `db.runtime()` returns a promise on MongoDB and is synchronous on PostgreSQL (conventions ask for this where it first appears) | +| 226-238 | `aggregate(pipeline)` | `raw-collection.ts:21,53` | Confirmed | +| 240-263 | `insertOne` → `{ insertedId }`; `insertMany` → `{ insertedCount, insertedIds }`, ids reshaped from the driver's index-keyed map | `mongo-driver.ts:118-145` (`Object.values(result.insertedIds)`) | Confirmed | +| 267 | `updateOne`/`updateMany` argument shapes and `{ matchedCount, modifiedCount, upsertedCount, upsertedId }` | `raw-collection.ts:65-71`; `mongo-driver.ts:124-158` | Confirmed | +| 293-305 | `deleteOne`/`deleteMany` → `{ deletedCount }` | `mongo-driver.ts:160-173` | Confirmed | +| 309 | `findOneAndUpdate` third argument `{ upsert }` | `raw-collection.ts:81-89` | Confirmed | +| 313 | `sort` and `returnDocument` "silently dropped, not a type or runtime error" | `raw-collection.ts:33-37` — the options type is `{ upsert?: boolean }`, so TypeScript's excess-property check rejects an options literal naming either key | **Corrected**: now says TypeScript rejects an options literal that names either key, and an untyped value is dropped without a run-time error; also names the direct route (`new RawFindOneAndUpdateCommand(...)` through `db.query.rawCommand(...)`), which the exports confirm | +| 314 | `returnDocument` never forwarded, driver default `'before'` applies | `mongo-driver.ts:176-186` (option omitted when undefined); `raw-commands.ts:106-111` | Confirmed | +| 318 | First upsert call returns empty; driver "yields `null`" | `mongo-driver.ts:183-185` — the driver returns `null` and the generator yields nothing | **Corrected**: "the driver returns `null`, and the query yields no row" | +| 336 | `findOneAndDelete` yields the raw matched document, not a wrapper | `mongo-driver.ts:188-198` | Confirmed | +| 346-354 | `db.query.rawCommand(command)` returns a plan with no `.build()`; rows typed `unknown`; `RawAggregateCommand` from `@prisma/orm-mongo/query-ast/execution` | `query-builder/src/query.ts:30,44-59`; `query-ast/src/exports/execution.ts:41-51`; `@prisma/orm-mongo/package.json` exports | Confirmed | + +## Q (unverifiable) + +- Line 290 `// { matchedCount: 2, modifiedCount: 2, ... }` and line 304 `// { deletedCount: 2 }`. These counts depend on seed data that neither this page nor the pipeline builder page shows. Searches run: `grep -rn "findOneAndUpdate" --include='*.test.ts' .`, `grep -rn "rawCollection\|mongoRaw\|\.collection(" --include='*.ts' test/integration/ packages/2-mongo-family/5-query-builders/orm/test/`, and a read of `packages/2-mongo-family/5-query-builders/orm/test/mongo-raw.test.ts` (which only checks the built command, never runs it). No live-database test exercises `db.raw.collection(...)` writes. +- Line 47 `// rows === [{ id: aliceId, upperEmail: 'ALICE@EXAMPLE.COM' }]` and the other example-schema result comments. The live tests use a different fixture (`users`/`posts` with int4 ids, `test/integration/test/sql-builder/fixtures/contract.ts`), not the schema this page names. + +## Notes for the operator + +- **The page's "every example is transcribed from an executable test suite" sentence is weaker than it reads.** The PostgreSQL raw tests do run against a live database, but against the `users`/`posts`/`comments` fixture in `wip/prisma-src/test/integration/test/sql-builder/fixtures/contract.ts`, not the `user`/`post` schema this page borrows from the SQL query builder page. The MongoDB raw collection methods have no live-database test at all; only the built command shape is tested. Every MongoDB example on this page is derived from the code, not from a test run. +- **No test covers the upsert-counter behaviour** described at lines 316-332. The driver tests (`packages/3-mongo-target/3-mongo-driver/test/mongo-driver.test.ts:176-230`) only pass `returnDocument: 'after'` explicitly. The page's claim follows from `mongo-driver.ts:176-186`, where the option is omitted when undefined. +- **The word "plan" appears 31 times**, and `conventions.md` bans it in favour of "the built query". I left it alone because the brief said to keep the banned-term agent's wording and the check script passes clean. If you want it removed, that is a separate pass. +- The bare word "codec" survives once, at line 205, inside the verbatim error message from the source. It has to stay. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/factcheck1.sql-query-builder.md b/docs/orm-docs-audit/c21-orm-reference/factcheck1.sql-query-builder.md new file mode 100644 index 00000000000..ec457767e50 --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-reference/factcheck1.sql-query-builder.md @@ -0,0 +1,71 @@ +Done. Report below. + +## 1. Claims checked + +| Line | Claim | Source that decides it | Verdict | Now says | +|---|---|---|---|---| +| 14 | SQL builder targets PostgreSQL and SQLite; MongoDB has none | `packages/3-targets/6-adapters/{postgres,sqlite}/src/core/adapter.ts`; no SQL builder under `2-mongo-family` | Confirmed | — | +| 19 | "every example is transcribed from an executable test suite that runs against a live PostgreSQL database" | `test/integration/test/sql-builder/setup.ts:41-95` seeds `users`/`posts`/`comments`/`profiles` with `int4` ids, not the page's `user`/`post`/`tag`/`post_tag` | **Corrected** | The test-suite claim is removed. Now: "All examples on this page run against the following schema." | +| 25-125 | Example schema | Matches `examples/prisma-8-demo/src/prisma/contract.prisma` except `Priority`, which is `pg/int4@1` with numeric values there. The page's text form matches the finished orm-client page's schema exactly, so the page is self-consistent | Confirmed (as the page's own schema) | — | +| 131 | Table accessors use "mapped table names (snake_case)" | `packages/2-sql/2-authoring/contract-psl/src/psl-attribute-parsing.ts:8` `lowerFirst`; used at `psl-field-resolution.ts:727` | **Corrected** | Default table name is the model name with a lowercase first letter unless `@@map`; column names are field names unless `@map`; `public` is the PostgreSQL schema | +| 135, 143 | Runtime comes from `await db.connect()` | `packages/3-extensions/postgres/src/runtime/postgres.ts:65-66, 289-328`. `connect()` exists but throws `DRIVER.ALREADY_CONNECTED` on a second call; `runtime()` is synchronous and is what the finished orm-client page uses | **Corrected** | "take a runtime from `db.runtime()` ... On PostgreSQL `db.runtime()` is synchronous, so there is nothing to await"; code uses `const runtime = db.runtime();` | +| 149-155 | `db.sql` is the result of `sql({ context, rawCodecInferer })`, importable from `@prisma/orm-postgres/builder/runtime` | `packages/2-sql/4-lanes/sql-builder/src/runtime/sql.ts:11-19`; `runtime/index.ts` exports `sql`; `@internal/sql-builder` exports `./runtime`; `orm-postgres/package.json` exports `./builder/runtime` | Confirmed (see note 3) | — | +| 154 | `inferCodec: () => 'pg/text'` | `packages/3-targets/3-targets/postgres/src/core/codec-ids.ts:8` (`pg/text@1`); `packages/3-targets/6-adapters/postgres/src/core/adapter.ts:129` | **Corrected** | `'pg/text@1'` | +| 187 | `f` is one property per column; joined columns under the table name; `fns` is the second argument | `runtime/field-proxy.ts` (whole file) | Confirmed | — | +| 195-201 | Three `select()` forms | `src/types/shared.ts` `WithSelect` | Confirmed | — | +| 199 | Unknown column name error | `runtime/builder-base.ts:310` throws `ORM.COLUMN_UNKNOWN`, message `Column "x" not found in scope` | **Corrected** | "throws an error whose `code` is `ORM.COLUMN_UNKNOWN`" | +| 255 | `where(predicate)` | `src/types/select-query.ts:38` | Confirmed | — | +| 288-296 | `innerJoin(other, on)`, other may be a table or an aliased subquery | `src/types/shared.ts` `WithJoin`; `runtime/joined-tables-impl.ts:168-190` | Confirmed | — | +| 313-317 | `outerLeftJoin`/`outerRightJoin`/`outerFullJoin` null semantics | `WithJoin` + `NullableScope` in `src/scope.ts:69-77` | Confirmed | — | +| 339 | "An adapter that does not has no `lateralJoin()` method" | `src/scope.ts:31` `GatedMethod` is type-level only; `runtime/builder-base.ts:280-293` throws `ORM.CAPABILITY_MISSING`; `test/e2e/framework/test/sqlite/sql-builder.test.ts:335-342` | **Corrected** | PostgreSQL only; on SQLite TypeScript types it `never`, and calling it anyway throws an error whose `code` is `ORM.CAPABILITY_MISSING`. Also added one sentence naming `outerLateralJoin()`, which the page had omitted | +| 340 | `lateral.from(otherTable)` then the usual SELECT chain | `src/types/shared.ts` `LateralBuilder`; `joined-tables-impl.ts:192-210` | Confirmed | — | +| 341 | `.as()` inside the callback throws `subquery.getRowFields is not a function` | `joined-tables-impl.ts:213` calls `subquery.getRowFields()`; `runtime/query-impl.ts:142-158` shows `.as()` returns only `getJoinOuterScope`/`buildAst` | Confirmed | — | +| 342 | Ambiguous names are dropped from the bare scope | `runtime/builder-base.ts:235-247`; `MergeScopes` in `src/scope.ts:56-61` | Confirmed | — | +| 388-390 | `orderBy` options `direction` and `nulls` | `src/expression.ts:46-49` declares both, but `runtime/builder-base.ts:357-390` reads only `options?.direction`. `OrderByItem` (`relational-core/src/ast/types.ts:1080-1098`) carries no null placement, and no renderer emits `NULLS FIRST`/`NULLS LAST` (grep over all of `packages` finds the strings nowhere) | **Corrected** | The `nulls` row now says TypeScript accepts it but nothing reads it, so no `NULLS FIRST`/`NULLS LAST` reaches the SQL and the database's own null ordering applies. The example keeps its heading and code, with a trailing comment saying `nulls` adds nothing to the SQL | +| 435, 466 | `distinct()` and `distinctOn()` return types | `src/types/shared.ts` `WithDistinct`; `src/types/select-query.ts:63-78` | Confirmed | — | +| 453 | `distinctOn()` availability wording | `select-query.ts:64` gates on `{ postgres: { distinctOn: true } }`; SQLite adapter has no `postgres` group (`adapters/sqlite/src/core/adapter.ts:76-85`); `sqlite/sql-builder.test.ts:344-352` | **Corrected** | PostgreSQL only; `DISTINCT ON` is a PostgreSQL extension to SQL; on SQLite it does not compile and throws `ORM.CAPABILITY_MISSING` if called | +| 454 | `distinctOn` ordering is not type-enforced | `runtime/builder-base.ts:418-446` does no order check | Confirmed | — | +| 491-492 | `limit(n)`/`offset(n)` take `number` | `src/types/shared.ts` `PaginationValue` = `number \| TraitExpression<['numeric']>` | **Corrected** | "`number`, or a numeric expression" | +| 516-528 | `.as(alias)` gives a join source, not a buildable query | `runtime/query-impl.ts:142-158` | Confirmed | — | +| 549-550, 693 | `GroupedQuery` has `having()`, aggregates, `orderBy`, `limit`, `offset`, `distinct`, `distinctOn` | `src/types/grouped-query.ts` | Confirmed | — | +| 584 | Aggregate decode note: `count`/integer `sum`/integer `avg` give `number`; out-of-range raises `RUNTIME.DECODE_FAILED`; `avg` is `float8` and unguarded; `countBigInt`/`sumBigInt`/`avgDecimal` | `postgres/src/core/aggregates.ts:5-6, 160-215`; `codec-helpers.ts:152-197` | Confirmed | — | +| 593-596 | `groupBy` forms | `src/types/select-query.ts:52-62`; `builder-base.ts:394-416` | Confirmed | — | +| 629 | `EXTRACT` result is a string because a raw expression is not converted | `builder-base.ts:317-334` passes `field.codec`, which a `.returns()` result does not carry (`relational-core/src/expression.ts:443-448`) | Confirmed (reworded only, to drop a banned term) | "Prisma ORM does not convert what a raw expression returns" | +| 634-638 | `having()` builds from aggregates and compares against a JavaScript literal, evaluated server-side | `grouped-query.ts:53-58`; `functions.ts:126-148` comment; `PgInt8NumberCodec.encode(value: number)` at `codecs.ts:707-716` | Confirmed | — | +| 721-722 | `insert()` always takes an array; multiple rows in one statement | `src/types/table-proxy.ts` `insert(rows: ReadonlyArray<...>)`; `adapters/postgres/src/core/sql-renderer.ts:913` | Confirmed | — | +| 728 | Insert values are sent as query parameters | `relational-core/src/expression.ts:336-360` | Confirmed | — | +| 763 | `returning()` availability | `src/types/mutation-query.ts:12` gates on `{ sql: { returning: true } }`; SQLite sets `returning: true` (`adapters/sqlite/src/core/adapter.ts:82`); `sqlite/sql-builder.test.ts:354-360` asserts it is **not** `never` | **Corrected** | "PostgreSQL and SQLite. Both support SQL's `RETURNING`." Plus the `never`/`ORM.CAPABILITY_MISSING` behaviour for an adapter without it | +| 796-799 | `update()` values-object and callback forms; `where()` on update/delete; no `where()` on insert | `src/types/table-proxy.ts`; `src/types/mutation-query.ts` | Confirmed | — | +| 841 | `delete()` returns a `DeleteQuery` | `src/types/table-proxy.ts` | Confirmed | — | +| 863-872 | `param(value, { codecId })` returns a `ParamRef`, imported from `@prisma/orm-postgres/relational-core/expression` | `relational-core/src/expression.ts:132-134`; `src/exports/expression.ts`; `orm-postgres/package.json` exports `./relational-core/expression` | Confirmed | — | +| 865 | Comparison values are parameterized from the neighbouring column | `runtime/functions.ts:46-96` (`resolveOperand` threads the column's type) | Confirmed | — | +| 866, 975 | No `build({ params })`; `build()` takes zero arguments everywhere | `src/types/shared.ts` `WithBuild`; `mutation-query.ts` `build()` | Confirmed | — | +| 906-910 | Built-in function list: `eq ne gt gte lt lte`, `in notIn`, `and or`, `exists notExists`, `raw` | `runtime/functions.ts:203-228` `createBuiltinFunctions` — exactly these thirteen | Confirmed | — | +| 912 | Aggregates come from the contract's aggregate map; PostgreSQL contributes eight | `postgres/src/core/aggregates.ts:157-227`; `runtime/sql.ts:21-30` | Confirmed | — | +| 916-923 | Per-aggregate return types | Same file | **Corrected** (two rows) | `sum(field)`: "`number` over an integer column; over any other column, the type PostgreSQL's own `sum` produces for it" (the old wording missed `time`/`interval` → `interval`). `min`/`max`: added "a `varchar` column widens to `text`" (`aggregates.ts:137-152`) | +| 925 | `ilike` from the Postgres adapter for textual columns; `cosineDistance` from pgvector | `adapters/postgres/src/core/descriptor-meta.ts:153-169`; `extensions/pgvector/src/core/descriptor-meta.ts:19` | Confirmed | — | +| 927-929 | No `fns.coalesce` or `fns.cast` | `runtime/functions.ts:203-228` | Confirmed | — | +| 933 | `fns.raw` parameterizes each interpolation; bare values get a type from their JavaScript type; `.returns(codecId)` declares the result type | `relational-core/src/expression.ts:336-360, 420-448` | Confirmed | — | +| 944 | `.returns()` is a type annotation only, no SQL cast, no conversion on read | `expression.ts:443-448` returns `{ returnType: { codecId, nullable } }` with no codec instance; `builder-base.ts:317-334` | Confirmed | — | +| 985 | `runtime.query` returns an `AsyncIterableResult`; `runtime.execute` resolves to `{ affectedRows }` | `relational-core/src/runtime-scope.ts:23-26`; `ast/driver-types.ts:24-26`; `mutation.test.ts:109` asserts `{ affectedRows: 1 }` | Confirmed (the `db.connect()` aside was corrected, see line 135) | — | +| 998-1000 | `ResultType` import path; one row not an array | `orm-postgres/package.json` `./components/runtime`; matches the finished orm-client page | Confirmed | — | +| 1001 | Since rc.10 `ResultType` also works on ORM queries | `docs/releases/v8.0.0-rc.10.md:27` | Confirmed | — | +| 1017 | Streaming versus collecting | `runtime-scope.ts:24`; `docs/releases/v8.0.0-rc.10.md:47` | **Corrected** | Now says to loop the same result with `for await`, that a result is read once one way, and records the rc.10 change: a buffered query hands its pooled connection back before the rows are read; a cursor stream keeps its connection until it finishes | + +## 2. Q (unverifiable) + +- **Line 244** `// rows === [{ id: aliceId, upperEmail: 'ALICE@EXAMPLE.COM', emailLength: 17 }]` and the other result comments (lines 308, 330, 375, 444, 480, 664, 687-688, 708, 785, 818, 830, 854). Searched `test/integration/test/sql-builder/*.test.ts` for these values and for the page's table names (`grep -rn "post_tag\|displayName\|'hp'\|latestPost" test/integration/test/sql-builder`). Nothing matches: those tests use a different fixture. The comments are arithmetically consistent with the page's own schema and seed values, so I left them, but nothing in prisma/orm confirms them. +- **Line 687** `avgDecimal(f.amount) would give the exact '300.0000000000000000'`. The type is confirmed (numeric decodes to a string, `codecs.ts:896-906`), the exact digit count is not. Searched `grep -rn "avgDecimal" packages test` — no test asserts a scale for an `avg` over `int4`. +- **Line 444** `// distinct priorities: ['high', 'low', 'urgent']` depends on the page's own seed data, which does not exist in the source. + +## 3. What page-conventions asked for + +- **`db.sql` keyed by table name, said once where `db.sql` first appears**: done at line 131, and corrected from "snake_case" to "the model name with a lowercase first letter unless `@@map`". +- **`db.runtime()` stated where it first appears**: done at line 135. +- **`sql({ context, rawCodecInferer })` paragraph (lines 149-157)**: every name is real and every one is publicly exported. `sql` and `SqlOptions` come from `@internal/sql-builder/runtime`, which the public `@prisma/orm-postgres` package republishes at `./builder/runtime`; `context` is `db.context` on the client (`PostgresClient.context`, `postgres.ts:61`); `RawCodecInferer` is `{ inferCodec(value): string }`. So nothing here is private API. It is still a low-level assembly path that no test or example in prisma/orm uses outside the test harness itself, and the paragraph is the densest on the page. **Recommendation for the reader round: cut lines 149-157 down to one sentence, or drop them.** I left them in place because the facts hold. +- **Banned terms**: `check-plain.sh` reports clean. The only remaining hits for the extra list are the pinned anchor `[#the-dbsql-facet]` (kept, as required) and `plan` as a variable name inside code blocks (prose says "the built query"). +- **Headings and anchors**: none renamed, renumbered, or reordered. No new sections. + +## 4. Line count + +Before: 1,017. After: 1,017. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/factcheck1.transactions-and-runtime.md b/docs/orm-docs-audit/c21-orm-reference/factcheck1.transactions-and-runtime.md new file mode 100644 index 00000000000..98664c3f88d --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-reference/factcheck1.transactions-and-runtime.md @@ -0,0 +1,78 @@ +Fact check complete for `apps/docs/content/docs/orm/reference/transactions-and-runtime.mdx`, against `wip/prisma-src/` at 8.0.0-rc.10. + +## 1. Claims checked + +| Line | Claim | Source that decides it | Verdict | Text now | +|---|---|---|---|---| +| 9–13 | Intro: page scope; "every PostgreSQL example is transcribed from an executable test suite"; "MongoDB behavior is verified the same way" | No fixture in the tree uses a `Tag` model with a `label` field (`test/integration/test/sql-orm-client/fixtures/mn-psl/contract.prisma:26` has `Tag { id, name }`) | Corrected | Dropped both process claims. Intro now ends "Transactions are fully supported on PostgreSQL and not shipped on MongoDB." | +| 28 | `postgres()` binds "in one of two ways" (`url`, `pg`) | `packages/3-extensions/postgres/src/runtime/binding.ts:16-31, 67-82` | Corrected | "one of three ways": `url`, `pg`, or a `binding` object naming its kind. Pass exactly one. | +| 29 | Pool defaults `20000` / `30000`, "source-read from `postgres.ts`" | `.../runtime/postgres.ts:141-142` | Corrected | Defaults confirmed; "source-read" dropped. Added that `poolOptions` applies only to a pool the client creates from `url` (`postgres.ts:132-135`). | +| 31 | `postgres(options)` does not connect; "call `connect()` to acquire a runtime" | `postgres.ts:222-262` (`getRuntime()` connects lazily); `packages/3-extensions/postgres/test/postgres-close.test.ts:146-154` calls `db.runtime()` with no `connect()` | Corrected | "The client opens one on first use, so `connect()` is optional: call it to open the connection up front and fail early." | +| 35–42 | `postgres` options table | `postgres.ts:76-109` | Corrected | Added `verifyMarker` (`'onFirstUse'` or `false`, default `'onFirstUse'`; `packages/2-sql/5-runtime/src/runtime-spi.ts:26-43`). Reworded `poolOptions`. `binding` covered in the Remarks instead of a table row, to hold the line count. | +| 48 | `PostgresClient` exposes `.orm`, `.sql`, `.enums`, `connect`, `runtime`, `transaction`, `prepare`, `close` | `postgres.ts:56-74` | Corrected | Added `.raw`, `.nativeEnums`, `.contract`, `.context`. | +| 81 | `connect()` returns `Promise`; runtime runs queries, opens connections, prepares | `postgres.ts:289-324`; `packages/2-sql/5-runtime/src/sql-runtime.ts:100-113` | Corrected | Added `runtime.telemetry()` to the list, plus the optional binding argument (`postgres.ts:305-307`). | +| 82 | `connect()` after close rejects `DRIVER.NOT_CONNECTED` (`Postgres client is closed`) | `postgres.ts:291-295`; `postgres-close.test.ts:140-144` | Confirmed | — | +| new | `connect()` twice rejects `DRIVER.ALREADY_CONNECTED` | `postgres.ts:298-303` | Corrected (added) | Stated in `connect()` Remarks; the page previously only implied it from the MongoDB section. | +| 105 | PostgreSQL `runtime()` is synchronous | `postgres.ts:66, 326-328` | Confirmed | — | +| 128 | After `close()`, `connect()` rejects `DRIVER.NOT_CONNECTED` | `postgres.ts:223-229, 291-295`; `postgres-close.test.ts:134-144` | Corrected | Now also names `runtime()`, which throws the same code. | +| 129 | You close your own `pg` instance | `postgres.ts:199-207, 374-379` (only a `url` binding sets `ownedDispose`) | Confirmed | — | +| 150–156 | `Symbol.asyncDispose`, disposal at end of block, closed afterwards | `postgres.ts:381-383`; `postgres-close.test.ts:146-154` | Confirmed | Error-code wording normalised. | +| 156 | `await using` needs `@types/node` or a `lib` declaring the symbol | — | **Q** | See section 2. | +| 181 | `mongo()` binds "in one of three ways" | `packages/3-extensions/mongo/src/runtime/binding.ts:13-41, 82-95` | Corrected | "one of four ways", adding the `binding` object. | +| 182 | Client ownership, "source-read from `binding.ts` / `mongo-driver.ts`" | `.../runtime/mongo.ts:131-145, 220-235` | Corrected | Fact confirmed; "source-read" dropped. | +| 183 | `mongo()` does not connect; runtime built lazily | `mongo.ts:147-169` | Confirmed | — | +| 187–193 | `mongo` options table | `mongo.ts:43-57` | Corrected | Added `mode` (`'strict'` / `'permissive'`, default `'strict'`; `packages/2-mongo-family/7-runtime/src/mongo-runtime.ts:120`). `binding` covered in Remarks. | +| 199 | `MongoClient` exposes `.orm`, `.query`, `.raw`, `.contract`, `.enums`, `.context`, `connect`, `runtime`, `close` | `mongo.ts:28-41` | Confirmed | — | +| 233 | Second `connect()` rejects `DRIVER.ALREADY_CONNECTED`; first connection can be implicit | `mongo.ts:197-214`; `packages/3-extensions/mongo/test/mongo.test.ts:229-243` | Confirmed | Error wording normalised; added that `connect()` accepts a binding (`mongo.test.ts:236-243`). | +| 234 | After close, `connect()` rejects `DRIVER.NOT_CONNECTED` (`Mongo client is closed`) | `mongo.ts:198-199`; `mongo.test.ts:414-422` | Confirmed | — | +| 257 | MongoDB `runtime()` returns a promise | `mongo.ts:38, 216-218` | Confirmed | The duplicate `:::note` was folded into the bullet (same facts, two fewer lines). | +| 263 | `MongoRuntime` has `query`, `execute`, `close` only | `packages/2-mongo-family/7-runtime/src/mongo-runtime.ts:76-101` | Confirmed | — | +| 285–301 | `(await db.runtime()).query(...)`; client has no `execute()`; `db.query.from('posts').build()` | `mongo.ts:28-41`; `packages/2-mongo-family/5-query-builders/query-builder/src/query.ts:27-29`; `.../state-classes.ts:99-108` | Confirmed | Example variable renamed `plan` → `built` per the conventions. | +| 311 | After close, `db.runtime()` and ORM access reject `DRIVER.NOT_CONNECTED` | `mongo.ts:147-150, 171-182`; `mongo.test.ts:405-412` | Confirmed | Wording normalised ("rejects", not "throws"). | +| 360 | `tx` offers `orm`, `sql`, `query`, `execute`, `enums`; parenthetical about what the test suite verifies | `postgres.ts:48-54, 341-372`; `packages/2-sql/5-runtime/src/sql-runtime.ts:139-141` | Corrected | Parenthetical removed; `tx.nativeEnums` added. `tx.raw` does **not** exist, and the page never claimed it. | +| new | No `isolationLevel` / `timeout` / `maxWait`; no nested transactions | `test/integration/test/ports/prisma/functional/interactive-transactions/interactive-transactions.test.ts:13-16` and its disposition list | Corrected (added) | One bullet added; the page had no such statement. | +| 362 | `tx.sql` keyed like `db.sql` | `postgres.ts:344-347` | Confirmed | — | +| 363–364 | Reads see own uncommitted writes; return value passes through | `sql-runtime.ts:961-1078` | Confirmed | — | +| 365, 424 | Escaped `AsyncIterableResult` rejects `RUNTIME.TRANSACTION_CLOSED` before any row | `sql-runtime.ts:968-982, 989-996, 948-953` | Confirmed | Error wording normalised. | +| 383–421 | Commit / rollback-on-throw / read-your-writes / `tx.execute` examples | `interactive-transactions.test.ts` ("basic", "rollback throw", "rollback query") | Confirmed | API shapes all exist; the `Tag { label }` model is the page's own example schema, not a repo fixture. | +| 437–451 | Prisma ORM 7 `$transaction([...])` becomes a callback | `interactive-transactions.test.ts:9-12` | Confirmed | — | +| 459 | `withTransaction` imported from `@prisma/orm-postgres/family-runtime` | `packages/9-public/@prisma/orm-postgres/package.json:113`; `.../test/facade-tarball.test.ts:188` | Confirmed | — | +| 460–461 | `withTransaction(runtime, cb)`; `tx` has `query` and `execute` only; commits on return, rolls back on throw | `sql-runtime.ts:961-1078` | Confirmed | — | +| 511 | `runtime.connection()` / `connection.transaction()` / `commit()` / `rollback()` / `release()` | `sql-runtime.ts:100-133` | Confirmed | — | +| 513 | `destroy(reason)` "source-read from `postgres-driver.ts`"; evicts one connection; runtime opens a replacement | `packages/3-targets/7-drivers/postgres/src/postgres-driver.ts:545-570`; `sql-runtime.ts:114-127` | Corrected | "source-read" dropped; added that `reason` is optional and advisory. | +| 523 | `destroy(reason)` type `Error`, required | `sql-runtime.ts:126` (`destroy(reason?: unknown)`) | Corrected | Now "`Error`, optional". | +| 562 | MongoDB has no `db.transaction`; runtime has no `connection()`/`prepare()`/`telemetry()`; "probing returns `undefined`, calling throws `TypeError`" | `mongo.ts:28-41`; `mongo-runtime.ts:76-101` | Corrected | Fact kept; the probe sentence replaced with "it has `query`, `execute`, and `close` and nothing else", per page-conventions. | +| 567 | Planned MongoDB transactions: `db.transaction(fn)` with `tx.orm` and `tx.query`, driver session, replica set required, must not degrade silently | `projects/sqlite-mongo-transactions/spec.md:113-131` (present in the rc.10 tree) | Confirmed | Banned phrase "source-read from an internal design document" was already gone; trimmed the trailing note about the page itself. | +| 579 | Declaration maps parameter name to a type id | `packages/2-sql/5-runtime/src/prepared/types.ts:20-30` | Confirmed | — | +| 580 | `runtime.prepare` callback takes one argument | `sql-runtime.ts:549-552`; `prepared/types.ts:51` | Confirmed | — | +| 581 | Unused declared parameter rejected at prepare time, `details.unused` | `sql-runtime.ts:568-573`; `packages/2-sql/5-runtime/test/prepared.test.ts:193-205` | Confirmed | Error wording normalised. | +| 634 | `db.prepare` callback takes `(sql, params)` | `postgres.ts:330-339` | Confirmed | — | +| 635 | `prepare()` starts the connection, so a later `db.connect()` fails `DRIVER.ALREADY_CONNECTED` | `postgres.ts:249-250, 298-303` | Confirmed | Wording normalised. | +| 672 | "A query ending with `.affectedCount()` prepares into a `PreparedExecution`" | `sql-runtime.ts:602-605` (needs `ast.kind === 'raw-query'`); `packages/2-sql/4-lanes/sql-builder/src/types/raw-query.ts:142` | Corrected | Now "A **raw** query ending with `.affectedCount()`". | +| 673–674 | `target` can be runtime, connection, or transaction; one statement runs against any | `prepared/types.ts:53-65`; `sql-runtime.ts:766-880, 986-1028` | Confirmed | — | +| 716 | `RuntimeExecuteOptions` is the second argument to every `query`/`execute` | `packages/1-framework/1-core/framework-components/src/execution/runtime-middleware.ts:352-359` | Confirmed | — | +| 724 | Pre-aborted signal rejects `RUNTIME.ABORTED`, `details.phase` `'stream'` | `.../execution/runtime-core.ts:118-148`; `.../execution/runtime-error.ts:36-46` | Confirmed | Added that `cause` is the signal's reason verbatim (`runtime-error.ts:41-46`), which the page did not state. | +| 725 | "Aborting mid-stream is not separately verified in the validation harness" | `runtime-core.ts:123, 148`; `runtime-error.ts:7-21` | Corrected | Replaced with the fact: the signal is checked between rows, and a mid-stream abort ends the stream with `RUNTIME.ABORTED` and a phase of `'stream'`, `'encode'`, or `'decode'`. | +| 726 | "`scope` selects the execution scope for the query" | `runtime-middleware.ts:60-81`; `sql-runtime.ts:428-441` | **Corrected (real error)** | `scope` is only the label middleware reads. The runtime sets it per scope; setting it yourself changes the label, not where the query runs. The options-table row was corrected too. | +| 755 | MongoDB runtime has no `telemetry()` | `mongo-runtime.ts:76-101` | Confirmed | Probe sentence removed. | +| 756 | `telemetry()` is `null` before any query | `sql-runtime.ts:217` | Confirmed | — | +| 757 | Shape `{ lane, target: 'postgres', fingerprint, outcome: 'success', durationMs? }` | `runtime-spi.ts:45-52`; `sql-runtime.ts:932-944` | Corrected | `outcome` is `'success'` or `'runtime-error'`; the shape line no longer pins it to `'success'`. | +| 780 | `AsyncIterableResult`: one consumption per mode; re-`await` safe; mode switch throws `RUNTIME.ITERATOR_CONSUMED` | `.../execution/async-iterable-result.ts:13-61` | Confirmed | Wording normalised. | +| new | rc.10: buffered PostgreSQL queries release the pooled connection before rows are read | `docs/releases/v8.0.0-rc.10.md` (Fixes, #30259); `postgres-driver.ts:296-333, 449-473`; `postgres.ts:245-247` | Corrected (added) | Two sentences added to the `AsyncIterableResult` section. I wrote it as "a query run through the client", because `postgres()` builds its driver with `cursor: { disabled: true }`, so every client query takes the buffered path; I did not claim cursor-stream behaviour the client cannot reach. | + +## 2. Q list (unverifiable) + +**`await using` requires `@types/node` (which augments `SymbolConstructor` with `asyncDispose`) or a `lib` that declares the symbol.** This is a TypeScript toolchain fact, not a Prisma ORM one, and nothing in the tree states it. Left as written. +Searches run: `grep -rn "asyncDispose" packages --include='*.ts'` (only the two client implementations and their tests); `grep -rn "await using" --include='*.ts' packages test`; `grep -rn "esnext.disposable\|ESNext.Disposable" -ri .`; checked `packages/9-public/@prisma/orm-postgres/package.json` (it does depend on `@types/node`, but nothing says `await using` requires it). + +## 3. Page-conventions last bullet + +The last bullet says the error-reference page is generated upstream and out of scope. My slice does not link to or restate the error-reference page, so nothing applied. + +## 4. Line count + +Before: 782. After: 782. + +Notes for the operator: +- The example schema on this page (`Tag` with a `label` column) does not match any fixture in the rc.10 tree. The API shapes in every example are confirmed, but the claim that the examples were transcribed from a live test suite is not supportable from this source, so I removed it rather than restating it. +- The one substantive behaviour error found was the `scope` option, which the page described as choosing where a query runs. It does not. \ No newline at end of file From f06dea3b1a5e01b0b397718224725046a587355c Mon Sep 17 00:00:00 2001 From: willbot Date: Sun, 13 Sep 2026 08:30:20 +0200 Subject: [PATCH 03/14] docs: spell-check the pinned anchors on the reference pages 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 +- apps/docs/cspell.json | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) 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 860394b2d3e..784c637134d 100644 --- a/apps/docs/content/docs/orm/reference/sql-query-builder.mdx +++ b/apps/docs/content/docs/orm/reference/sql-query-builder.mdx @@ -861,7 +861,7 @@ Say explicitly how a raw value is stored and read when it has no column to take #### Remarks - Import `param` from `@prisma/orm-postgres/relational-core/expression`. -- You rarely need `param()`. Values passed to `insert()`, `update()`, and comparison functions such as `fns.eq(f.col, value)` are already sent as query parameters, never pasted into the SQL, and how each is stored and read comes from the target column. `param()` is the manual escape hatch for a bound value with no neighbouring column to take that from. +- You rarely need `param()`. Values passed to `insert()`, `update()`, and comparison functions such as `fns.eq(f.col, value)` are already sent as query parameters, never pasted into the SQL, and how each is stored and read comes from the target column. `param()` is the manual escape hatch for a bound value with no neighboring column to take that from. - There is **no** `build({ params })` form. Parameter values are embedded into the query where they're supplied. See [`build()`](#build). #### Options diff --git a/apps/docs/cspell.json b/apps/docs/cspell.json index e90bae5aaf1..c80a3c186cf 100644 --- a/apps/docs/cspell.json +++ b/apps/docs/cspell.json @@ -467,7 +467,10 @@ "urlencode", "extensionless", "EBADDEVENGINES", - "Initialising" + "Initialising", + "dbsql", + "txexecute", + "txsql" ], "patterns": [ { From 400b728a98a6e372fe77e3b180017714a228a9e8 Mon Sep 17 00:00:00 2001 From: willbot Date: Sun, 13 Sep 2026 08:38:31 +0200 Subject: [PATCH 04/14] docs: orm/reference reader-review round one reports Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- .../pipeline-builder.part1.round1.md | 99 ++++++++++++++ .../pipeline-builder.part2.round1.md | 124 ++++++++++++++++++ .../raw-queries-and-index.round1.md | 63 +++++++++ .../sql-query-builder.part1.round1.md | 81 ++++++++++++ .../sql-query-builder.part2.round1.md | 94 +++++++++++++ .../transactions-and-runtime.part1.round1.md | 97 ++++++++++++++ .../transactions-and-runtime.part2.round1.md | 92 +++++++++++++ 7 files changed, 650 insertions(+) create mode 100644 docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part1.round1.md create mode 100644 docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part2.round1.md create mode 100644 docs/orm-docs-audit/c21-orm-reference/raw-queries-and-index.round1.md create mode 100644 docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part1.round1.md create mode 100644 docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part2.round1.md create mode 100644 docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part1.round1.md create mode 100644 docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part2.round1.md diff --git a/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part1.round1.md b/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part1.round1.md new file mode 100644 index 00000000000..839a816edb0 --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part1.round1.md @@ -0,0 +1,99 @@ +# Reader review: `pipeline-builder.part1.mdx` + +I used Prisma ORM 7 for two years. I have never seen this API. + +## Sentences I could not restate after one reading + +**Line 95** — "`from(root)` takes a contract **root name**: the collection name from the contract's `roots` map (`'posts'`, `'users'`), the same names the ORM client uses (`db.orm.users`), not the model names." + +What stopped me: "contract" and "the contract's `roots` map". I have no idea what a contract is, where it lives, or what file I would open to see this map. It is bolded like a defined term, but it is never defined and never linked. Plainest wording I would have understood: "`from()` takes the collection name — the same name you use on `db.orm` (`'posts'`, `'users'`), not the model name `Post`." + +**Line 97** — "The builder moves through three states as you chain: a starting collection, a filtered collection after `match()`, and a pipeline chain after any other stage. Each state exposes the methods that are valid at that point." + +What stopped me: too many ideas, and all of them about the library's internal design. The only part I can act on is the parenthetical about `insertOne` / `insertMany`. Plainest wording: "Some methods are only available before you add stages — `insertOne` and `insertMany`, for example. If a method is missing from autocomplete, you have already chained past it." + +**Line 126** — the whole bullet. "`runtime.query(plan)` is the executor. It accepts any plan, including one built by the pipeline builder. On MongoDB `db.runtime()` returns a promise, so the call reads `(await db.runtime()).query(plan)`; hold the runtime in a variable if you run several plans. It returns an `AsyncIterableResult`: `await` it for an array of documents, or `for await` over it to take one at a time. There is no `db.execute` on the MongoDB client." + +What stopped me: five separate facts in one bullet. Split it into five bullets, or better, show the "hold the runtime in a variable" version as code — you never do, despite every example paying the double-`await` cost. + +**Line 127** — "**Read results are decoded, up to a point.** ... so `_id` comes back as a decoded hex string and other fields come back as their contract types." + +What stopped me: "up to a point" is vague until I read the next bullet, "contract types" again leans on the undefined word "contract", and "carries a result shape" is internal vocabulary. The actionable rule is buried: the list of eight stages that preserve decoding. + +**Line 150** — "`match()` filter values are **not encoded to the stored type**. The value you pass is compared as-is against the stored value." + +What stopped me: "encoded to the stored type" means nothing to me until the `_id` warning 40 lines later. On its own I cannot tell what could go wrong. + +**Line 151** — "`eq` and `expr(...)`-wrapped comparisons run in Prisma ORM's own tests; the remaining operators follow the matching MongoDB query operators." + +What stopped me: I could not tell what this means for me. Do `gt` and `in` work or not? "Follow the matching MongoDB query operators" sounds like a description of behavior, but the first clause implies a warning. Say plainly: "These operators are untested by Prisma but behave like the MongoDB operators of the same name." + +**Line 191** — "the pipeline builder's value resolver walks the value as a plain object, destructuring an `ObjectId` into its internal `buffer` bytes, so the filter that reaches MongoDB never carries a real `ObjectId`." + +What stopped me: this is a description of a bug in Prisma's internals. I only needed sentence one: `_id` equality in `match()` silently returns nothing; use `rawCommand()` or `db.orm`. + +**Line 191, last two sentences** — "Note that the hex-string form **compiles without any cast**. ... Only the driver-`ObjectId` form requires a cast to attempt." + +What stopped me: "cast" — a cast to what type? Neither sentence tells me what I would type, and I cannot work out why "requires a cast to attempt" matters if neither form works. + +**Line 616** — "`rawPath` builds a field path without checking it against the model, and a field path is compiled by prefixing a `$`, so `'$KEEP'` reaches the server as `$$KEEP`." + +What stopped me: I had to hold three steps in my head and there is no example to check them against. Plainest wording: "Write `f.rawPath('$KEEP')`. The builder adds one more `$`, so the server sees `$$KEEP`." + +**Line 626** — "**The expression fields inside these options are opaque.** Fields like `bucket.groupBy` ... are raw aggregation expressions (`MongoAggExpr`), not reachable through the typed `(f) => ...` callback the classic stages use. You build these values with expression helpers and pass the underlying node (`fn.year(ref).node`), or name a field directly with `MongoAggFieldRef.of('price')`." + +What stopped me: "opaque", "the underlying node", and `ref` — which appears from nowhere and is never defined. This is the hardest paragraph on the page and the only one with no example. + +**Line 644** — "they need Atlas Search's `mongot` process ... because those tests use an in-memory MongoDB with no Atlas Search." + +What stopped me: `mongot` is a word I have never seen, and both halves are about Prisma's test setup. The fact I need is the last sentence: it will fail at runtime, not at build time. + +## Words and phrases I had to guess + +- **"contract"** (lines 95, 127) — I guessed it is some generated description of my schema, maybe a file, maybe a type. I still do not know. +- **"roots map"** — guessed: a list of collection names somewhere in that contract. +- **"plan"** (line 116 onward) — guessed: the object `build()` returns, which you hand to the executor. The page never says this in one sentence; I inferred it from the variable name. +- **"discriminated model"** (line 23) — guessed: one collection storing two kinds of document, told apart by a field. `@@discriminator`, `@@base`, and `@@type("mongo/string@1")` in the schema are all new to me and unexplained. +- **"result shape"** / **"carries the shape forward"** — guessed: Prisma knows the column types, so it converts values back. +- **"decoded hex string"** vs **"raw driver `ObjectId`"** — guessed: one is a `string`, the other is the mongodb driver's class. +- **"inert"** (line 122) — guessed: nothing runs until you execute. +- **"the executor"** (line 126) — guessed: just a name for `runtime.query`. +- **"opaque"** (line 626) — guessed: not type-checked. +- **"the underlying node"** — guessed: some internal AST object; `.node` is a property I am told to reach for without being told what it is. + +## Where I asked "so what do I actually type?" + +1. **Where does `db` come from?** Every single code block starts with `db`, and no block imports it or creates it. The `fn` / `acc` / `expr` imports are shown, so the omission is glaring. One line and a link would fix it. +2. **`mongoQuery(...)`** (line 94) — named as an alternative entry point, with no import, no signature, no example, no link. +3. **`build()` vs `aggregate()`** (line 122) — I am told they are aliases. I am never told which one to prefer, so I do not know which is idiomatic. +4. **Holding the runtime in a variable** (line 126) — the page recommends it and then never shows it. I would have to invent `const rt = await db.runtime()` myself. +5. **`redact()`** — no Options table, no Return type table, and **no example**, unlike every other stage on the page. It is also the stage with the most confusing workaround. I would have to assemble `fn.cond(someCondition, f.rawPath('$KEEP'), f.rawPath('$PRUNE'))` from prose, and I am not confident that is right. +6. **`unwind()`** — no example, and the reason given is "the example schema has no array field." Add an array field to the schema. +7. **Every option-object stage** — `bucket`, `bucketAuto`, `geoNear`, `graphLookup`, `setWindowFields`, `densify`, `fill`, `facet` are named and then shown zero code. The one example is `unionWith('users')`, the simplest of the group. `facet` in particular ("an object that maps each output field name to an array of raw stages") is impossible for me to write from that sentence. +8. **`search()` / `searchMeta()` / `vectorSearch()`** — no example block, only a one-line table cell. The `vectorSearch` spec is listed as bare field names with no types. +9. **Comparing `ObjectId`s** — "Compare them with `String(...)`" appears twice. Show the line: `String(a._id) === String(b._id)`. +10. **`lookup()` chain** — `from(root).on((local, foreign) => ({ local, foreign })).as(name)` is written once as a signature, where `local` and `foreign` are both parameter names and object keys. I had to read the example twice to see that the keys are fixed. + +## Places explaining internals when I only wanted to know what to do + +- Line 15: "Where Prisma ORM's own tests do not run a method against a live MongoDB, the Remarks say so." I do not care about your test suite. Say "some methods are unverified" if you must, but the repeated test-coverage notes (lines 151, 625, 644) read like release notes for the Prisma team. +- Line 97: the three builder states. +- Line 126–128: "result shape" carried and dropped. +- Line 191: the value resolver and the `buffer` bytes. +- Line 451: "(Some older internal material shows a two-argument callback. That is incorrect.)" This is a note to whoever wrote the previous draft. Delete it; just show the correct signature. +- Line 616: the double-`$` compilation trick, explained as mechanism rather than as an instruction. +- Line 626: `MongoAggExpr`, `.node`, `MongoAggFieldRef`, `MongoMatchStage`, `MongoCountStage`, and "a known gap in the typed API." +- Line 644: `mongot`, in-memory test MongoDB. + +## Could I do what the page is for, after one reading? + +Partly. I could write the simple stages — `match`, `sort`, `limit`, `skip`, `sample`, `project`, `addFields`, `group`, `count`, `sortByCount`, `lookup`, `replaceRoot`. The examples for those are complete and I could copy them. + +What I would still not know: + +- **How to get `db`.** I cannot run a single example on the page. This is the biggest failure. +- **What a "contract" is**, so I cannot check what names `from()` will accept without guessing from `@@map`. +- **How to write `unwind`, `bucket`, `bucketAuto`, `geoNear`, `graphLookup`, `setWindowFields`, `densify`, `fill`, `facet`, `redact`, `search`, `searchMeta`, or `vectorSearch`.** That is 13 of the stages the page claims to document. The page says "This page documents every stage" (line 15); for these it names them and stops. +- **Whether my results will be decoded.** I understand the rule exists. I would not remember which eight stages preserve it, and the page gives me no way to check at runtime other than inspecting values. +- **Why `Article` and `Tutorial` are in the schema.** No example uses them, and the schema syntax around them is all unfamiliar. They cost me reading time for nothing. +- **Whether `gt`, `in`, `exists`, `type` actually work**, because line 151 hedges without saying what the hedge means for me. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part2.round1.md b/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part2.round1.md new file mode 100644 index 00000000000..bcc884968f1 --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part2.round1.md @@ -0,0 +1,124 @@ +# Reader report: `pipeline-builder.part2.mdx` + +I have used Prisma ORM 7 for two years. I have never seen this builder. Here is what stopped me. + +--- + +## 1. Sentences I could not restate after one reading + +**Line 33** — "The examples below are the common accumulators, all executed against a live database. The `N`-variants (`firstN` shown), `top`/`bottom`, and the standard-deviation accumulators share the same call shapes; `firstN` is exercised here as a representative of the family." + +Stopped by: "executed against a live database" and "exercised here as a representative of the family". This is a note about how you tested the docs, not about how I use the thing. I do not care where you ran them. "Exercised" is test-suite vocabulary. Plainest wording: "`firstN` is shown below. `lastN`, `maxN`, and `minN` are called the same way." And drop the live-database clause entirely. + +**Line 33, second half** — "In every signature, `sortBy` is a plain `{ field: 1 | -1 }` object and `n` is an expression, so pass `fn.literal(2)` rather than `2`." + +Stopped by: "`n` is an expression". At this point in the page "expression" has not been defined — expression helpers start 80 lines later. I only learned from the trailing example what I actually type. Plainest wording: "`n` must be wrapped: write `n: fn.literal(2)`, not `n: 2`." Also `1 | -1` is never spelled out; I guessed ascending/descending. + +**Line 120** — "The `fn.*` namespace covers 73 of MongoDB's aggregation expression operators. It is not all of them: there is no `fn.and`, `fn.or`, `fn.not`, `fn.switch`, `fn.ifNull`, `fn.map`, `fn.reduce`, or `fn.filter`, and arithmetic stops at `add`, `subtract`, `multiply`, and `divide`." + +Too many ideas in one paragraph, and it tells me what is missing without ever telling me what is there. The table below it is headed "Helpers (examples)" — so eight helpers are named out of 73 and the other 65 are unlisted. I cannot use this page to find out whether `fn.substr` or `fn.dateAdd` exists. This is the biggest gap on the page. + +**Line 127** — "Return a boolean expression; use `.node` where a raw expression is required." + +Stopped by `.node`. It is never defined anywhere on the page. It is used at lines 136, 180, 407, 428 and I still do not know what it is or when I need it, beyond "`fn.cond` wants it and `match()` does not". Plainest wording: say what `.node` gives you and give me a one-line rule for when to add it. + +**Line 136-138** — the `fn.cond` / `expr()` remarks. + +The rule is buried under an internal crash message: "throws `TypeError: visitor.expr is not a function`". That error names an internal thing called a "visitor" that I have no way to connect to anything. Plainest wording: "Inside `fn.cond()`, pass `fn.eq(a, b).node`. Inside `match()`, pass `expr(fn.eq(a, b))`." + +**Line 138** — "`fn.toObjectId(fn.literal(id))` throws (`Cannot read properties of undefined (reading 'codecId')`), because `fn.literal()` has no field type information." + +"codecId" is your internals leaking. So is "field type information". And the fix — "Give `fn.toObjectId()` an expression taken from the `f` argument instead" — has no example, so I still do not know what to type when the ObjectId I want to convert is a string variable from my own code and not a field on the document. That is the normal case and the page does not cover it. + +**Line 207** — "It keeps the row type you already had; `pipe(stage)` lets you declare a new one. Whatever the row type says, the rows are no longer decoded, because a stage the builder does not recognise drops the result shape." + +Three ideas, one sentence, and the key word "decoded" is never defined. It appears again at lines 259, 322, 466, 468. I guessed it means "converted from raw BSON into the model's types", but the page never says so. Plainest wording: "After `pipe()`, the rows come back exactly as MongoDB returns them — dates and ids are raw driver values, whatever the TypeScript type claims." + +**Line 247** — "A bare (non-array) operation throws as soon as you call the write method: `Error: Unreachable: items.length > 0 but first is undefined`. This is an internal consistency guard, not a friendly validation message..." + +You are apologising for a bug in the middle of a reference. I do not need the crash text or the words "internal consistency guard". The rule is one sentence: "Always return an array." Then: "(Some older internal material shows the bare form. That is incorrect.)" — I cannot resolve "older internal material". I have never seen any internal material. Delete it or name the thing. + +**Line 249** — "You cannot mix operator-form ops and pipeline-form (`f.stage.*`) ops in one updater." + +Neither form is defined until line 400, 150 lines later. On first reading this sentence is meaningless. + +**Line 260** — "The builder does not check it against the model, and its values are stored as you pass them, the same way [`match()`](#match) filter values are." + +"stored as you pass them" — as opposed to what? The contrast is never stated, so the warning has no teeth. What actually goes wrong if I pass a string where the contract says Date? The page does not say. + +**Line 322** — "Both disappear from the type after a stage MongoDB's `findAndModify` command cannot carry, such as `skip()`; forced past the type system they throw an error whose `code` is `ORM.OPERATION_UNSUPPORTED`." + +The worst sentence on the page. "Disappear from the type" plus "a stage `findAndModify` cannot carry" plus "forced past the type system" — three unfamiliar ideas stacked. Plainest wording: "You cannot call `findOneAndUpdate()` or `findOneAndDelete()` after `skip()`. TypeScript will not offer them there." That is all I need. + +**Line 386** — "If you force it past the type system, the unknown key is silently ignored and the default (`'after'`) applies." + +"Force it past the type system" appears three times on the page (322, 386, and implied at 138) and is never explained. I guessed it means casting with `as any`. Say that, or drop the whole clause — TypeScript already rejects it, so this is an essay about what happens if I defeat my own compiler. + +**Line 466** — "`db.query.rawCommand(command)` packages a command node (for example `new RawAggregateCommand(collection, pipeline)`) into a plan, without translating it." + +"command node", "without translating it", and (line 462) "bypassing the typed AST". AST is internal architecture. I only want to know: raw pipeline in, untyped rows out. + +--- + +## 2. Words and phrases I had to guess at + +| Quote | What I guessed | +|---|---| +| `.node` | Some unwrapped inner value. Still not sure. | +| "decoded" / "undecoded" | Converted from raw driver values to the model's types. Never stated. | +| "result shape" | The TypeScript type of a returned row. | +| "the typed AST" | Your internal query representation. | +| "expression" | Anything built from `fn.*` or taken off `f`. Never defined on this page. | +| "plan" vs "Built query" (line 227) | The same thing under two names. The return-type table says "Built query", the prose everywhere says "plan". | +| "terminals" (anchor `#read-terminals`, and `#pipeline-write-terminals-out-and-merge`) | The last call in a chain. It only shows in anchors, but I saw it. | +| "escape hatch" (208, 467) | A way out of the typed API. Used twice, never unpacked. | +| `_id: null` in every `group()` example | "Put everything in one group." Never said. | +| `1 | -1` in `sortBy` | Ascending / descending. | +| `output` in `acc.top({ output, sortBy })` | The fields to return. Never explained and never shown. | +| `$redact` with `$$KEEP` / `$$PRUNE` (467) | Raw MongoDB features. Nothing on this page tells me what they are or why they are named here. | +| "field path" (line 130, `fn.literal` description) | A reference to a document field, as opposed to a constant. | + +--- + +## 3. "So what do I actually type?" — places the page does not say + +1. **`pipe()` has no example at all.** It is the escape hatch and it shows zero code. "`new MongoMatchStage(...)`" — with what arguments? What does the import line look like? I cannot use `pipe()` after reading this. +2. **The 65 unlisted `fn.*` helpers.** I need `fn.substr` or a date-add. The page gives me a naming rule (drop the `$`, camelCase) and five exceptions, but no list, so I have to guess and hope the compiler agrees. +3. **The 14 operator-form operators at line 406** — `inc`, `push`, `pull`, `pullAll`, `pop`, `currentDate`, `setOnInsert`, `rename`. Every example on the page uses `set` and nothing else. What are the arguments to `f.tags.pull(...)`? To `f.views.inc(...)`? To `f.updatedAt.currentDate(...)`? Unanswered. +4. **`merge()`'s `whenMatched` and `whenNotMatched`** (line 440). Named, no values listed, no example. I cannot write a merge that does anything other than the default. +5. **`out()`'s second argument** (line 439): "A second argument names a different database." A bare string? An object? No example. +6. **`acc.top` / `bottom` / `topN` / `bottomN`.** Line 33 says they "share the same call shapes" with `firstN`, but they do not — they take `output` and `sortBy`, and `firstN` takes `input`. So the one worked example does not transfer, and `output` is never shown. +7. **Converting a string id to an ObjectId.** Line 138 tells me the obvious approach throws, and points me at "an expression taken from the `f` argument" — which is not what I have when the id came from a URL parameter. The answer turns out to be `rawCommand()` 330 lines later, but the two are never connected at the point where I hit the problem. +8. **`durationSum: acc.sum(fn.literal(1))`** (line 44). The variable is called "durationSum" but it sums the constant 1, which just counts. I do not learn how to sum an actual field. Show `acc.sum(f.views)`. +9. **`findOneAndUpdate`'s second argument** — is it optional? Both examples pass it. The default is stated, so presumably yes, but the signature is never given. +10. **What `acc.first()` / `last()` mean without a `sort()`.** The example sorts first. The table says "first document in the group" with no warning that this is meaningless unsorted. + +--- + +## 4. Places that explain how the tool works inside, when I only wanted to know what to do + +- Line 136: `TypeError: visitor.expr is not a function` — a "visitor" is your code, not mine. +- Line 138: `Cannot read properties of undefined (reading 'codecId')` and "because `fn.literal()` has no field type information" — internal cause for an internal crash. +- Line 207: "because a stage the builder does not recognise drops the result shape" — the reason is internal; the consequence is all I need. +- Line 247: `Error: Unreachable: items.length > 0 but first is undefined` plus "This is an internal consistency guard, not a friendly validation message". +- Line 407: "(which emits an `$addFields`-style stage)" — I do not need to know what it compiles to. +- Line 462/466/467: "bypassing the typed AST", "packages a command node", "without translating it". +- Lines 33, 120: "executed against a live database", "exercised by the test suite", "exercised here as a representative" — three references to your test suite. Your tests are not my concern, and "the ones exercised by the test suite" tells me the table's contents were chosen by what you happened to test, which makes me trust the table less. + +--- + +## 5. Could I do what the page is for, after one reading? + +Partly. I could write a `group()` with `count`, `sum`, `avg`, `min`, `max`, `first`, `last`, `push`, `addToSet`. I could write a simple `updateMany` with `set`, a `deleteMany`, an `insertOne`, and a `findOneAndUpdate` with `returnDocument`. Those have real, complete, copyable examples. + +I could **not**: + +- Use any `fn.*` helper not in the eight-row table, because I have no list of the other 65 and no way to check whether the one I want exists. +- Use any update operator other than `set` — that is 13 of the 14 listed. +- Use `pipe()`, because there is no example and no import. +- Use `acc.top`, `bottom`, `topN`, or `bottomN`, because `output` is undefined and unshown. +- Configure `merge()` beyond the default, because `whenMatched` and `whenNotMatched` have no values listed. +- Convert a string id from my application into an ObjectId for a filter, which is something I do constantly in ORM 7. +- Explain to a colleague what `.node` is, or when a value is "decoded" and when it is not — and both of those change what my code gets back at run time, so not knowing them is a real risk, not a cosmetic one. + +The page reads as if it were assembled from a test suite: what was tested got an example, what was not got a name in a table or nothing. The reference sections that matter most for day-to-day work — the full `fn.*` list and the update operators — are the ones reduced to a name. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/raw-queries-and-index.round1.md b/docs/orm-docs-audit/c21-orm-reference/raw-queries-and-index.round1.md new file mode 100644 index 00000000000..9152608728a --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-reference/raw-queries-and-index.round1.md @@ -0,0 +1,63 @@ +# Reader review, round 1: index.mdx and raw-queries.mdx + +## index.mdx + +### Sentences I could not restate + +1. "Prisma ORM has three query surfaces." "Query surface" is not a word I know; I guessed "three different APIs for writing queries". Say that. +2. "The pipeline builder gives you a typed way to build MongoDB aggregation pipelines through `db.query`." `db.query` appears here and nowhere else; the other two are named after the property (`db.orm`, `db.sql`), so I expected `db.pipeline`. Plainest: "The pipeline builder builds MongoDB aggregation pipelines. You reach it at `db.query`." +3. "The method reference pages document each method with the classic Remarks / Options / Return type / Examples structure (the raw queries page is deliberately more narrative)." "Classic" to whom? "More narrative": guessed "prose instead of tables". Why am I told this before reading anything? +4. "When a method's behavior differs between PostgreSQL and MongoDB, exists on only one database, or is type-checked but not enforced at runtime, the method's own Remarks call that out inline." Three conditions plus a promise. "Type-checked but not enforced at runtime" stopped me. Plainest: "If a method works differently on PostgreSQL and MongoDB, or exists on only one of them, its Remarks say so." + +### Words guessed +- "query surfaces": three separate query APIs. +- "model-level methods like `where()`, `create()`, and `include()`": in Prisma 7 these were arguments, not methods. Guessed they are chained calls now. The page never says this changed, and it is the biggest difference. +- "client lifecycle": connecting and disconnecting. +- "raw escape hatches": the way out when the typed API cannot do it. + +### So what do I type? +The whole page: not one line of code. One three-line snippet showing the same query in the ORM client and the SQL query builder would answer the page's own question. + +### Contradiction +"Reach for the SQL query builder when you need a join", but the ORM client has `include()`, which is how I did joins in Prisma 7. Which do I use? + +### Could I do what the page is for? +Partly: I can pick which page to open. I cannot tell whether my Prisma 7 queries map to the ORM client or the SQL query builder, and I do not know what `db.query` is. + +## raw-queries.mdx + +### Sentences I could not restate + +1. The page-level warning (lines 13 to 15): one block, six claims, `codecId` used four times before it is defined. "`.returnsRow(spec)` names a `codecId` for every column, so its rows are converted just like a builder query's." What is a codec, what does "converted" mean, what does a builder query's row look like? Plainest: "A `codecId` such as `pg/text@1` tells Prisma how to turn the database's bytes into a JavaScript value. `.returnsRow()` needs one per column, so you get real JavaScript values back." "A `fns.raw` fragment's `.returns(codecId)` only tells TypeScript what type to expect, so the value is whatever the driver returns." What is whatever the driver returns? The first example then shows `upperEmail: 'ALICE@EXAMPLE.COM'`, a normal string, so the warning scared me and the example contradicted it. +2. Line 23: "A whole raw statement is a complete SQL statement written with `db.raw.sql`, which you terminate with `.returnsRow(spec).build()` for a plan `runtime.query(...)` streams rows from, or `.affectedCount().build()` for a plan `runtime.execute(...)` reports statistics for. Both sizes end in a plan, so both go through the same middleware and the same runtime." Too much: two APIs, two terminators, two runtime methods, "plan" and "middleware" both new. "Streams rows from" made me expect an iterator; the example awaits an array. Plainest: "Write the statement with `db.raw.sql`. If it returns rows, end with `.returnsRow(spec).build()` and run it with `runtime.query(plan)`. If it does not, end with `.affectedCount().build()` and run it with `runtime.execute(plan)`." +3. Line 67: "Interpolating another `Expression` ... splices that expression's AST into the fragment. It is not a string concatenation of rendered SQL: the interpolated expression lowers to its own AST node, so it stays parameterized and type-checked." Internals. Needed: "Interpolated values are always sent as query parameters, never pasted into the SQL string." +4. Line 84: "A bare JavaScript value can be interpolated directly, and the adapter picks a `codecId` for it from its JavaScript type: `number` becomes `pg/int4@1`, `pg/int8number@1`, or `pg/float8@1` depending on its range". "The adapter": first mention. "Depending on its range" gives no ranges. If I pass 3000000000, which do I get? +5. Line 118: "`db.raw` is the client's raw SQL API ... Its one key, `sql`, is the same tagged template as `fns.raw`." Wrong by the page's own account: line 212 uses `db.raw.collection(...)`. +6. Line 171: "A row-returning raw query composes: interpolate one into another template and its SQL and its parameters are spliced into the outer statement, in order, which is how you build a subquery or a CTE. The outer template declares its own row spec, and a column the inner query already declared can be inherited from its `.returns` record rather than restated." Four ideas. Also `.returns` is now a property holding a record, after being a method in five earlier examples. +7. Line 198: "A statement terminated with `.affectedCount()` is a plan handle, not a row source, so it cannot be interpolated." Invented terms. Plainest: "You cannot interpolate an `.affectedCount()` statement, because it produces no rows. You can interpolate an `UPDATE ... RETURNING`, because it does." +8. Lines 202 to 208: "Interpolating anything else, such as a `Date`, throws synchronously with code `RUNTIME.RAW_SQL_UNSUPPORTED_INTERPOLATION`" then "TypeScript already rejects an unsupported value at compile time." If TypeScript rejects it, when does the runtime error fire? The quoted message has literal ellipses, so I cannot match it. +9. Line 313, `findOneAndUpdate()`: "The MongoDB command AST carries `sort` and `returnDocument` fields, but the public wrapper does not expose them ... a value that reaches the call untyped is dropped without a run-time error." Internals. Needed: "`findOneAndUpdate()` accepts only `{ upsert }`. To sort, or to get the document after the update, do X." It then names `new RawFindOneAndUpdateCommand(...)` with no code and no import. +10. Line 346: "runs a raw MongoDB aggregate command through the pipeline builder, bypassing the typed AST. Its rows come back as `unknown`; you type them yourself." How? `aggregate()` took a type parameter; does this? + +### Words guessed +"raw driver values" (values with no Prisma conversion); `codecId`; "a plan" (a prepared object you hand to the runtime); "middleware" (mentioned once, never explained); "a function bag `fns`"; "the adapter" (the PostgreSQL driver layer); `Expression`; "contract column" (`user.columns.id`); "the contract's lowercase plural root" (the collection name); "buildable command"; "return an image of it"; "row source" / "plan handle". + +### So what do I type? +1. Two ways to get a runtime, both unexplained: PostgreSQL `const runtime = await db.connect()`, MongoDB `(await db.runtime()).query(plan)`. How do they relate? Which do I use? +2. Where is the list of `codecId` values? I see eight; `pg/timestamptz-temporal@1` appears once, unexplained. For a uuid, jsonb, or numeric I have no idea and no link. +3. `new RawFindOneAndUpdateCommand(...)`: the only way to sort or get the post-update document, with no signature, import, or example. +4. Filtering by `_id` in a raw pipeline: line 356 says `rawCommand()` exists partly for `_id` equality filters and then sends me to another page. Four lines of code would do. +5. Undeclared variables in every example: `aliceId`, `bobId`, `limit`, `kind`, `minPosts`. On MongoDB `aliceId` is a hex string (`new ObjectId(aliceId)`); on PostgreSQL presumably a UUID. Never said. +6. Is `fns.raw` safe with user input? "Stays parameterized" is buried under AST language, and nothing is said about interpolating a column or table name from user input, which is the real danger. +7. Two forms of `select()` with no bridge: `.select('upperEmail', (f, fns) => ...)` and `.select((f) => ({ id: f.id, serverNow }))`. +8. MongoDB writes go through `query()`, not `execute()`, destructured out of an array; PostgreSQL writes go to `execute`. Never pointed out. +9. Can I run a raw query inside a transaction? Nothing. + +### Internals instead of instructions +Line 67 (AST); line 23 (middleware, plans); line 313 (command AST, wrapper); line 346 (typed AST); line 263 "`insertMany()`'s `insertedIds` is a plain array of `ObjectId`, reshaped from the native driver's index-keyed map". + +### Smaller things +`.returnsRow(spec)` is singular but returns many rows. Line 137's SQL writes `FROM "user"` with no schema while the page insists tables live under `db.sql.public`. The MongoDB section says `'users'`, `'posts'`, `db.orm.users`, while elsewhere models are `db.orm.public.User`; no sentence explains that MongoDB has no schema segment. + +### Could I do what the page is for? +PostgreSQL raw SQL: mostly yes. Still unknown: which `codecId` for types not in the short list; whether a fragment's value comes back usable; whether interpolating user input is safe. MongoDB raw commands: no for the two cases I would hit: sorting or post-update document from `findOneAndUpdate`, and filtering by `_id` in a `rawCommand`. diff --git a/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part1.round1.md b/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part1.round1.md new file mode 100644 index 00000000000..a19d2071c64 --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part1.round1.md @@ -0,0 +1,81 @@ +## Sentences I could not restate after one reading + +**Line 11** — "Where a method works only on some databases, or has a caveat that only shows up when the query runs, the Remarks call that out." +"the Remarks" arrives capitalised, as if I already know it. I don't meet a Remarks section until line 337. Plainer: "Some methods only work on some databases, or fail only when the query actually runs. Each method's **Remarks** section says so." + +**Line 131** — "You build queries from the client's `sql` property, which knows your contract and how your queries are run." +I do not know what "your contract" is. In Prisma 7 I had a schema and a generated client. The word never gets defined, but `contract.json` and `contract.d` show up in the very next code block as things I am supposed to already have. Plainer: say what a contract is and which command produces those two files, or link to the page that does. + +**Line 135** — "Create the client with `postgres(...)`, take a runtime from `db.runtime()`, and build queries off `db.sql.public`. On PostgreSQL `db.runtime()` is synchronous, so there is nothing to await." +Three new nouns in one breath: client, runtime, and `public`. And the second sentence implies runtimes are sometimes asynchronous without saying when or what I'd do then. I could not restate what a "runtime" *is* — only that I need one to run a query. + +**Line 149** — "`db.sql` holds the result of an internal `sql({ context, rawCodecInferer })` call, connected to the client's way of running queries and to the adapter's rule for how values are stored and read." +This defeated me. "internal", "context", "rawCodecInferer", "adapter", "rule for how values are stored and read" — five unknowns, one sentence, and it describes the inside of the tool. I wanted to know what to type. + +**Line 149, second half** — "a `rawCodecInferer` (a fallback rule for how to store and read raw expressions that have no column to take that from)" +"that have no column to take that from" — I read this three times. Take *what* from a column? I think it means: a raw SQL expression has no column, so the builder cannot work out its type, and this supplies the default. If that is the meaning, say that. + +**Line 199** — "Each name resolves against the current scope; an unknown name throws an error whose `code` is `ORM.COLUMN_UNKNOWN`." +"the current scope" is never defined. I only work out roughly what it means 140 lines later, from the `lateralJoin` remark about ambiguous names being dropped. Also: does it throw when I call `select()`, when I call `build()`, or when the query runs? The page draws that distinction at line 11 and then never tells me which side this falls on. + +**Line 341** — "`.as(...)` returns the wrong shape (a bare join source), which throws `subquery.getRowFields is not a function`." +This is an internal JavaScript error message pasted into a reference page. I can match it if I see it, but "bare join source" versus the join source `.as()` is documented to return at line 528 is a contradiction I cannot resolve. At 528 `.as()` produces exactly what joins want. Here the same value is "the wrong shape". Which is it? + +**Line 342** — "the merged scope **drops** the ambiguous names." +I had to guess that "merged scope" means the combined set of column names from both sides of the join. The sentence describes an internal bookkeeping step; what I need is the rule: after a lateral join, if both tables have a column of the same name, you must write `f.post.id`, never `f.id`. + +## Words and phrases I had to guess + +- **"contract"** — guessed: the new name for the generated schema types. Guessed `contract.json` and `contract.d` are generated by some CLI command. +- **"runtime"** — guessed: the thing that holds the database connection and executes queries. +- **"plan"** (the variable name, used in every example) — guessed: the compiled query object that `build()` hands to `runtime.query()`. The page never once says what `build()` returns or why it is called a plan. +- **"escape hatch"** (line 9) — guessed: a way out of the normal API for cases it doesn't cover. +- **"facet"** — appears only in the anchor `#the-dbsql-facet` (line 133) while the heading says "builder". Two names for one thing; I don't know if a facet is a distinct concept. +- **"function bag `fns`"** (line 187) — guessed: an object full of helper functions. "Bag" is not a word I've seen used this way. +- **"codec"** — never used in prose, but `'pg/text@1'` and `'pg/int4@1'` appear five times and `rawCodecInferer` names it. Guessed: a type tag telling Prisma how to encode and decode a Postgres value. +- **`Uuid`**, **`@@type("pg/text@1")`**, **`@@discriminator(type)`**, **`@@base(Task, "bug")`**, **`types { Embedding1536 = pgvector.Vector(1536) }`** — all new to me since v7. All guessed. The schema is presented as background, but it is the least familiar thing on the page. +- **"adapter"** (lines 149, 339) — guessed: the per-database driver. +- **"join source"** (lines 170, 528) — guessed: anything you're allowed to put on the right of a join. +- **"derived table"** (line 341, 348) — guessed: the SQL term for a subquery used as a table. + +## Places I asked "so what do I actually type?" + +1. **Where do `contract.json` and `contract.d` come from?** Every example depends on them. No command, no link, nothing. This is the first thing that blocks me from running a single line on the page. "This is a reference page, the project already exists" does not cover it — I have a v7 project and no contract. +2. **SQLite.** Line 14 says SQLite is supported. Every example is `postgres(...)` and `db.sql.public`. What do I import for SQLite? What replaces `public`, given SQLite has no schemas? The page raises SQLite four times (lines 14, 339, 453) and never shows one line of SQLite code. +3. **The codec strings.** `.returns('pg/int4@1')` is required for every raw expression, and there is no list of valid values and no link to one. I can copy `pg/text@1` and `pg/int4@1`. For a `numeric`, a `timestamptz`, a `bool`, I am stuck. +4. **`context`** in the line 154 example. The code is `sql({ context, ... })` with `context` appearing from nowhere. "Pass your `context` (from your database client)" — which client? I just created the client with `postgres(...)`, and that one has `db.sql` already. If this path is for people not using `postgres()`, I have no idea what object to pass. +5. **`limit(n)`** — "`number`, or a numeric expression" (line 491). No example of the expression form. What would I write? +6. **Does `distinctOn()` have to come after `orderBy()`?** The example puts it last (line 477), the prose says "according to the query's `orderBy()`" and "Pair it with an `orderBy()`", but never says whether call order matters. I would have to experiment. +7. **`.as()` on non-SELECT queries.** Line 516 says "a completed `SELECT` query". Is there a way to use an INSERT's returned rows as a source? Silence — possibly answered in part 2. +8. **`orderBy()` twice** — line 396 says calling it again adds secondary keys. Does calling `select()` twice add columns or replace them? Line 226 chains two `select()` calls and the result seems additive, but the prose at line 200 says "Add one computed column" only for the aliased form. For the column-name form at line 199 it says "Keep the named columns" — so a second call presumably replaces. Unclear. + +## Places that explain the inside when I wanted the outside + +- **Line 149**, the whole `sql({ context, rawCodecInferer })` paragraph. It tells me what `db.sql` *holds internally* and what it is *connected to*. I am reading a reference to write queries. This belongs somewhere else, or reduced to one sentence: "If you are wrapping your own database client instead of using `postgres()`, call `sql(...)` yourself — see X." +- **Line 135** — "On PostgreSQL `db.runtime()` is synchronous, so there is nothing to await." The useful half is "don't await it". The reason is implementation. +- **Line 339 and 453** — "TypeScript types `lateralJoin` as `never`, so the call does not compile". How the restriction is implemented. What I need: "Not available on SQLite; the call won't compile." +- **Line 341** — "`lateralJoin()`'s own first argument already names the derived table" plus the raw `subquery.getRowFields is not a function` error. The instruction "return the chain, don't call `.as()`" is enough. + +## A specific problem, not a comprehension one + +**Line 390** — "`options.nulls` | `'first' \| 'last'` | No | TypeScript accepts it, but nothing reads it: no `NULLS FIRST` or `NULLS LAST` reaches the SQL, so the database's own null ordering applies." + +Then **line 417** gives it a worked example titled "Control direction and null placement", whose comment says `// nulls adds nothing to the SQL`. The page documents a broken option and then demonstrates it. As a reader I assume the option works and I misread the caveat, or that the docs are papering over a bug. Either delete the option from the table and the example, or say plainly "`nulls` is currently ignored; do not use it." + +**Lines 135 and 161** are word-for-word identical from "A `select()`, `insert()`..." onward. On second reading I lost my place, thinking I had scrolled backwards. + +**Line 19** promises "The select and mutation examples use this `user` / `post` / `post_tag` / `tag` schema". The expanded schema also contains `Address`, `Task`, `Bug`, `Feature`, `user_type`, `Priority` and `Embedding1536`. Nothing in part 1 uses `Address`, `Task`, `Bug` or `Feature`, and they are the most unfamiliar syntax on the page. They made me stop and try to decode `@@base` and `@@discriminator` for no payoff. + +## Could I do what the page is for, after one reading? + +Partly. I could write a `select` with a `where`, an `innerJoin`, an `orderBy`, a `limit`, and a subquery join — the SQL concepts are ones I know and the examples are concrete and complete. The three-form table for `select()` (line 197) and the join examples are the strongest parts. + +What I still would not know: + +1. **How to get to line 1.** I cannot produce `contract.json` or `contract.d`, so I cannot run any example. That is a hard stop, and it is the page's own opening code block. +2. **What a "contract" or a "runtime" is**, beyond that I need both. +3. **How to use any of this on SQLite**, despite being told twice it is supported. +4. **Any raw expression whose type isn't text or int4**, because there is no codec list. +5. **When errors surface** — at `build()` or at `runtime.query()`. The page flags the distinction as important and then never applies it to `ORM.COLUMN_UNKNOWN` or `ORM.CAPABILITY_MISSING`. +6. **Whether `select()` accumulates or replaces** when called twice with column names. +7. **Whether the schema's new syntax matters to me.** `Uuid`, `@@type`, `@@discriminator`, `@@base` and the `types { }` block are all new since v7 and all unexplained. I do not know if I must adopt them to use `db.sql`, or if they are incidental to the examples. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part2.round1.md b/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part2.round1.md new file mode 100644 index 00000000000..b1b0866ca62 --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part2.round1.md @@ -0,0 +1,94 @@ +I read the file once, straight through, as the reader described. Here is what happened. + +## Sentences I could not restate after one reading + +**1. The aggregate note, line 38.** This is one sentence carrying six separate facts. + +> "For `count()` and integer `sum()`, a value outside the safe-integer range, ±(2^53 − 1), raises `RUNTIME.DECODE_FAILED` rather than answering with a rounded value (`avg()` is computed as `float8` and has no such guard), so reach for the lossless form when the magnitude is real: `countBigInt()` returns a `bigint`, `sumBigInt(field)` returns a `bigint`, and `avgDecimal(field)` returns an exact decimal string." + +What stopped me: the parenthetical about `avg()` interrupts the rule, `float8` is never explained, "reach for the lossless form when the magnitude is real" is a figure of speech I had to decode, and "answering with a rounded value" describes something that does *not* happen, so I had to hold a counterfactual in my head. Plainer: "If a `count()` or integer `sum()` result is larger than 2^53 − 1, the query fails with error code `RUNTIME.DECODE_FAILED`. Use `countBigInt()` or `sumBigInt()` instead when totals can get that large. `avg()` does not fail this way; it loses precision instead, so use `avgDecimal()` when you need an exact figure." + +**2. Line 366.** + +> "They come from the aggregate map in the contract written by `prisma contract emit`, which each target and extension contributes to, so which ones exist depends on your database." + +"Aggregate map", "target", and "extension" are all undefined here, and `prisma contract emit` appears for the first and only time on the page. I could not resolve any of them. Plainer: "Which aggregate functions you get depends on your database. PostgreSQL gives you these eight." + +**3. Line 379.** + +> "Any further function is registered dynamically: `ilike` is registered by the Postgres adapter for textual columns, while functions like `cosineDistance` come from an extension pack (pgvector) registered for your contract; without the matching adapter or extension pack, those functions are absent." + +Three unexplained terms ("registered dynamically", "adapter", "extension pack") and two ideas in one sentence. I do not know whether I have to do anything to get `ilike`, or whether it is just there. + +**4. Line 398.** + +> "A raw expression PostgreSQL computes as `numeric` still arrives as a string even when annotated `'pg/int4@1'`, because Prisma ORM does not convert the values a raw fragment returns." + +I read this three times. The page is telling me `.returns()` is a lie I tell TypeScript. That is a genuinely important warning and it is buried in a subordinate clause. It needs to be blunt: "`.returns()` only tells TypeScript what type to expect. It does not change the value you actually get back. If the real value is a string, your typed `number` will be a string at runtime." + +**5. Line 471, last sentence.** + +> "Since `8.0.0-rc.10` a buffered query hands its pooled connection back before you read the rows, so a paused loop no longer blocks other queries; a cursor stream keeps its connection until it finishes." + +"Buffered query" and "cursor stream" appear here for the first time. I do not know which one I get when I write `for await`, and the page never tells me how to ask for either. I could not restate this at all. + +**6. Line 471, "Each result is read once, one way."** Read once by whom? Does the array version stop me from also iterating? Does iterating twice throw? I guessed it means "you can `await` it or `for await` it, not both," but I am guessing. + +**7. Line 217.** + +> "On an adapter for a database without it, TypeScript types `returning` as `never`, so the call does not compile; calling it anyway throws an error whose `code` is `ORM.CAPABILITY_MISSING`." + +If it does not compile, how do I call it anyway? I could not work out the situation being described. + +## Words and phrases I had to guess + +- **`plan`** — every example names the variable `plan`, the prose calls it a "built query", and an anchor is `#executing-a-plan`. I guessed these are all the same thing. The page never says so. +- **`codecId`** — guessed: a string naming a database type. What `@1` means I have no idea; guessed a version number. +- **`'pg/int4@1'`, `'pg/text@1'`, `'pg/bool@1'`** — guessed these map to PostgreSQL `int4`, `text`, `boolean`. There is no list anywhere on the page, so I would not know what to write for a date, a UUID, or a numeric. +- **`1n`** (line 92) — guessed a JavaScript bigint literal. The page assumes I know, and it never explains why I would have reached for one. +- **"Gate the update with `where()`"** (lines 244, 289) — guessed "restrict which rows it touches". "Gate" is not a plain word for this. +- **"how it is stored and read comes from the target column"** (lines 182, 318) — guessed this means encoding and decoding. The phrase is repeated twice without ever being made concrete. +- **"function bag"** (line 352) — guessed "an object holding functions". +- **"family-agnostic utility"** (line 452) — guessed "works with more than just the SQL builder". "Family" is never defined. +- **"lossless form"** — guessed "the version that does not lose precision". Fine, but it appears before it is explained. +- **`distinctOn()`** (line 147) — appears once, in a list, never defined or shown. Guessed it is PostgreSQL `DISTINCT ON`. +- **"a `varchar` column widens to `text`"** (lines 376-377) — guessed this only matters for the TypeScript type. + +## Places I asked "so what do I actually type?" + +1. **Where does `runtime` come from?** Every single example calls `runtime.query(plan)`. I was told `db.runtime().query(...)` in the first half. No example on this page shows `const runtime = db.runtime()`. I would have copied an example and hit an undefined variable. +2. **Do I have to generate ids myself?** The schema says `id Uuid @id @default(uuid())`, but every insert example passes `crypto.randomUUID()` explicitly. So does `db.sql` apply the schema default or not? The page never says. This is the first thing I would try and the first thing that would break. +3. **What about insert conflicts?** No `ON CONFLICT`, no upsert, no mention that the topic exists. In ORM 7 I used upsert constantly. I do not know if it is unsupported, or somewhere else. +4. **Does `delete()` take arguments?** It is the only method with no Options section at all. I do not know if `delete()` is always bare. +5. **What happens if I `delete()` or `update()` without a `where()`?** Not stated. I would want that answered before I typed it. +6. **Which columns am I allowed to `select()` alongside a `groupBy()`?** SQL requires grouped or aggregated columns. The page never says what happens if I break that rule. +7. **Can I `groupBy` an alias?** In the computed-value example (lines 77-79) I have to retype the whole `EXTRACT(YEAR FROM ...)` expression in both `select()` and `groupBy()`. I would want to write `.groupBy('yearPlaced')`. The page does not say whether that works. +8. **`orderBy('totalAmount')` sorts by an aggregate alias** (line 158), but elsewhere `orderBy` takes column names. The page says "Sort by an aggregate alias to order groups" but does not say whether the alias must be one I selected, or what happens if it is not. +9. **When is `.returns()` not required?** The table says "Yes for a projected or compared value". I do not know what the third case is. +10. **Where do the imports come from?** `param` from `@prisma/orm-postgres/relational-core/expression`, `ResultType` from `@prisma/orm-postgres/components/runtime`. Two unrelated-looking subpaths of the same package, with no rule I can infer for a third import. +11. **How do I pick a `codecId` for a bare value?** Line 387 says "the builder picks the `codecId` from its JavaScript type" — but not which type maps to which codec, so I cannot predict when I need `param()`. +12. **`query` or `execute`?** Line 439 says `execute` is "for writes that return no rows". The insert-with-`returning` example uses `query`, the insert-without uses `execute`. So what do I call for an `update()` with no `returning()`, and what does `query` do if I use it on a write? Not stated. + +## Places the page explains internals when I only wanted to know what to do + +- **Line 366-379**, the whole explanation of contracts, targets, adapters, extension packs, and dynamic registration. I wanted a list of functions I can call. I got the tool's plugin architecture. +- **Line 176**: "Multiple rows are inserted in one `INSERT ... VALUES (...), (...)` statement, not one round-trip per row." This is the implementation. It happens to be reassuring, but it is not something I do. +- **Line 398**: "It does not cast the value in SQL or change how the driver decodes it." I wanted "you may get a string, check it." I got the mechanism. +- **Line 471**: pooled connections, paused loops, cursor streams. I wanted "use `for await` if the result set is big." +- **Line 92**: "PostgreSQL evaluates the comparison server-side." I wanted "pass a plain number." The reason why does not help me type anything. +- **Line 82-83**: the whole explanation of why `yearPlaced` is `'2024'` rather than `2024`. Correct and necessary, but delivered as a code comment about internals rather than as a warning I can act on. + +## Could I do what the page is for, after one reading? + +Partly. I could write a `groupBy` with a `count`, a `having` with a sum threshold, a multi-row insert, and an update with `returning`. Those four patterns are shown clearly enough to copy. + +I could not: + +- **Run any of it.** I do not have `runtime`. Every example uses it; none creates it. +- **Insert a row without guessing.** I do not know whether schema defaults apply, so I do not know whether `id` is mine to supply. +- **Write any `fns.raw` of my own beyond copy-paste.** I have three `codecId` strings from examples and no list, no naming rule, and no way to find one for a date or a numeric. +- **Predict what `.returns()` gets me.** After reading line 398 I know the annotation can be wrong, but I do not know which cases are safe, so I would have to test every raw expression by hand. +- **Handle insert conflicts at all.** The topic is absent. +- **Decide between `query` and `execute`, or between collecting and streaming**, with any confidence. The last section describes two internal modes I was never taught how to select. + +The single biggest problem is the missing `runtime` definition. The second is that `codecId` is required in six places and defined in none. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part1.round1.md b/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part1.round1.md new file mode 100644 index 00000000000..cd1d98358bd --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part1.round1.md @@ -0,0 +1,97 @@ +Read once, top to bottom, as a Prisma 7 user. Blunt notes. + +## Sentences I could not restate after one reading + +1. **"Pass the contract with `contractJson` (a JSON contract you import) or `contract` (a contract value). Supply exactly one."** (line 23) + What stopped me: I do not know what a contract is. In Prisma 7 I had a schema and a generated client. The page never says what a contract is, where `./contract.json` and `./contract.d` come from, or what generates them. "A JSON contract you import" defines the word with the word. And I cannot tell when I would use `contract` instead of `contractJson` — the page gives the choice and no reason to pick. + Plainest wording I would have understood: "A contract is the generated description of your database shape. `prisma generate` writes `contract.json` and `contract.d.ts` into X. Import the JSON and pass it as `contractJson`. Pass `contract` instead only if Y." + +2. **"`verifyMarker` | `'onFirstUse'` or `false` | Whether the runtime reads the contract marker row on the first query and warns when it does not match."** (line 39) + What stopped me: "contract marker row". I have never heard of it. Which table is it in? Who writes it? What does "does not match" mean, and what do I do when I see the warning? This is the only mention on the page. + +3. **"If your contract uses types from an extension pack, pass the pack in `extensions` (for example `extensions: [pgvector]`)."** (line 26) + What stopped me: "extension pack" is undefined, and I do not know how to tell whether my contract uses one. `pgvector` is shown as a bare identifier with no import line. Where does it come from — `@prisma/...`? The `pgvector` npm package? + +4. **"`middleware` | Array of middleware | Middleware applied to every execution."** (line 38) + What stopped me: circular. It tells me middleware is middleware. No shape, no signature, no example, no link. "every execution" — execution of what, a query? + +5. **"`mode` | `'strict'` or `'permissive'` | The mode middleware reads on every execution. Defaults to `'strict'`."** (line 193) + What stopped me: this is the one MongoDB option that sounds like it changes behaviour, and the description explains it in terms of an internal mechanism ("the mode middleware reads") rather than telling me what strict does differently from permissive. After one reading I have no idea what setting this to `'permissive'` would let me do. + +6. **"Transactions are fully supported on PostgreSQL and not shipped on MongoDB."** (line 9) versus **"(see [Transactions (MongoDB)](#transactions-mongodb) ...)"** (line 260) + What stopped me: the intro says there are no MongoDB transactions, then a link points to a MongoDB transactions section. One of these is wrong, and I cannot tell which from here. Also "not shipped" — does that mean not yet, or never? I would say "MongoDB transactions are not supported in this release." + +7. **"Returns a `Promise`. The runtime is what runs built queries directly (`runtime.query(...)` for rows, `runtime.execute(...)` for writes that return none), opens connections (`runtime.connection()`), prepares statements (`runtime.prepare(...)`), and reports telemetry (`runtime.telemetry()`)."** (line 78) + What stopped me: five new APIs in one sentence, none of them explained or linked, plus "built queries" which is undefined at this point in the page. "opens connections (`runtime.connection()`)" is the worst of it — I just connected; what further connection is this and why would I want one? Split it into a list with a link per item. + +8. **"The first connection can happen implicitly, the moment any query builds the runtime, so a later explicit `connect()` collides the same way."** (line 233) + What stopped me: "the moment any query builds the runtime" reads backwards — I thought the runtime runs queries, not that queries build the runtime. Plainer: "Running any query connects the client. If you then call `connect()`, it fails, because the client is already connected." + +9. **"`connect()` accepts a binding, so you can defer the choice of database: `mongo({ contractJson })` then `await db.connect({ url, dbName })`."** (line 234) + What stopped me: "defer the choice of database" — why would I want to? No situation is given. Also this contradicts the `mongo(options)` Options table, which marks a binding as required ("One binding"). + +10. **"Returns an [`AsyncIterableResult`](#asynciterableresult): `await` it for an array, or `for await` to stream rows."** (line 283) + What stopped me: the type name. The link goes to part two, which I do not have, so at this point the page defines it only by how you consume it. I could guess, but I could not restate what it actually is. + +11. **"`runtime.execute(built)` is the sibling call for writes that return no rows; it resolves the statement's statistics."** (line 284) + What stopped me: "resolves the statement's statistics". What statistics? Row count? I would write "returns the number of rows affected" if that is what it is. + +12. **"`await using` requires `@types/node` (which augments `SymbolConstructor` with `asyncDispose`) or a `lib` that declares the symbol."** (line 155) + What stopped me: the parenthetical is TypeScript internals I do not need. And "or a `lib` that declares the symbol" does not tell me which lib value to set. Plainer: "`await using` needs `@types/node` installed, or `"lib": ["esnext.disposable"]` in your tsconfig." + +13. **"This is what lets you share one `MongoClient` between Prisma ORM and driver-level code."** (line 181) + What stopped me: "driver-level code" — I guessed it means code I write against the `mongodb` package directly, but the page never says so. + +## Words and phrases I had to guess + +- **"contract"** — guessed: the new name for the generated schema/client types. +- **"binding"** — guessed: the way you tell the client which database to talk to. The page uses it as a noun ("One binding" in the Required column) before defining it, and I had to read line 24 to work out that "Required: One binding" means "pass exactly one of these three rows". +- **"runtime"** — guessed: the connected thing that actually talks to the database, as opposed to the client object. +- **"built query" / "a built query" / `built`** — guessed: the object a builder returns when you call `.build()`. Never stated. +- **"extension pack"** — guessed: a plugin that adds support for a Postgres extension's types. +- **"contract marker row"** — guessed: a row Prisma writes somewhere to check the deployed database matches the contract. No idea if that is right. +- **"telemetry"** — guessed: query timings/metrics. +- **"pipeline builder"** — guessed: a MongoDB aggregation-pipeline API. Only appears as a link. +- **"`db.sql.public.tag`"** (line 95) — guessed: schema `public`, table `tag`. Never explained; it just appears mid-example. +- **"`.orm`, `.sql`, `.raw`, `.enums`, `.nativeEnums`, `.contract`, `.context`"** (line 45) — guessed at most of these. `.nativeEnums` versus `.enums` I could not guess at all. `.context` I could not guess at all. +- **"not shipped"** — guessed: not implemented in this release. +- **"no-op"** (line 181) — fine for me, but it is jargon sitting in the middle of the one paragraph about who closes what. + +## Places I asked "so what do I actually type?" + +1. **Where `contract.json` and `contract.d` come from.** Every example imports them. No example shows the command that produces them, and no link is offered. This is the first thing I would need and the page never says it. +2. **How to install.** `import postgres from '@prisma/orm-postgres/runtime'` — is that a package I install, and with what? Same for `@prisma/orm-mongo/runtime`. +3. **`middleware`.** I cannot write one. No signature, no example, no link. +4. **`extensions: [pgvector]`.** Where do I import `pgvector` from? +5. **`contract` (the non-JSON form).** No example anywhere on the page shows it being used. +6. **`runtime.connection()`, `runtime.prepare()`, `runtime.telemetry()`.** Named once in a sentence with no example and no link. +7. **`binding` as an object.** `{ kind: 'url' | 'pgPool' | 'pgClient', ... }` — the `...` is the part I need. What are the other fields for each kind? Same for MongoDB, where the page says "a `binding` object that names its kind" and never even lists the kinds. +8. **When to call `connect()` on PostgreSQL at all.** Line 27 says it is optional; line 94's example calls it. I do not know which one I should do. +9. **Can I call `db.runtime()` on PostgreSQL before connecting?** It is synchronous and the client connects lazily, so this is the obvious question. The page only tells me what happens after `close()`. +10. **What `close()` does to in-flight queries.** "Close the client and release its pool" — does it wait for running queries? +11. **`uri` versus `url` on MongoDB.** Both are connection strings. The only stated difference is that `dbName` is optional with one and required with the other. I would not know which to type, or why both exist. +12. **Catching the errors.** The page repeatedly says things reject "with an error whose `code` is `DRIVER.NOT_CONNECTED`". It never shows the import or the shape, so I cannot write the `catch` block. Is `DRIVER.NOT_CONNECTED` a string I compare against, or a constant I import? +13. **The MongoDB example's `db.query.from('posts')`** (line 297) — `'posts'` is a bare string with no explanation of where the name comes from, and `db.query` is described in the return table only as `.query`. + +## Places that explain the internals when I only wanted to know what to do + +- **"Whether the runtime reads the contract marker row on the first query"** (line 39) — tells me the mechanism, not whether I should ever turn it off. +- **"The mode middleware reads on every execution"** (line 193) — describes plumbing instead of the behaviour I would observe. +- **"which augments `SymbolConstructor` with `asyncDispose`"** (line 155) — compiler internals in place of the tsconfig line I need. +- **"The underlying driver connection is built lazily and asynchronously on first use."** (line 258) — this one earns its place, because it explains why I must `await`. But it is immediately followed by a repeat of the same point, and the whole remark says the PostgreSQL/MongoDB difference three times in one paragraph (line 258) after already saying it at line 104 and line 171. +- **"the moment any query builds the runtime"** (line 233) — internal ordering, where what I need is "running a query connects you". + +## Could I do what the page is for, after one reading? + +Partly. I could create a PostgreSQL client from a connection string, connect it, close it, and use `await using` — those four have clear examples. I could do the same on MongoDB, and I understood the ownership rule (if I pass my own pool or `MongoClient`, I close it; if I pass a URL, Prisma closes it). That rule is the clearest thing on the page. + +What I would still not know: + +- **What a contract is or how to get one.** This blocks every example. I cannot run a single code block on this page without already having `contract.json`, and the page does not tell me how to get it or link anywhere that does. The intro says the page "assumes a project that already exists", but a Prisma 7 project does not have a contract, and I am the exact reader who is upgrading. +- **How to run a query.** The only PostgreSQL query example is one line, `db.sql.public.tag.select('id', 'label').build()`, with no explanation of any part of it. I would not know whether to reach for `db.orm`, `db.sql`, `db.raw`, or `runtime.query`. +- **Whether MongoDB has transactions.** The page tells me both. +- **What `mode: 'permissive'` does.** +- **How to handle a connection error**, because I cannot write the check against `DRIVER.NOT_CONNECTED`. +- **What most of the client surface is.** Line 45 lists eleven members; the page documents five. + +One more thing that tripped me up specifically as a reader of both halves: the MongoDB **Return type is `MongoClient`** (line 199), and two lines below, an example imports a completely different `MongoClient` from the `mongodb` package (line 217). Two different things with the same name on the same screen. I read line 199 as "this returns the driver's client" and had to back up. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part2.round1.md b/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part2.round1.md new file mode 100644 index 00000000000..d73e3bc78d6 --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part2.round1.md @@ -0,0 +1,92 @@ +Read once, top to bottom, as a Prisma 7 user. + +## Sentences I could not restate after one reading + +1. `"- Inside the callback, query through the `tx` handle, not `db`: `tx.orm` for models, `tx.sql` with `tx.query(...)` (rows) or `tx.execute(...)` (writes that return none) for queries built with the SQL builder, and `tx.enums` and `tx.nativeEnums` for enum members."` + Five ideas in one sentence, and it introduces `tx.enums` and `tx.nativeEnums` with no explanation of what either is or how they differ. "writes that return none" made me stop: none of what? Plainer: "Query through `tx`, not `db`. `tx.orm` holds your models. Build SQL with `tx.sql`, then run it with `tx.query(...)` to get rows back, or `tx.execute(...)` when the write returns no rows." Then define the enum properties separately, or drop them. + +2. `"`connection.destroy(reason)` evicts that one connection from the pool instead of reusing it: Prisma ORM passes a truthy error to `pg`'s `PoolClient.release(err)`, which tells the pool to drop the client."` + I wanted to know what `destroy` does to me. Instead I got the node-postgres call Prisma makes internally. Plainer: "`destroy(reason)` throws the connection away instead of returning it to the pool." + +3. `"`reason` is optional and advisory: the pool only reads whether it is set."` + I read this three times. I think it means the error I pass is never looked at, only its presence. But then why does the example pass `new Error('connection no longer trusted')`? So what do I actually type — `destroy()` or `destroy(someError)`? + +4. `"As designed, it will require a MongoDB replica set and must not silently degrade to non-transactional execution on a standalone server."` + "must not silently degrade" is a requirement written for whoever builds this, not for me. I cannot act on it. The whole paragraph at line 219 describes software that does not exist; the note above it already told me that. + +5. `"Same result as `runtime.prepare(...)`, but the callback receives **two** arguments, `(sql, params)`, because the client has no closed-over SQL builder for you to reach for."` + "closed-over" is a compiler word explaining why the API is shaped this way. I only needed: "off the client, the SQL builder is handed to you as the first argument." + +6. `"A raw query ending with `.affectedCount()` prepares into a `PreparedExecution` instead, consumed with `execute(target, params)`, which resolves the statement's statistics."` + Three undefined things at once: "raw query", `.affectedCount()`, and "the statement's statistics". What statistics? A row count? There is no example, no options table, and no return type for `PreparedExecution` anywhere on the page. + +7. `"The runtime also checks the signal between rows, and passes it into the calls that read each value."` + Internals. And the next clause tells me the phase can be `'stream'`, `'encode'`, or `'decode'` "depending on where it landed", but never says which is which. If I have to branch on `details.phase`, I still cannot. + +8. `"It is the label middleware reads to tell a top-level query from one inside a dedicated connection or a transaction."` + "middleware" appears here and in the options table and nowhere else on the page. I do not know what middleware is in this product, how to write one, or how it gets this label. Nothing tells me why I would ever set `scope` myself, and the sentence after says setting it does nothing except change the label. + +9. `"After a query, it returns an object of the shape `{ lane, target: 'postgres', fingerprint, outcome, durationMs? }`"` + `lane` and `fingerprint` are never explained, and they are two of the five fields. I cannot use this object. + +10. `"A result is consumed once per mode."` + "mode" is undefined. From the next clause I worked out that the two modes are `await` and `for await`, but the sentence alone told me nothing. + +11. `"On PostgreSQL, a query run through the client buffers its rows and hands its pooled connection back before you read them, so a result you hold or iterate slowly no longer keeps a connection checked out."` + One long sentence about pool mechanics. The part I care about is the last clause. "no longer" also implies a change from version 7 without saying so. + +## Words and phrases I had to guess + +- `pg/text@1`, `pg/int4@1` — I guessed these are Postgres type names with a version number. I have no idea what the id is for uuid, timestamp, numeric, or an array, and the page never links a list. +- "type id" — guessed it means a string naming a column type. +- `@prisma/orm-postgres/family-runtime` — "family-runtime" means nothing to me. Guessed it is just where lower-level helpers live. +- "a bare transaction context" — guessed: an object with only `query` and `execute`. +- "or a standalone SQL builder" — guessed there is some way to make a SQL builder without a client. The page never says how. +- `Promise` — guessed `R` is whatever my callback returns. +- "plan" — the anchor `#run-a-sql-builder-plan-with-txsql-and-txexecute`, `SqlQueryPlan`, and `(params) => plan` all say "plan", while the prose says "a built query". I guessed they are the same thing. Pick one word. +- `db.connect()` — appears at line 135 with no introduction. I already know `db.runtime()` from the first half. I guessed `connect()` opens the connection and returns the same runtime, mostly because line 287 says calling both is an error. +- "lane", "fingerprint" — no guess. I have nothing. +- `all()` and `createAll()` — assumed these came from the first half. `createAll()` being listed under "the calls that run a read query" still looks wrong to me. + +## Where I asked "so what do I actually type?" + +- **Isolation levels.** `"There are no `isolationLevel`, `timeout`, or `maxWait` options"`. In version 7 I set `isolationLevel: 'Serializable'` on a transaction. The page tells me the option is gone and then stops. Do I run `SET TRANSACTION ISOLATION LEVEL` myself through `tx.execute`? Through the manual connection? It does not say. +- **Timeouts.** Same sentence removes `timeout` and `maxWait`. Is `signal` the replacement? The page never connects them. +- **Where `runtime` comes from** in "Manual connection and transaction control". Line 182 starts `const connection = await runtime.connection();` with no preceding line defining `runtime`. Same in the `runtime.prepare` examples. +- **Type ids.** I cannot prepare a statement on anything but text without knowing the id, and there is no list or link. +- **`PreparedExecution`.** No example. I do not know what `execute(target, params)` resolves to, so I cannot write the line after it. +- **Nested transactions.** `"transactions do not nest"`. My version 7 code calls a service function that opens its own transaction. What do I do instead — pass `tx` down? The page names the limit and offers nothing. +- **Retries.** No mention of what to do when a transaction fails and should be retried. +- **`RuntimeExecuteOptions` reach.** `"Every runtime `query(...)` and `execute(...)` call accepts a `RuntimeExecuteOptions` object as its second argument."` Does `tx.query` take it? `connection.query`? `ps.query`'s second argument is already `params`, so how do I cancel a prepared statement? +- **Which transaction API to pick.** Three levels are announced, but nothing says when `withTransaction` beats `db.transaction`, or when manual control beats either. "A lower-level transaction helper you import directly" is not a reason to use it. +- **Error codes.** `RUNTIME.TRANSACTION_CLOSED`, `RUNTIME.PREPARE_UNUSED_PARAM`, `RUNTIME.ABORTED`, `RUNTIME.ITERATOR_CONSUMED`, `DRIVER.ALREADY_CONNECTED` all appear as bare strings. Nothing shows me how to read `code` off a caught error, and there is no link to a code list. + +## Where it explains the inside when I wanted the outside + +- The `pg` / `PoolClient.release(err)` explanation in `destroy`. +- "the client has no closed-over SQL builder for you to reach for". +- "The runtime also checks the signal between rows, and passes it into the calls that read each value." +- The buffering paragraph at line 434. +- The whole MongoDB planned-design paragraph at line 219. + +## Two things that look like mistakes + +- The heading `##### Run a SQL builder query with `tx.sql` and `tx.execute`` carries the anchor `#run-a-sql-builder-plan-with-txsql-and-txexecute`. Text and anchor disagree. +- The "Options" tables under "Manual connection and transaction control" list method names in a Name/Type/Required table with `—` in Required for every row. Those are not options, and the table gives me no information. +- The return type table says `ps.query` returns `AsyncIterableResult`, but the example does `await ps.query(...)` and then `inTx[0]!.id`. So an awaited result is an array. Nothing states that plainly. + +## Could I do what the page is for, after one reading? + +Partly. + +I could write a `db.transaction(async (tx) => ...)` and port my version 7 `$transaction([...])` array. The diff at line 91 is the clearest thing on the page and it told me exactly what to do. + +I could not: + +- Set an isolation level or a timeout. Version 7 code of mine does both. +- Restructure the nested transactions my service layer has today. +- Prepare a statement for any parameter type other than text. +- Use `PreparedExecution` at all. +- Do anything with `scope`, or with the telemetry object's `lane` and `fingerprint`. +- Decide between `db.transaction`, `withTransaction`, and manual connections. +- Handle the abort phases, because I cannot tell `'encode'` from `'decode'`. \ No newline at end of file From 3fe433f1f7862de02234e6bc03209a42fc15d540 Mon Sep 17 00:00:00 2001 From: willbot Date: Sun, 13 Sep 2026 08:51:20 +0200 Subject: [PATCH 05/14] docs(orm8): orm/reference pages, reader-review round one applied Seven readers, seven fixers, one per page half. Wording, order, and examples; facts unchanged except where a reader's question had a verified answer: contract defaults apply on SQL-builder insert(), no ON CONFLICT in the SQL builder, select() adds rather than replaces, the pipeline builder's full helper and update-operator lists, redact() and unwind() and the option-object stages shown, the client's binding forms and verifyMarker, the abort phases, PreparedExecution. The pipeline page grew by about 100 lines of examples readers asked for; the other pages held their length. Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- .../docs/content/docs/orm/reference/index.mdx | 25 +- .../docs/orm/reference/pipeline-builder.mdx | 487 +++++++++++------- .../docs/orm/reference/raw-queries.mdx | 139 +++-- .../docs/orm/reference/sql-query-builder.mdx | 179 ++++--- .../reference/transactions-and-runtime.mdx | 235 +++++---- apps/docs/cspell.json | 4 +- .../c21-orm-reference/conventions.md | 2 +- 7 files changed, 583 insertions(+), 488 deletions(-) diff --git a/apps/docs/content/docs/orm/reference/index.mdx b/apps/docs/content/docs/orm/reference/index.mdx index b51ca6da61f..d3876affa9f 100644 --- a/apps/docs/content/docs/orm/reference/index.mdx +++ b/apps/docs/content/docs/orm/reference/index.mdx @@ -6,21 +6,20 @@ metaTitle: Prisma ORM API reference metaDescription: 'Reference index for the Prisma ORM client, SQL query builder, pipeline builder, raw queries, and runtime APIs.' --- -Prisma ORM has three query surfaces. The **ORM client** gives you model-level methods like `where()`, `create()`, and `include()`, and works against both PostgreSQL and MongoDB. The **SQL query builder** gives you table-level, SQL-shaped methods like `select()`, `innerJoin()`, and `groupBy()`, and today targets PostgreSQL only. The **pipeline builder** gives you a typed way to build MongoDB aggregation pipelines through `db.query`. +Prisma ORM gives you three ways to write a query. The **ORM client** is at `db.orm`. It has model methods such as `where()`, `create()`, and `include()`, and it works on both PostgreSQL and MongoDB. The **SQL query builder** is at `db.sql`. It has table methods that match SQL, such as `select()`, `innerJoin()`, and `groupBy()`, and today it works on PostgreSQL only. The **pipeline builder** is at `db.query`. It builds MongoDB aggregation pipelines. -Use the ORM client for everyday application queries across models and relations. Reach for the SQL query builder when you need a join, aggregate, or SQL feature the ORM client doesn't expose on PostgreSQL, and the pipeline builder for MongoDB aggregation pipelines. When even those can't express a query, drop to a [raw query](/orm/reference/raw-queries); for client lifecycle, transactions, and prepared statements, see [Transactions and runtime](/orm/reference/transactions-and-runtime). +Use the ORM client for everyday reads and writes across your models, including loading related rows with `include()`. Use the SQL query builder when the result you want is not a model row, such as a projection across two tables or an aggregate over a join. Use the pipeline builder for MongoDB aggregation. When none of them can express the query, drop to a [raw query](/orm/reference/raw-queries). For connecting and disconnecting the client, transactions, and prepared statements, see [Transactions and runtime](/orm/reference/transactions-and-runtime). In Prisma ORM 7 you passed `findMany({ where, include })` arguments; those arguments are chained methods now, and [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7) maps each one. -## How database differences are documented +```ts +const admins = await db.orm.public.User.where((u) => u.kind.eq('admin')).all(); -The method reference pages document each method with the classic Remarks / Options / Return type / Examples structure (the raw queries page is deliberately more narrative). When a method's behavior differs between PostgreSQL and MongoDB, exists on only one database, or is type-checked but not enforced at runtime, the method's own Remarks call that out inline. +const plan = db.sql.public.user.select('id', 'email').where((f, fns) => fns.eq(f.kind, 'admin')).build(); +const rows = await runtime.query(plan); +``` -For a conceptual walkthrough of reading, writing, and querying data (rather than an exhaustive method-by-method reference), see the [Fundamentals](/orm/fundamentals/reading-data) section: +## How database differences are documented -- [Reading data](/orm/fundamentals/reading-data) -- [Writing data](/orm/fundamentals/writing-data) -- [Relations and joins](/orm/fundamentals/relations-and-joins) -- [Transactions](/orm/fundamentals/transactions) -- [Advanced queries](/orm/fundamentals/advanced-queries) +Every method on a reference page has the same four parts: Remarks, Options, Return type, and Examples. The raw queries page uses prose instead. If a method works differently on PostgreSQL and MongoDB, or exists on only one of them, its Remarks say so. For a walkthrough of reading and writing data rather than a method list, see [Reading data](/orm/fundamentals/reading-data), [Writing data](/orm/fundamentals/writing-data), [Relations and joins](/orm/fundamentals/relations-and-joins), [Transactions](/orm/fundamentals/transactions), and [Advanced queries](/orm/fundamentals/advanced-queries). }> @@ -30,12 +29,12 @@ For a conceptual walkthrough of reading, writing, and querying data (rather than Every SQL query builder method for building typed, table-level queries against PostgreSQL. }> - Every MongoDB pipeline-builder stage, accumulator, expression helper, and write terminal. + Every MongoDB pipeline-builder stage, accumulator, expression helper, and write method. }> - Raw escape hatches: PostgreSQL raw SQL fragments and MongoDB raw commands. + Raw SQL on PostgreSQL and raw commands on MongoDB, for queries the typed APIs cannot express. }> - Client lifecycle, transactions, prepared statements, and execution options. + Connecting and disconnecting the client, transactions, prepared statements, and execution options. diff --git a/apps/docs/content/docs/orm/reference/pipeline-builder.mdx b/apps/docs/content/docs/orm/reference/pipeline-builder.mdx index 455d371fd1a..7c596333902 100644 --- a/apps/docs/content/docs/orm/reference/pipeline-builder.mdx +++ b/apps/docs/content/docs/orm/reference/pipeline-builder.mdx @@ -8,11 +8,9 @@ metaDescription: 'Reference for the Prisma ORM MongoDB pipeline builder''s stage The pipeline builder gives you a typed way to build MongoDB aggregation pipelines. You reach it through `db.query`, chain aggregation stages onto a starting collection, and finish the chain with the call that builds the query. It is the MongoDB counterpart to the [SQL query builder](/orm/reference/sql-query-builder): a lower-level API for the queries the [ORM client](/orm/reference/orm-client) doesn't express. -The pipeline builder is aggregation-only by design. There is no `find` and no `distinct`; to fetch a single document, filter and then `limit(1)`. Everything the builder produces compiles to a MongoDB aggregation pipeline. +Everything the builder produces compiles to a MongoDB aggregation pipeline. There is no `find` and no `distinct`. To fetch a single document, filter and then `limit(1)`. Use the ORM client (`db.orm`) for everyday reads and writes across models and relations, and the pipeline builder when you need a stage the ORM client doesn't expose: grouping, `$lookup` joins with post-processing, computed projections, multi-stage transformations, or aggregation-time writes with `$out` / `$merge`. -Use the ORM client (`db.orm`) for everyday reads and writes across models and relations. Drop down to the pipeline builder when you need a stage the ORM client doesn't expose: grouping, `$lookup` joins with post-processing, computed projections, multi-stage transformations, or aggregation-time writes with `$out` / `$merge`. - -This page documents every stage, accumulator, expression helper, and write method, with the caveats that apply to each. Where Prisma ORM's own tests do not run a method against a live MongoDB, the Remarks say so. +This page documents every stage, accumulator, expression helper, and write method, with the caveats that apply to each. Where a method has not been verified against a running MongoDB, the Remarks say so. :::note[This is the MongoDB pipeline builder] The pipeline builder targets MongoDB only. PostgreSQL has no pipeline builder: use the [SQL query builder](/orm/reference/sql-query-builder) for PostgreSQL joins and aggregates, and the [ORM client](/orm/reference/orm-client) for everyday queries on either database. @@ -20,7 +18,7 @@ The pipeline builder targets MongoDB only. PostgreSQL has no pipeline builder: u ## Example schema -All examples on this page run against the following schema. `Post` is a discriminated model (`Article` and `Tutorial` variants share the `posts` collection, discriminated by `kind`). +All examples on this page run against the schema below. It is the same MongoDB schema as the [ORM client](/orm/reference/orm-client) page, so examples carry over between the two pages. The `posts` collection holds two kinds of document, `Article` and `Tutorial`, told apart by the value of the `kind` field. No example on this page uses them.
@@ -85,22 +83,25 @@ model Tutorial { Every pipeline starts with `db.query.from(root)` and ends with a call that turns the chain into a query you can run. Between them you chain stages. +`db` is the client you create with `mongo(...)`. See [Transactions and runtime](/orm/reference/transactions-and-runtime#mongooptions) for how to create it. Your contract is the `contract.prisma` file that replaced `schema.prisma`. Running `npx prisma contract emit` compiles it into two generated files: `contract.json`, read at run time, and `contract.d.ts`, read by TypeScript. [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7) has the setup steps. + ### `from()` Enter the pipeline builder on a collection. #### Remarks -- MongoDB only. `db.query` is the pipeline-builder entry point on the client, the same object the standalone `mongoQuery(...)` returns. -- `from(root)` takes a contract **root name**: the collection name from the contract's `roots` map (`'posts'`, `'users'`), the same names the ORM client uses (`db.orm.users`), not the model names. The collection name is the model's `@@map` value, or the model name with a lowercase first letter when there is no `@@map`. -- Passing an unknown root throws right away, with an error whose `code` is `ORM.MODEL_UNKNOWN` and whose message is `Unknown root: "". Valid roots: ...`. -- `from()` returns a builder you chain stages onto. The builder moves through three states as you chain: a starting collection, a filtered collection after `match()`, and a pipeline chain after any other stage. Each state exposes the methods that are valid at that point (for example, the root-level writes `insertOne` / `insertMany` are only available before you add stages). +- MongoDB only. `db.query` is the pipeline-builder entry point on the client. +- `from()` takes the collection name, the same name you use on `db.orm` (`'posts'`, `'users'`), not the model name `Post`. The collection name is the model's `@@map` value, or the model name with a lowercase first letter when there is no `@@map`. +- Passing an unknown collection name throws right away, with an error whose `code` is `ORM.MODEL_UNKNOWN` and whose message is `Unknown root: "". Valid roots: ...`. +- `from()` returns a builder you chain stages onto. Some methods are only available before you add stages, `insertOne` and `insertMany` for example. If a method is missing from autocomplete, you have already chained past it. +- You can build the same pipelines without a client. `import { mongoQuery } from '@prisma/orm-mongo/query-builder'`, then `mongoQuery({ contractJson }).from('posts')`, passing the `contract.json` you import. #### Options | Name | Type | Required | Description | |---|---|---|---| -| `root` | Contract root name (`string` literal) | Yes | The collection to aggregate over. | +| `root` | Collection name (`string` literal) | Yes | The collection to aggregate over. | #### Return type @@ -113,19 +114,27 @@ Enter the pipeline builder on a collection. ##### Enter the builder on a collection ```typescript +const runtime = await db.runtime(); + const plan = db.query.from('posts').build(); -const posts = await (await db.runtime()).query(plan); +const posts = await runtime.query(plan); ``` +Every later example on this page assumes you already have `runtime` from `await db.runtime()`. + ### Building and executing a pipeline -A pipeline is inert until you run it. `build()` (or its alias `aggregate()`) turns the chain into a plan; `(await db.runtime()).query(plan)` runs it. +Nothing runs until you execute the pipeline. `build()` turns the chain into a built query. `runtime.query(...)` runs it. #### Remarks -- `runtime.query(plan)` is the executor. It accepts any plan, including one built by the pipeline builder. On MongoDB `db.runtime()` returns a promise, so the call reads `(await db.runtime()).query(plan)`; hold the runtime in a variable if you run several plans. It returns an `AsyncIterableResult`: `await` it for an array of documents, or `for await` over it to take one at a time. There is no `db.execute` on the MongoDB client. -- **Read results are decoded, up to a point.** Unlike [raw queries](/orm/reference/raw-queries), a pipeline built through `db.query` carries a result shape, so `_id` comes back as a decoded hex string and other fields come back as their contract types. Only `match()`, `sort()`, `limit()`, `skip()`, `sample()`, `project()`, `addFields()`, and `vectorSearch()` carry that shape forward. -- Every other stage, `lookup()` and `group()` included, drops the result shape. From that point the whole row comes back as the driver gives it: a looked-up sub-document's `_id`, and the top-level `_id` beside it, are raw driver `ObjectId` values. Compare them with `String(...)`. +- `build()` and `aggregate()` are two names for the same method. This page uses `build()`. +- On MongoDB, `db.runtime()` returns a promise, so you must await it: `const runtime = await db.runtime()`. Hold it in a variable and reuse it. +- `runtime.query(built)` runs any built query, including one from the pipeline builder. +- `await` that call for an array of documents, or `for await` over it to take one document at a time. +- There is no `db.execute` on the MongoDB client. +- **Read results are decoded.** Prisma ORM converts each field from what the driver returns into the type your contract gives it: `_id` becomes a hex string, and a date becomes the date type in your contract. +- Only `match()`, `sort()`, `limit()`, `skip()`, `sample()`, `project()`, `addFields()`, and `vectorSearch()` keep that. After any other stage, `lookup()` and `group()` included, documents come back exactly as MongoDB returns them. Both a looked-up sub-document's `_id` and the top-level `_id` are then the driver's own `ObjectId` values. Compare two of those with `String(a._id) === String(b._id)`. #### Examples @@ -133,7 +142,7 @@ A pipeline is inert until you run it. `build()` (or its alias `aggregate()`) tur ```typescript const plan = db.query.from('posts').sort({ createdAt: 1 }).build(); -const posts = await (await db.runtime()).query(plan); +const posts = await runtime.query(plan); ``` ## Pipeline stages @@ -147,8 +156,8 @@ Filter documents by a predicate. #### Remarks - `match()` takes a callback whose `f` argument has one property per field, and returns a filter expression (`(f) => f.kind.eq('tutorial')`). -- `match()` filter values are **not encoded to the stored type**. The value you pass is compared as-is against the stored value. For most scalar fields this is what you want, but it breaks down for `_id` (see the warning below). -- Leaf fields expose these filter operators: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`, `exists`, and `type`. `eq` and `expr(...)`-wrapped comparisons run in Prisma ORM's own tests; the remaining operators follow the matching MongoDB query operators (`$ne`, `$gt`, `$in`, `$exists`, `$type`, and so on). +- The value you pass is compared exactly as you wrote it, with no conversion into the stored type. That is what you want for most scalar fields. It is why an `_id` filter never matches: your hex string is compared as a string against a stored `ObjectId`. See the warning below. +- Leaf fields expose these filter operators: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`, `exists`, and `type`. They work like the MongoDB query operators of the same name (`$ne`, `$gt`, `$in`, `$exists`, `$type`, and so on). - For an aggregation-expression predicate (comparing computed values rather than a field against a constant), wrap it in `expr(...)`: `match((f) => expr(fn.gt(fn.year(f.createdAt), fn.literal(2023))))`. See [Expression helpers](#expression-helpers). #### Options @@ -172,7 +181,7 @@ const plan = db.query .from('posts') .match((f) => f.kind.eq('tutorial')) .build(); -const tutorials = await (await db.runtime()).query(plan); +const tutorials = await runtime.query(plan); ``` ##### Aggregation-expression predicate @@ -184,11 +193,11 @@ const plan = db.query .from('posts') .match((f) => expr(fn.gt(fn.year(f.createdAt), fn.literal(2023)))) .build(); -const recent = await (await db.runtime()).query(plan); +const recent = await runtime.query(plan); ``` :::warning[`_id` equality filters do not work through the pipeline builder] -Filtering by `_id` equality inside `match()` **never matches any document**. Neither a hex string (`f._id.eq('507f...')`) nor a real driver `ObjectId` matches: the pipeline builder's value resolver walks the value as a plain object, destructuring an `ObjectId` into its internal `buffer` bytes, so the filter that reaches MongoDB never carries a real `ObjectId`. Note that the hex-string form **compiles without any cast**. Round-tripping a decoded `_id` back into `match()` typechecks cleanly and silently returns zero rows. Only the driver-`ObjectId` form requires a cast to attempt. +Filtering by `_id` equality inside `match()` **never matches any document**. Neither a hex string (`f._id.eq('507f...')`) nor the driver's own `ObjectId` matches. The hex-string form passes TypeScript, so taking an `_id` you read back and passing it to `match()` compiles cleanly and then returns zero documents. There is currently no supported way to filter by `_id` equality through the typed pipeline builder. The escape hatch is [`rawCommand()`](#rawcommand), which places a real `ObjectId` directly in a raw pipeline document and matches correctly, or the [ORM client](/orm/reference/orm-client), which handles `_id` encoding for you (`db.orm.posts.where({ _id })`). ::: @@ -219,7 +228,7 @@ Order documents by a field spec. ```typescript const plan = db.query.from('posts').sort({ createdAt: -1 }).build(); -const newestFirst = await (await db.runtime()).query(plan); +const newestFirst = await runtime.query(plan); ``` ### `limit()` @@ -248,7 +257,7 @@ Cap the number of documents. ```typescript const plan = db.query.from('posts').sort({ createdAt: 1 }).limit(1).build(); -const [oldest] = await (await db.runtime()).query(plan); +const [oldest] = await runtime.query(plan); ``` ### `skip()` @@ -273,7 +282,7 @@ Offset into the sorted result set. ```typescript const plan = db.query.from('posts').sort({ createdAt: 1 }).skip(1).build(); -const afterFirst = await (await db.runtime()).query(plan); +const afterFirst = await runtime.query(plan); ``` ### `sample()` @@ -298,7 +307,7 @@ Draw a random subset of documents. ```typescript const plan = db.query.from('posts').sample(1).build(); -const oneRandom = await (await db.runtime()).query(plan); +const oneRandom = await runtime.query(plan); ``` ### `addFields()` @@ -332,7 +341,7 @@ const plan = db.query .from('posts') .addFields((f) => ({ shoutTitle: fn.toUpper(f.title) })) .build(); -const withShout = await (await db.runtime()).query(plan); +const withShout = await runtime.query(plan); ``` ### `lookup()` @@ -341,9 +350,10 @@ Join documents from another collection (`$lookup`). #### Remarks -- `lookup()` takes a callback that builds the join with `from(root).on((local, foreign) => ({ local, foreign })).as(name)`: the foreign collection, the local and foreign fields to match on, and the output array field name. -- The joined documents land in an array under the `as` name. -- A looked-up sub-document's `_id` comes back as a raw driver `ObjectId`, not a decoded hex string. Compare it with `String(...)`. +- `lookup()` takes a callback that builds the join with `from(root).on((local, foreign) => ({ local, foreign })).as(name)`: the other collection, the two fields to match on, and the name of the output array field. +- The object returned by `on()` has two fixed keys, `local` and `foreign`. Write `({ local: local.authorId, foreign: foreign._id })`. +- The joined documents arrive in an array under the `as` name. +- A joined sub-document's `_id` is the driver's own `ObjectId`, not a hex string. Compare two of them with `String(a._id) === String(b._id)`. #### Options @@ -371,8 +381,8 @@ const plan = db.query .as('author'), ) .build(); -const withAuthor = await (await db.runtime()).query(plan); -// withAuthor[0].author is an array; author[0]._id is a raw ObjectId — compare with String(...) +const withAuthor = await runtime.query(plan); +// withAuthor[0].author is an array; compare ids with String(a._id) === String(b._id) ``` ### `project()` @@ -403,8 +413,8 @@ Reshape each document. ```typescript const plan = db.query.from('posts').project('title', 'kind').build(); -const trimmed = await (await db.runtime()).query(plan); -// each document has title, kind, and _id — no content +const trimmed = await runtime.query(plan); +// each document has title, kind, and _id, but no content ``` ##### Callback form @@ -416,7 +426,7 @@ const plan = db.query .from('posts') .project((f) => ({ title: 1, shout: fn.toUpper(f.title) })) .build(); -const projected = await (await db.runtime()).query(plan); +const projected = await runtime.query(plan); ``` ### `unwind()` @@ -425,8 +435,8 @@ Unroll an array field into one document per element. #### Remarks -- `unwind(field, { preserveNullAndEmptyArrays? })` maps to MongoDB's `$unwind`. It requires an **array-typed** field to unroll. -- The example schema has no array field, so there is no example here. One input document becomes one output document per element of the named array. +- `unwind(field, { preserveNullAndEmptyArrays? })` maps to MongoDB's `$unwind`. The named field must hold an array. +- One input document becomes one output document per element of that array. #### Options @@ -441,6 +451,23 @@ Unroll an array field into one document per element. |---|---|---| | Pipeline builder | `db.query.from(root).unwind('items')` | A builder emitting one document per array element. | +#### Examples + +##### Unroll an array field + +This example assumes a `tags String[]` field on `Post`, which the example schema above does not have. + +```typescript +const plan = db.query + .from('posts') + .match((f) => f.title.eq('Hello world')) + .unwind('tags') + .sort({ tags: 1 }) + .build(); +const perTag = await runtime.query(plan); +// one document per tag, each with the post's other fields beside it +``` + ### `group()` Group documents by a key and compute per-group aggregates. @@ -448,7 +475,7 @@ Group documents by a key and compute per-group aggregates. #### Remarks - `group()` takes a callback that receives **only** the `f` argument, one property per field (`(f) => ...`), and returns a spec object. The spec's `_id` sets the grouping key; every other key must be an **accumulator**. -- Accumulators come from the imported `acc` namespace, **not** a second callback argument: `import { acc } from '@prisma/orm-mongo/query-builder'`, then `acc.count()`, `acc.push(f.title)`. The callback signature is single-argument; there is no `(f, acc) => ...` form. (Some older internal material shows a two-argument callback. That is incorrect.) +- Accumulators come from the imported `acc` namespace, **not** a second callback argument: `import { acc } from '@prisma/orm-mongo/query-builder'`, then `acc.count()`, `acc.push(f.title)`. The callback takes one argument; there is no `(f, acc) => ...` form. - A `_id: null` key groups the whole collection into a single bucket. - A non-accumulator value for a non-`_id` key throws as soon as you call `group()`, with an error whose `code` is `ORM.ARGUMENT_INVALID`: `group() field "" must use an accumulator (e.g. acc.sum(), acc.count()). Got "" expression.` A `null` value for a key other than `_id` throws the same way. @@ -479,7 +506,7 @@ const plan = db.query titles: acc.push(f.title), })) .build(); -const perAuthor = await (await db.runtime()).query(plan); +const perAuthor = await runtime.query(plan); ``` ##### Group the whole collection @@ -491,7 +518,7 @@ const plan = db.query .from('posts') .group((f) => ({ _id: null, total: acc.count(), latest: acc.max(f.createdAt) })) .build(); -const [summary] = await (await db.runtime()).query(plan); +const [summary] = await runtime.query(plan); // { _id: null, total: 2, latest: } ``` @@ -499,7 +526,7 @@ For Prisma ORM 7 users, `groupBy` becomes a `group()` stage on the pipeline buil ```diff - const perAuthor = await prisma.post.groupBy({ by: ['authorId'], _count: true }); -+ const perAuthor = await (await db.runtime()).query( ++ const perAuthor = await runtime.query( + db.query.from('posts').group((f) => ({ _id: f.authorId, postCount: acc.count() })).build(), + ); ``` @@ -542,7 +569,7 @@ const plan = db.query ) .replaceRoot((f) => fn.arrayElemAt(f.author, fn.literal(0))) .build(); -const authors = await (await db.runtime()).query(plan); +const authors = await runtime.query(plan); // each document is the promoted author sub-document ``` @@ -568,7 +595,7 @@ Reduce the pipeline to a single document holding the count. ```typescript const plan = db.query.from('posts').count('total').build(); -const [{ total }] = await (await db.runtime()).query(plan); +const [{ total }] = await runtime.query(plan); // { total: 2 } ``` @@ -601,8 +628,8 @@ const plan = db.query .from('posts') .sortByCount((f) => f.kind) .build(); -const byKind = await (await db.runtime()).query(plan); -// two buckets, each { _id: , count: 1 } — order of tied counts is not guaranteed +const byKind = await runtime.query(plan); +// two buckets, each { _id: , count: 1 }. Order of tied counts is not guaranteed. ``` ### `redact()` @@ -612,8 +639,38 @@ Keep or prune documents (and sub-documents) based on an expression. #### Remarks - `redact()` maps to MongoDB's `$redact`, whose expression must evaluate to one of the system variables `$$KEEP`, `$$DESCEND`, or `$$PRUNE`. -- There is no dedicated expression helper for a system variable, and `fn.literal(...)` cannot stand in: `fn.literal('KEEP')` compiles to the plain string `KEEP` and `fn.literal('$$KEEP')` is wrapped in `$literal`, and the server rejects both. -- Use `f.rawPath('$KEEP')` instead. `rawPath` builds a field path without checking it against the model, and a field path is compiled by prefixing a `$`, so `'$KEEP'` reaches the server as `$$KEEP`. Branch with `fn.cond(...)`, with `f.rawPath('$KEEP')` and `f.rawPath('$PRUNE')` as the two arms. For anything more involved, prefer [`rawCommand()`](#rawcommand), where you can write the `$redact` stage with `$$KEEP` / `$$PRUNE` directly. +- There is no expression helper for a system variable, and `fn.literal(...)` cannot stand in. The server rejects both `fn.literal('KEEP')` and `fn.literal('$$KEEP')`. +- Write `f.rawPath('$KEEP')` instead. The builder adds one more `$`, so the server sees `$$KEEP`. +- Choose between the two with `fn.cond(...)`, as in the example below. +- For anything more involved, use [`rawCommand()`](#rawcommand), where you write the `$redact` stage with `$$KEEP` and `$$PRUNE` directly. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `spec` | Callback `(f) => expression` | Yes | An expression evaluating to `$$KEEP`, `$$DESCEND`, or `$$PRUNE`. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| Pipeline builder | `db.query.from('posts').redact(...)` | A builder that keeps or prunes each document. | + +#### Examples + +##### Keep tutorials and prune everything else + +```typescript +import { fn } from '@prisma/orm-mongo/query-builder'; + +const plan = db.query + .from('posts') + .redact((f) => + fn.cond(fn.eq(f.kind, fn.literal('tutorial')).node, f.rawPath('$KEEP'), f.rawPath('$PRUNE')), + ) + .build(); +const tutorials = await runtime.query(plan); +``` ### Option-object stages @@ -621,9 +678,18 @@ Several stages take a single MongoDB stage options object rather than a typed `( #### Remarks -- `bucket`, `bucketAuto`, `geoNear`, `graphLookup`, `setWindowFields`, `densify`, and `fill` take an options object. Two in this group differ: `unionWith` takes a collection name and an optional array of raw stages, and `facet` takes an object that maps each output field name to an array of raw stages. -- `geoNear`, `densify`, and `fill` are **not run against a live database** by Prisma ORM's own tests; only their signatures are checked. The rest are. -- **The expression fields inside these options are opaque.** Fields like `bucket.groupBy`, `graphLookup.startWith`, `setWindowFields.partitionBy`, and `geoNear.near` are raw aggregation expressions (`MongoAggExpr`), not reachable through the typed `(f) => ...` callback the classic stages use. You build these values with expression helpers and pass the underlying node (`fn.year(ref).node`), or name a field directly with `MongoAggFieldRef.of('price')`. That is less type-safe than the callback stages, and a known gap in the typed API. Those classes, and the stage classes `unionWith` and `facet` take (`MongoMatchStage`, `MongoCountStage`, and the rest), come from `@prisma/orm-mongo/query-ast/execution`. +- `bucket({ groupBy, boundaries, default_?, output? })` sorts documents into the ranges you name. +- `bucketAuto({ groupBy, buckets, output?, granularity? })` sorts them into a given number of equal-sized ranges. +- `geoNear({ near, distanceField, spherical?, maxDistance?, minDistance?, query?, key?, distanceMultiplier?, includeLocs? })`. +- `graphLookup({ from, startWith, connectFromField, connectToField, as, maxDepth?, depthField?, restrictSearchWithMatch? })`. +- `setWindowFields({ partitionBy?, sortBy?, output })`. +- `densify({ field, partitionByFields?, range })`. +- `fill({ partitionBy?, partitionByFields?, sortBy?, output })`. +- `facet(facets)` takes an object mapping each output field name to an array of stages. +- `unionWith(collection, pipeline?)` takes a collection name and an optional array of stages. +- `geoNear`, `densify`, and `fill` have not been verified against a running MongoDB. +- These options take raw expressions, not the typed `(f) => ...` callback the other stages use. Build a raw expression with the `fn.*` helpers and pass its `.node` property, or name a field with `MongoAggFieldRef.of('price')`. The fields that work this way are `bucket.groupBy`, `bucketAuto.groupBy`, `graphLookup.startWith`, `setWindowFields.partitionBy`, `fill.partitionBy`, and `geoNear.near`. +- The stages you pass to `facet` and `unionWith` are classes too: `MongoMatchStage`, `MongoCountStage`, `MongoSortStage`, `MongoLimitStage`, and the rest. Import them, and `MongoAggFieldRef`, from `@prisma/orm-mongo/query-ast/execution`. #### Examples @@ -631,20 +697,60 @@ Several stages take a single MongoDB stage options object rather than a typed `( ```typescript const plan = db.query.from('posts').unionWith('users').build(); -const combined = await (await db.runtime()).query(plan); +const combined = await runtime.query(plan); // posts followed by users ``` +##### Run several sub-pipelines with `facet()` + +```typescript +import { MongoCountStage, MongoFieldFilter, MongoLimitStage, MongoMatchStage, MongoSortStage } from '@prisma/orm-mongo/query-ast/execution'; + +const plan = db.query + .from('posts') + .facet({ + totalCount: [new MongoCountStage('count')], + newest: [new MongoSortStage({ createdAt: -1 }), new MongoLimitStage(2)], + tutorials: [ + new MongoMatchStage(MongoFieldFilter.eq('kind', 'tutorial')), + new MongoCountStage('count'), + ], + }) + .build(); +const [facets] = await runtime.query(plan); +// facets.totalCount is [{ count: 2 }]; facets.newest is the two newest posts +``` + +##### Sort into ranges with `bucket()` + +`duration` is a field on `Tutorial`, so posts without it fall into the `Other` bucket. + +```typescript +import { MongoAggFieldRef } from '@prisma/orm-mongo/query-ast/execution'; + +const plan = db.query + .from('posts') + .bucket({ + groupBy: MongoAggFieldRef.of('duration'), + boundaries: [0, 15, 60], + default_: 'Other', + }) + .build(); +const buckets = await runtime.query(plan); +// one document per bucket, each { _id: , count: n } +``` + ### Atlas-only stages `search()`, `searchMeta()`, and `vectorSearch()` build MongoDB Atlas Search stages (`$search`, `$searchMeta`, `$vectorSearch`). #### Remarks -- These stages **require MongoDB Atlas** (they need Atlas Search's `mongot` process) and Prisma ORM's own tests **never run them**, because those tests use an in-memory MongoDB with no Atlas Search. Only the built query's shape is checked. Nothing in the builder refuses them off Atlas: the stage is built and sent, and a MongoDB with no Atlas Search backend is what rejects it. -- `search(spec)` and `searchMeta(spec)` take an Atlas Search operator spec (for example `{ text: { query: 'hello', path: 'title' } }`), and an optional index name as a second argument. -- `vectorSearch(spec)` takes an Atlas Vector Search spec (`{ index, path, queryVector, numCandidates, limit }`, plus an optional `filter`). -- Verify these against a real Atlas deployment before relying on them. +- These stages **require MongoDB Atlas**. The builder does not refuse them elsewhere: your query builds fine, and the MongoDB server rejects it when you run it. +- `search(config, index?)` takes an Atlas Search operator object, for example `{ text: { query: 'hello', path: 'title' } }`, and an optional index name. +- `searchMeta(config, index?)` takes the same two arguments and returns counts and facets instead of documents. +- `vectorSearch({ index, path, queryVector, numCandidates, limit, filter? })` takes the index name, the field holding the vectors, the vector to search for as an array of numbers, how many candidates to consider, how many documents to return, and an optional filter object. +- None of the three has been verified against a running Atlas deployment. Test them against your own before relying on them. #### Return type @@ -684,21 +790,23 @@ The builder exposes nineteen accumulators: | `acc.stdDevPop(expr)` | `stdDevPop(expression)` | Population standard deviation. | | `acc.stdDevSamp(expr)` | `stdDevSamp(expression)` | Sample standard deviation. | -The examples below are the common accumulators, all executed against a live database. The `N`-variants (`firstN` shown), `top`/`bottom`, and the standard-deviation accumulators share the same call shapes; `firstN` is exercised here as a representative of the family. In every signature, `sortBy` is a plain `{ field: 1 | -1 }` object and `n` is an expression, so pass `fn.literal(2)` rather than `2`. +`firstN` is shown below. `lastN`, `maxN`, and `minN` take the same arguments. `top`, `bottom`, `topN`, and `bottomN` take `output` and `sortBy` instead. `output` is the expression whose value you want back from each chosen document. `sortBy` is a plain object such as `{ createdAt: 1 }`, where `1` sorts ascending and `-1` sorts descending. Wherever a signature takes `n`, wrap the number: write `n: fn.literal(2)`, not `n: 2`. `acc.first()` and `acc.last()` only mean something when a `sort()` comes before the `group()`. #### Examples ##### `count()` and `sum()` +`acc.sum(fn.literal(1))` adds 1 for each document, which counts them. To add up a field instead, pass the field, as in `acc.sum(f.views)` on a collection with a numeric `views` field. + ```typescript import { acc, fn } from '@prisma/orm-mongo/query-builder'; const plan = db.query .from('posts') - .group((f) => ({ _id: null, total: acc.count(), durationSum: acc.sum(fn.literal(1)) })) + .group((f) => ({ _id: null, total: acc.count(), counted: acc.sum(fn.literal(1)) })) .build(); -const [{ total, durationSum }] = await (await db.runtime()).query(plan); -// { total: 2, durationSum: 2 } +const [{ total, counted }] = await runtime.query(plan); +// { total: 2, counted: 2 } ``` ##### `avg()`, `min()`, and `max()` @@ -708,15 +816,10 @@ import { acc, fn } from '@prisma/orm-mongo/query-builder'; const plan = db.query .from('posts') - .group((f) => ({ - _id: null, - earliest: acc.min(f.createdAt), - latest: acc.max(f.createdAt), - avgYear: acc.avg(fn.year(f.createdAt)), - })) + .group((f) => ({ _id: null, earliest: acc.min(f.createdAt), avgYear: acc.avg(fn.year(f.createdAt)) })) .build(); -const [stats] = await (await db.runtime()).query(plan); -// { earliest: , latest: , avgYear: 2024 } +const [stats] = await runtime.query(plan); +// { earliest: , avgYear: 2024 } ``` ##### `first()` and `last()` @@ -729,7 +832,7 @@ const plan = db.query .sort({ createdAt: 1 }) .group((f) => ({ _id: null, firstTitle: acc.first(f.title), lastTitle: acc.last(f.title) })) .build(); -const [{ firstTitle, lastTitle }] = await (await db.runtime()).query(plan); +const [{ firstTitle, lastTitle }] = await runtime.query(plan); // { firstTitle: 'Hello world', lastTitle: 'Tutorial one' } ``` @@ -740,13 +843,9 @@ import { acc } from '@prisma/orm-mongo/query-builder'; const plan = db.query .from('posts') - .group((f) => ({ - _id: null, - allTitles: acc.push(f.title), - distinctKinds: acc.addToSet(f.kind), - })) + .group((f) => ({ _id: null, allTitles: acc.push(f.title), distinctKinds: acc.addToSet(f.kind) })) .build(); -const [{ allTitles, distinctKinds }] = await (await db.runtime()).query(plan); +const [{ allTitles, distinctKinds }] = await runtime.query(plan); ``` ##### `firstN()` (an `N`-variant) @@ -757,9 +856,13 @@ import { acc, fn } from '@prisma/orm-mongo/query-builder'; const plan = db.query .from('posts') .sort({ createdAt: 1 }) - .group((f) => ({ _id: null, firstTwo: acc.firstN({ input: f.title, n: fn.literal(2) }) })) + .group((f) => ({ + _id: null, + firstTwo: acc.firstN({ input: f.title, n: fn.literal(2) }), + newestTitle: acc.top({ output: f.title, sortBy: { createdAt: -1 } }), // output is what you get back + })) .build(); -const [{ firstTwo }] = await (await db.runtime()).query(plan); +const [{ firstTwo, newestTitle }] = await runtime.query(plan); // firstTwo is ['Hello world', 'Tutorial one'] ``` @@ -771,25 +874,30 @@ Expression helpers (`fn.*`) build the computed values used inside stages like `a import { fn } from '@prisma/orm-mongo/query-builder'; ``` -The `fn.*` namespace covers 73 of MongoDB's aggregation expression operators. It is not all of them: there is no `fn.and`, `fn.or`, `fn.not`, `fn.switch`, `fn.ifNull`, `fn.map`, `fn.reduce`, or `fn.filter`, and arithmetic stops at `add`, `subtract`, `multiply`, and `divide`. Each helper is usually the camelCase name of the operator without its `$` prefix (`$toUpper` becomes `fn.toUpper`, `$dateToString` becomes `fn.dateToString`). Five are renamed to avoid clashes: `fn.firstElem` and `fn.lastElem` for `$first` and `$last`, `fn.isIn` for `$in`, `fn.typeOf` for `$type`, and `fn.toString_` for `$toString`. The helpers below are grouped by category, with the ones exercised by the test suite. +Each helper is the camelCase name of the MongoDB aggregation operator without its `$` prefix, so `$toUpper` is `fn.toUpper` and `$dateToString` is `fn.dateToString`. Five have a different name because the plain one was taken: `fn.firstElem` for `$first`, `fn.lastElem` for `$last`, `fn.isIn` for `$in`, `fn.typeOf` for `$type`, and `fn.toString_` for `$toString`. Here is every helper. Anything missing from this list has no helper, including `$and`, `$or`, `$not`, `$switch`, `$ifNull`, `$map`, `$reduce`, and `$filter`. -| Category | Helpers (examples) | Notes | -|---|---|---| -| Arithmetic | `fn.add`, `fn.multiply` | Numeric arithmetic over expressions. | -| String | `fn.concat`, `fn.toUpper`, `fn.split` | String composition and transformation. | -| Date | `fn.year` | Extract a component from a date field. | -| Comparison | `fn.eq`, `fn.gt` | Return a boolean expression; use `.node` where a raw expression is required. | -| Array | `fn.size`, `fn.arrayElemAt` | Array length and element access. | -| Control flow | `fn.cond` | Ternary branch on a boolean expression. | -| Literal | `fn.literal` | Wrap a constant so it is not interpreted as a field path. | -| Type | `fn.toObjectId` | Convert a value to an `ObjectId`. | +| Group | Helpers | +|---|---| +| Arithmetic | `add`, `subtract`, `multiply`, `divide` | +| Strings | `concat`, `toLower`, `toUpper`, `substr`, `substrBytes`, `trim`, `ltrim`, `rtrim`, `split`, `strLenCP`, `strLenBytes`, `replaceOne`, `replaceAll` | +| Regular expressions | `regexMatch`, `regexFind`, `regexFindAll` | +| Dates | `year`, `month`, `dayOfMonth`, `hour`, `minute`, `second`, `millisecond`, `dateToString`, `dateFromString`, `dateDiff`, `dateAdd`, `dateSubtract`, `dateTrunc` | +| Comparison | `cmp`, `eq`, `ne`, `gt`, `gte`, `lt`, `lte` | +| Arrays | `size`, `arrayElemAt`, `concatArrays`, `firstElem`, `lastElem`, `isIn`, `indexOfArray`, `isArray`, `reverseArray`, `slice`, `zip`, `range` | +| Sets | `setUnion`, `setIntersection`, `setDifference`, `setEquals`, `setIsSubset`, `anyElementTrue`, `allElementsTrue` | +| Type conversion | `typeOf`, `convert`, `toInt`, `toLong`, `toDouble`, `toDecimal`, `toString_`, `toObjectId`, `toBool`, `toDate` | +| Objects | `objectToArray`, `arrayToObject`, `getField`, `setField` | +| Constants and branching | `literal`, `cond` | + +`fn.literal(value)` wraps a constant so MongoDB reads it as a value and not as the name of a field. Every `fn.*` call returns a wrapper object. Its `.node` property is the raw expression inside that wrapper. `match()` takes the wrapper itself, through `expr(...)`. `fn.cond()` and the stages that take an options object take `.node`. #### Remarks -- Some helpers take a named-arguments object rather than positional arguments: `fn.dateToString({ date, format })`, `fn.trim({ input })`, `fn.regexMatch({ input, regex })`, `fn.convert({ input, to })`, and the rest of the date, regex, and replace helpers. -- **`fn.cond()`'s condition is a raw expression, not a filter.** Pass a comparison helper's `.node`: `fn.cond(fn.eq(a, b).node, thenExpr, elseExpr)`. Passing the `expr(...)` form (which is for `match()` filters) throws `TypeError: visitor.expr is not a function`. -- The `expr()` wrapper is only for `match()` filters. Inside `match()`, `expr(fn.gt(...))` is correct; inside `fn.cond()` it is not. -- **`fn.toObjectId()` cannot be composed over `fn.literal()`.** `fn.toObjectId(fn.literal(id))` throws (`Cannot read properties of undefined (reading 'codecId')`), because `fn.literal()` has no field type information. Give `fn.toObjectId()` an expression taken from the `f` argument instead. +- Some helpers take one object of named arguments rather than positional ones: `fn.dateToString({ date, format })`, `fn.trim({ input })`, `fn.regexMatch({ input, regex })`, `fn.convert({ input, to })`, and the rest of the date, regex, replace, and object helpers. +- **Inside `fn.cond()`, pass `.node`.** Write `fn.cond(fn.eq(a, b).node, thenExpr, elseExpr)`. Passing the `expr(...)` form there throws a `TypeError`. +- The `expr()` wrapper is only for `match()` filters. Inside `match()`, `expr(fn.gt(...))` is correct. Inside `fn.cond()` it is not. +- **`fn.toObjectId()` cannot take a `fn.literal()` value.** `fn.toObjectId(fn.literal(id))` throws, because a literal carries no field type to convert from. Give `fn.toObjectId()` an expression taken from the `f` argument instead. +- To turn a string id from your own code into an `ObjectId`, build the `ObjectId` yourself and put it in a raw pipeline with [`rawCommand()`](#rawcommand). The last example on this page does exactly that. #### Examples @@ -800,12 +908,9 @@ import { fn } from '@prisma/orm-mongo/query-builder'; const plan = db.query .from('posts') - .project((f) => ({ - title: 1, - computed: fn.add(fn.literal(1), fn.multiply(fn.literal(2), fn.literal(3))), - })) + .project((f) => ({ title: 1, computed: fn.add(fn.literal(1), fn.multiply(fn.literal(2), fn.literal(3))) })) .build(); -const rows = await (await db.runtime()).query(plan); +const rows = await runtime.query(plan); // computed is 7 for every document ``` @@ -818,7 +923,7 @@ const plan = db.query .from('posts') .project((f) => ({ shout: fn.concat(fn.toUpper(f.title), fn.literal('!')) })) .build(); -const rows = await (await db.runtime()).query(plan); +const rows = await runtime.query(plan); ``` ##### Branch with `cond()` @@ -830,67 +935,54 @@ const plan = db.query .from('posts') .project((f) => ({ title: 1, - label: fn.cond( - fn.eq(f.kind, fn.literal('tutorial')).node, - fn.literal('is-tutorial'), - fn.literal('not-tutorial'), - ), + label: fn.cond(fn.eq(f.kind, fn.literal('tutorial')).node, fn.literal('is-tutorial'), fn.literal('not-tutorial')), })) .build(); -const rows = await (await db.runtime()).query(plan); +const rows = await runtime.query(plan); ``` -##### Array length +### `pipe()` + +Append an arbitrary raw pipeline stage the typed methods don't cover. + +#### Remarks + +- `pipe(stage)` appends a raw stage that you construct yourself, such as `new MongoMatchStage(...)` from `@prisma/orm-mongo/query-ast/execution`. It keeps the row type you already had. `pipe(stage)` lets you declare a different one. +- After a `pipe()` stage the rows come back exactly as MongoDB returns them, whatever the TypeScript type claims. Dates and ids are raw driver values. +- Prefer a typed stage where one exists. `pipe()` adds one raw stage to a typed chain, and [`rawCommand()`](#rawcommand) replaces the whole command. + +#### Examples + +##### Append a raw stage ```typescript -import { fn } from '@prisma/orm-mongo/query-builder'; +import { MongoFieldFilter, MongoMatchStage } from '@prisma/orm-mongo/query-ast/execution'; const plan = db.query .from('posts') - .project((f) => ({ title: 1, titleWords: fn.size(fn.split(f.title, fn.literal(' '))) })) + .pipe(new MongoMatchStage(MongoFieldFilter.eq('kind', 'tutorial'))) .build(); -const rows = await (await db.runtime()).query(plan); +const rows = await runtime.query(plan); ``` -### `pipe()` - -Append an arbitrary raw pipeline stage the typed methods don't cover. - -#### Remarks - -- `pipe(stage)` appends a raw stage node, such as `new MongoMatchStage(...)` from `@prisma/orm-mongo/query-ast/execution`. It keeps the row type you already had; `pipe(stage)` lets you declare a new one. Whatever the row type says, the rows are no longer decoded, because a stage the builder does not recognise drops the result shape. -- Prefer a typed stage when one exists; `pipe()` is the in-chain escape hatch, and [`rawCommand()`](#rawcommand) is the whole-command one. - ## Read methods [#read-terminals] -`build()` turns the chain into a plan you can run. Pass the plan to `(await db.runtime()).query(plan)` to run it. +Finish a read chain with `build()`, then run the built query with `runtime.query(...)`. ### `build()` and `aggregate()` -Compile the pipeline into a plan. +Compile the pipeline into a query you can run. #### Remarks -- `build()` produces the plan. `aggregate()` is an alias: it returns the identical plan and executes identically. -- Neither runs the query. Pass the plan to `(await db.runtime()).query(plan)` to fetch documents. +- `build()` produces the built query. `aggregate()` is an alias: it returns the identical query and runs identically. +- Neither runs the query. Pass the result to `runtime.query(...)` to fetch documents. #### Return type | Return type | Example | Description | |---|---|---| -| Built query | `db.query.from('posts').build()` | A plan you run with `(await db.runtime()).query(...)`. | - -#### Examples - -##### `build()` and its `aggregate()` alias - -```typescript -const built = db.query.from('posts').sort({ createdAt: 1 }).build(); -const aggregated = db.query.from('posts').sort({ createdAt: 1 }).aggregate(); -// built and aggregated produce the same plan - -const rows = await (await db.runtime()).query(aggregated); -``` +| Built query | `db.query.from('posts').sort({ createdAt: 1 }).build()` | A query you run with `runtime.query(...)`. | ## Write methods @@ -898,9 +990,9 @@ The pipeline builder can write, not just read. Write methods are available at th Updater callbacks (the `(f) => [...]` argument to the update methods) must return an **array** of operations. This is a firm rule: -- A bare (non-array) operation throws as soon as you call the write method: `Error: Unreachable: items.length > 0 but first is undefined`. This is an internal consistency guard, not a friendly validation message, so returning `[f.bio.set('x')]` rather than `f.bio.set('x')` matters. (Some older internal material shows the bare form. That is incorrect.) +- Always return an array. Returning a bare operation, `f.bio.set('x')` rather than `[f.bio.set('x')]`, throws as soon as you call the write method. - An empty array throws an error whose `code` is `ORM.MUTATION_DATA_MISSING`: `Updater returned no operations. Return at least one update from the callback ...`. -- You cannot mix operator-form ops and pipeline-form (`f.stage.*`) ops in one updater. TypeScript rejects the mixed array, and at run time it throws an error whose `code` is `ORM.ARGUMENT_INVALID`: `Cannot mix ...`. Each updater is one form or the other. +- An updater is either all operator form or all pipeline form. Operator form calls an operator on a field, as in `f.bio.set(value)`. Pipeline form calls `f.stage.*` and can read other fields of the same document. You cannot mix the two in one updater. TypeScript rejects the mixed array, and at run time it throws an error whose `code` is `ORM.ARGUMENT_INVALID`: `Cannot mix ...`. - Applying the same operator to the same field twice in one updater throws an error whose `code` is `ORM.ARGUMENT_INVALID`: `Update spec collision: ...`. ### Root-level writes @@ -909,30 +1001,26 @@ Updater callbacks (the `(f) => [...]` argument to the update methods) must retur #### Remarks -- These return a plan; run it with `(await db.runtime()).query(plan)`. -- Write results are result objects, not decoded documents: `insertOne` gives `{ insertedId }`, `insertMany` gives `{ insertedIds, insertedCount }`, the update methods and `upsertOne` give `{ matchedCount, modifiedCount, upsertedCount, upsertedId }`, and the delete methods give `{ deletedCount }`. -- The document you pass to `insertOne` / `insertMany` is a plain record. The builder does not check it against the model, and its values are stored as you pass them, the same way [`match()`](#match) filter values are. `insertMany([])` throws an error whose `code` is `ORM.MUTATION_DATA_MISSING`. +- These return a built query. Run it with `runtime.query(...)`. +- Write results are result objects, not documents: `insertOne` gives `{ insertedId }`, `insertMany` gives `{ insertedIds, insertedCount }`, the update methods and `upsertOne` give `{ matchedCount, modifiedCount, upsertedCount, upsertedId }`, and the delete methods give `{ deletedCount }`. +- The document you pass to `insertOne` / `insertMany` is a plain record. The builder does not check it against your contract, and each value is stored exactly as you pass it, with no conversion to the type your contract gives that field. [`match()`](#match) filter values follow the same rule. `insertMany([])` throws an error whose `code` is `ORM.MUTATION_DATA_MISSING`. #### Examples ##### `insertOne()` and `insertMany()` ```typescript -const onePlan = db.query.from('users').insertOne({ - name: 'Carol', - email: 'carol@example.com', - bio: null, - role: 'author', - address: null, -}); -const [insertOneResult] = await (await db.runtime()).query(onePlan); +const onePlan = db.query + .from('users') + .insertOne({ name: 'Carol', email: 'carol@example.com', bio: null, role: 'author', address: null }); +const [insertOneResult] = await runtime.query(onePlan); // { insertedId: } const manyPlan = db.query.from('users').insertMany([ { name: 'Carol', email: 'carol@example.com', bio: null, role: 'author', address: null }, { name: 'Dave', email: 'dave@example.com', bio: null, role: 'reader', address: null }, ]); -const [insertManyResult] = await (await db.runtime()).query(manyPlan); +const [insertManyResult] = await runtime.query(manyPlan); // { insertedIds: [, ], insertedCount: 2 } ``` @@ -940,14 +1028,14 @@ const [insertManyResult] = await (await db.runtime()).query(manyPlan); ```typescript const plan = db.query.from('users').updateAll((f) => [f.bio.set('everyone now has bio')]); -await (await db.runtime()).query(plan); +await runtime.query(plan); ``` ##### `deleteAll()` ```typescript const plan = db.query.from('users').deleteAll(); -const [result] = await (await db.runtime()).query(plan); +const [result] = await runtime.query(plan); // { deletedCount: 2 } ``` @@ -956,11 +1044,10 @@ const [result] = await (await db.runtime()).query(plan); On the root collection, `upsertOne()` takes a filter callback and an updater callback: ```typescript -const plan = db.query.from('users').upsertOne( - (f) => f.email.eq('erin@example.com'), - (f) => [f.name.set('Erin'), f.email.set('erin@example.com'), f.bio.set(null)], -); -const [result] = await (await db.runtime()).query(plan); +const plan = db.query + .from('users') + .upsertOne((f) => f.email.eq('erin@example.com'), (f) => [f.name.set('Erin'), f.email.set('erin@example.com')]); +const [result] = await runtime.query(plan); // inserts when no document matches: result.upsertedId is defined ``` @@ -970,10 +1057,11 @@ After a `match()` filter, `updateMany`, `updateOne`, `deleteMany`, `deleteOne`, #### Remarks -- The `match()` filter supplies the write's filter; you do not repeat it. +- The `match()` filter supplies the write's filter, so you do not repeat it. - After `match()`, `upsertOne()` takes the updater callback only (the filter comes from `match()`). -- `updateOne` / `deleteOne` affect at most one matching document; `updateMany` / `deleteMany` affect all matches. Which document `updateOne` / `deleteOne` picks is up to the driver. Use `findOneAndUpdate()` after a `sort()` when the choice matters. -- `findOneAndUpdate()` and `findOneAndDelete()` return the document exactly as the driver gives it, undecoded, so its `_id` is a raw `ObjectId` even though the TypeScript type says the decoded shape. Both disappear from the type after a stage MongoDB's `findAndModify` command cannot carry, such as `skip()`; forced past the type system they throw an error whose `code` is `ORM.OPERATION_UNSUPPORTED`. +- `updateOne` / `deleteOne` affect at most one matching document. `updateMany` / `deleteMany` affect all matches. Which document `updateOne` / `deleteOne` picks is up to the driver. Use `findOneAndUpdate()` after a `sort()` when the choice matters. +- `findOneAndUpdate()` and `findOneAndDelete()` return the document exactly as the driver gives it, so its `_id` is a raw `ObjectId` even though the TypeScript type says otherwise. +- You cannot call `findOneAndUpdate()` or `findOneAndDelete()` after `skip()`, and TypeScript does not offer them there. If a cast gets one past TypeScript, it throws an error whose `code` is `ORM.OPERATION_UNSUPPORTED`. #### Examples @@ -984,13 +1072,13 @@ const manyPlan = db.query .from('users') .match((f) => f.role.eq('author')) .updateMany((f) => [f.bio.set('matched-many')]); -await (await db.runtime()).query(manyPlan); +await runtime.query(manyPlan); const onePlan = db.query .from('users') .match((f) => f.email.eq('alice@example.com')) .updateOne((f) => [f.bio.set('single-update')]); -const [result] = await (await db.runtime()).query(onePlan); +const [result] = await runtime.query(onePlan); // { matchedCount: 1, modifiedCount: 1 } ``` @@ -1001,7 +1089,7 @@ const plan = db.query .from('users') .match((f) => f.role.eq('author')) .deleteMany(); -const [result] = await (await db.runtime()).query(plan); +const [result] = await runtime.query(plan); // { deletedCount: 2 } ``` @@ -1012,32 +1100,25 @@ const plan = db.query .from('users') .match((f) => f.email.eq('alice@example.com')) .upsertOne((f) => [f.bio.set('upserted via match')]); -const [result] = await (await db.runtime()).query(plan); +const [result] = await runtime.query(plan); // updates on a hit: result.modifiedCount is 1 ``` ##### `findOneAndUpdate()` and `returnDocument` -`findOneAndUpdate()` returns the matched document. Its second argument takes `returnDocument`, which controls which version you get back: `'before'` returns the document before the update, `'after'` returns it after. The default is `'after'`. The same argument takes `upsert`, which defaults to `false` and inserts a document when nothing matches. +`findOneAndUpdate()` returns the matched document. Its second argument is optional. That argument takes `returnDocument`, which controls which version you get back: `'before'` returns the document before the update, `'after'` returns it after. The default is `'after'`. The same argument takes `upsert`, which defaults to `false` and inserts a document when nothing matches. ```typescript -const afterPlan = db.query - .from('users') - .match((f) => f.email.eq('alice@example.com')) - .findOneAndUpdate((f) => [f.bio.set('changed')], { returnDocument: 'after' }); -const [afterDoc] = await (await db.runtime()).query(afterPlan); -// afterDoc.bio is 'changed' - -const beforePlan = db.query +const plan = db.query .from('users') .match((f) => f.email.eq('alice@example.com')) .findOneAndUpdate((f) => [f.bio.set('changed')], { returnDocument: 'before' }); -const [beforeDoc] = await (await db.runtime()).query(beforePlan); -// beforeDoc.bio is the pre-update value +const [beforeDoc] = await runtime.query(plan); +// beforeDoc.bio is the value from before the update ``` :::warning[The option is `returnDocument`, not `returnNewDocument`] -`returnNewDocument` is not a valid option. TypeScript rejects it (the suggested fix is `returnDocument`). If you force it past the type system, the unknown key is silently ignored and the default (`'after'`) applies, so a typo produces a wrong-but-plausible result rather than a loud error. Use `returnDocument: 'before' | 'after'`. +`returnNewDocument` is not a valid option name. TypeScript rejects it and suggests `returnDocument`. If a cast gets it past TypeScript, the unknown key is ignored and the default `'after'` applies, so a typo produces a wrong but plausible result rather than a loud error. Use `returnDocument: 'before' | 'after'`. ::: ##### `findOneAndDelete()` @@ -1047,7 +1128,7 @@ const plan = db.query .from('users') .match((f) => f.email.eq('alice@example.com')) .findOneAndDelete(); -const [deleted] = await (await db.runtime()).query(plan); +const [deleted] = await runtime.query(plan); // deleted is the removed document ``` @@ -1057,9 +1138,28 @@ Inside an updater callback, each operation targets a field. There are two mutual #### Remarks -- **Operator form**: field-level operators on the `f` argument, for example `f.bio.set(value)`. Per-field operators include `set`, `unset`, `rename`, `inc`, `mul`, `min`, `max`, `push`, `addToSet`, `pop`, `pull`, `pullAll`, `currentDate`, and `setOnInsert`. -- **Pipeline form**: aggregation-pipeline update stages through `f.stage.*`, for example `f.stage.set({ bio: f.name.node })` (which emits an `$addFields`-style stage). The four emitters are `set`, `unset`, `replaceRoot`, and `replaceWith`. Pipeline-form ops let an update reference other fields of the same document. -- The two forms cannot be mixed in a single updater (see the write-methods intro). An updater is entirely operator-form or entirely pipeline-form. +- **Operator form**: call an operator on the `f` argument, one operator per field. There are fourteen, listed under the bullets. +- **Pipeline form**: aggregation-pipeline update stages through `f.stage.*`, for example `f.stage.set({ bio: f.name.node })`. The four stages are `set`, `unset`, `replaceRoot`, and `replaceWith`. Pipeline-form operations let an update read other fields of the same document. +- The two forms cannot be mixed in a single updater (see the write-methods intro). An updater is entirely operator form or entirely pipeline form. + +Operator form takes these arguments: + +| Operator | What you pass | +|---|---| +| `set(value)` | The new value. | +| `unset()` | Nothing. Removes the field. | +| `rename(newName)` | The new field name, as a string. | +| `inc(amount)` | A number to add. A negative number subtracts. | +| `mul(factor)` | A number to multiply the stored value by. | +| `min(value)` | A value, written only if it is lower than the stored one. | +| `max(value)` | A value, written only if it is higher than the stored one. | +| `push(value)` | One value to append to an array field. | +| `addToSet(value)` | One value, appended only if the array does not already hold it. | +| `pop(direction)` | `1` removes the last element, `-1` the first. Optional, and defaults to `1`. | +| `pull(value)` | The value to remove from an array field. | +| `pullAll(values)` | An array of values to remove. | +| `currentDate()` | Nothing. Writes the current date. | +| `setOnInsert(value)` | A value written only when an upsert inserts a new document. | #### Examples @@ -1070,9 +1170,11 @@ const plan = db.query .from('users') .match((f) => f.role.eq('author')) .updateMany((f) => [f.bio.set('operator form')]); -await (await db.runtime()).query(plan); +await runtime.query(plan); ``` +The example schema has no numeric field and no array field, so `inc` and `push` have no example here. On a collection with a numeric `views` field and an array `tags` field, they read `updateMany((f) => [f.views.inc(1), f.tags.push('mongodb')])`. + ##### Pipeline form (`f.stage.*`) ```typescript @@ -1080,7 +1182,7 @@ const plan = db.query .from('users') .match((f) => f.role.eq('author')) .updateMany((f) => [f.stage.set({ bio: f.name.node })]); -await (await db.runtime()).query(plan); +await runtime.query(plan); // each author's bio is set to that author's own name ``` @@ -1090,9 +1192,10 @@ await (await db.runtime()).query(plan); #### Remarks -- `out(collection)` writes the pipeline output into a destination collection, replacing its contents. A second argument names a different database. -- `merge({ into })` streams the pipeline output into a target collection, merging with existing documents. `into` is a collection name or `{ db, coll }`. The options object also takes `on` (the field or fields to match on), `whenMatched`, and `whenNotMatched`. -- Both end the chain: they return a plan you run with `(await db.runtime()).query(plan)`, and produce no rows of their own. +- `out(collection)` writes the pipeline output into a destination collection, replacing its contents. A second argument, a plain string, names a different database: `out('users_snapshot', 'archive')`. +- `merge({ into })` streams the pipeline output into a target collection, merging with existing documents. `into` is a collection name or `{ db, coll }`. The options object also takes `on`, the field or fields to match on, plus `whenMatched` and `whenNotMatched`. +- `whenMatched` takes `'replace'`, `'keepExisting'`, `'merge'`, or `'fail'`, or an array of update stages. `whenNotMatched` takes `'insert'`, `'discard'`, or `'fail'`. The builder passes both to MongoDB unchecked. +- Both end the chain: they return a built query you run with `runtime.query(...)`, and produce no rows of their own. #### Examples @@ -1100,26 +1203,28 @@ await (await db.runtime()).query(plan); ```typescript const plan = db.query.from('users').out('users_snapshot'); -await (await db.runtime()).query(plan); +await runtime.query(plan); // the users_snapshot collection now holds the pipeline output ``` ##### Merge with `merge()` ```typescript -const plan = db.query.from('users').merge({ into: 'users_archive' }); -await (await db.runtime()).query(plan); +const plan = db.query + .from('users') + .merge({ into: 'users_archive', on: 'email', whenMatched: 'merge', whenNotMatched: 'insert' }); +await runtime.query(plan); ``` ## `rawCommand()` -Run a raw MongoDB aggregate command through the pipeline builder, bypassing the typed AST. +Run a raw MongoDB aggregate command through the pipeline builder. #### Remarks -- `db.query.rawCommand(command)` packages a command node (for example `new RawAggregateCommand(collection, pipeline)`) into a plan, without translating it. The rows come back as `unknown`; you type them yourself. -- Because it bypasses the typed AST, `rawCommand()` is the escape hatch for anything the typed builder can't express, including `_id` equality filters (see the [`match()`](#match) warning) and `$redact` with `$$KEEP` / `$$PRUNE`. -- For the full raw MongoDB API (raw collection methods, untyped writes, and the undecoded-results behavior), see [Raw queries](/orm/reference/raw-queries). +- `db.query.rawCommand(command)` takes a command you build yourself, for example `new RawAggregateCommand(collection, pipeline)`, and sends it as you wrote it. The rows come back as `unknown`, so you type them yourself. +- Use it for anything the typed builder cannot express, including `_id` equality filters (see the [`match()`](#match) warning) and MongoDB's `$redact` stage. +- For the full raw MongoDB API (raw collection methods, untyped writes, and rows that come back as the driver gives them), see [Raw queries](/orm/reference/raw-queries). #### Examples @@ -1129,11 +1234,11 @@ Run a raw MongoDB aggregate command through the pipeline builder, bypassing the import { RawAggregateCommand } from '@prisma/orm-mongo/query-ast/execution'; const plan = db.query.rawCommand(new RawAggregateCommand('posts', [{ $count: 'total' }])); -const rows = await (await db.runtime()).query(plan); +const rows = await runtime.query(plan); // [{ total: 2 }] ``` -##### Filter by `_id` (the working escape hatch) +##### Filter by `_id` (the way that works) ```typescript import { RawAggregateCommand } from '@prisma/orm-mongo/query-ast/execution'; @@ -1142,6 +1247,6 @@ import { ObjectId } from 'mongodb'; const plan = db.query.rawCommand( new RawAggregateCommand('posts', [{ $match: { _id: new ObjectId(postId) } }]), ); -const rows = await (await db.runtime()).query(plan); +const rows = await runtime.query(plan); // a real ObjectId in a raw pipeline document matches correctly ``` diff --git a/apps/docs/content/docs/orm/reference/raw-queries.mdx b/apps/docs/content/docs/orm/reference/raw-queries.mdx index 961d503e381..38dedc44f0c 100644 --- a/apps/docs/content/docs/orm/reference/raw-queries.mdx +++ b/apps/docs/content/docs/orm/reference/raw-queries.mdx @@ -11,18 +11,18 @@ Raw queries are the escape hatches for queries the typed APIs can't express. Rea This page covers two escape hatches: **PostgreSQL raw SQL** (the `fns.raw` tagged template for fragments and the `db.raw.sql` tag for whole statements) and **MongoDB raw commands** (`db.raw.collection(...)` and `db.query.rawCommand(...)`). Every example is transcribed from an executable test suite that runs against a live database. :::warning[Fragments and raw commands return raw driver values] -Only a whole `db.raw.sql` statement converts its results for you. `.returnsRow(spec)` names a `codecId` for every column, so its rows are converted just like a builder query's. Nothing else on this page is converted. A `fns.raw` fragment's `.returns(codecId)` only tells TypeScript what type to expect, so the value is whatever the driver returns. On MongoDB, raw results are native BSON: `_id` fields are driver `ObjectId` instances rather than hex strings, and write methods return the driver's own result objects (`{ insertedId }`, `{ matchedCount, modifiedCount }`, `{ deletedCount }`). Converting the values is up to you: compare an `ObjectId` with `String(...)`, and read those result keys directly. +A type id such as `pg/text@1` tells Prisma ORM how to turn the database's bytes into a JavaScript value. Only a whole `db.raw.sql` statement converts its results for you. `.returnsRow(spec)` names a type id for every column, so you get real JavaScript values back. Nothing else on this page is converted. A `fns.raw` fragment's `.returns(codecId)` only tells TypeScript what type to expect, so the value is whatever the driver returns. For a `text` or `integer` column that value is already a JavaScript string or number, which is why the `upperEmail` example below is a plain string. A `numeric` column arrives as a string, and a date arrives as the driver's own value. On MongoDB, raw results are native BSON. `_id` fields are driver `ObjectId` instances rather than hex strings, and write methods return the driver's own result objects (`{ insertedId }`, `{ matchedCount, modifiedCount }`, `{ deletedCount }`). Converting the values is up to you: compare an `ObjectId` with `String(...)`, and read those result keys directly. ::: ## PostgreSQL raw SQL -Raw SQL comes in two sizes. +Raw SQL comes in two forms. -A **raw fragment** is a piece of SQL spliced into a builder query: you write it as a tagged template with `fns.raw` and drop it into a `select()`, `where()`, `orderBy()`, or `update()` call. The rest of the query stays typed. +A **raw fragment** is a piece of SQL you put inside a builder query. Write it as a tagged template with `fns.raw` and pass it to a `select()`, `where()`, `orderBy()`, or `update()` call. The rest of the query stays typed. -A **whole raw statement** is a complete SQL statement written with `db.raw.sql`, which you terminate with `.returnsRow(spec).build()` for a plan `runtime.query(...)` streams rows from, or `.affectedCount().build()` for a plan `runtime.execute(...)` reports statistics for. Both sizes end in a plan, so both go through the same middleware and the same runtime. +A **whole raw statement** is a complete SQL statement written with `db.raw.sql`. If it returns rows, end it with `.returnsRow(spec).build()` and pass the result to `runtime.query(...)`. If it does not, end it with `.affectedCount().build()` and pass the result to `runtime.execute(...)`. -The examples in this section run against the `user` / `post` schema from the SQL query builder page. See its [example schema](/orm/reference/sql-query-builder#example-schema) for the full model definitions. As on that page, create the client with `postgres(...)`, connect it for a runtime, and reach tables through `db.sql.public`, which is keyed by table name (the model name with a lowercase first letter unless the model sets `@@map`): +The examples in this section run against the `user` / `post` schema from the SQL query builder page. See its [example schema](/orm/reference/sql-query-builder#example-schema) for the full model definitions. As on that page, create the client with `postgres(...)`, connect it for a runtime, and reach tables through `db.sql.public`, which is keyed by table name (the model name with a lowercase first letter unless the model sets `@@map`). On PostgreSQL you get the runtime from `await db.connect()`. On MongoDB you get it from `await db.runtime()` instead. ```ts import postgres from '@prisma/orm-postgres/runtime'; @@ -31,11 +31,13 @@ import contractJson from './contract.json' with { type: 'json' }; const db = postgres({ contractJson, url: process.env.DATABASE_URL }); const runtime = await db.connect(); + +const aliceId = '00000000-0000-4000-8000-000000000001'; ``` ### `fns.raw` in a projection -Inside a `select()` callback you get a function bag `fns` whose `raw` member is a tagged template. Write a SQL fragment, interpolate columns and values with `${...}`, and declare the fragment's result type with `.returns(codecId)`. +A `select()` callback receives two arguments: `f`, with one property per column, and `fns`, with the expression helpers. `fns.raw` is a tagged template. Write a SQL fragment, interpolate columns and values with `${...}`, and declare the fragment's result type with `.returns(codecId)`. `select()` has three forms, and the two used on this page are the aliased form below and the object form further down. See [`select()`](/orm/reference/sql-query-builder#select) for all three. ```ts const plan = db.sql.public.user @@ -47,11 +49,11 @@ const rows = await runtime.query(plan); // rows === [{ id: aliceId, upperEmail: 'ALICE@EXAMPLE.COM' }] ``` -`.returns(codecId)` is a compile-time type annotation only. It declares how the fragment's result is typed on the JavaScript side; it does not cast the value in SQL or change how the driver decodes it. See the SQL query builder's [`fns.raw` and `.returns()`](/orm/reference/sql-query-builder#fnsraw-and-returns) for the full treatment. +`.returns(codecId)` only tells TypeScript what to expect. It does not cast the value in SQL and it does not change how the driver reads it. For a type id that is not in the short list below, take the type from the column instead: `db.sql.public.user.columns.id`. See the SQL query builder's [`fns.raw` and `.returns()`](/orm/reference/sql-query-builder#fnsraw-and-returns) for the full treatment. ### `fns.raw` as a `where()` predicate -`where()`'s callback returns a boolean expression, which is the same generic `Expression` shape the comparison helpers return. A raw fragment typed `.returns('pg/bool@1')` satisfies `where()` on its own, with no `fns.eq(...)` wrapper. +`where()`'s callback returns a boolean expression, the same thing the comparison helpers such as `fns.eq(...)` return. A raw fragment declared `.returns('pg/bool@1')` is one, so it satisfies `where()` on its own, with no `fns.eq(...)` wrapper. ```ts const plan = db.sql.public.user @@ -64,7 +66,7 @@ const rows = await runtime.query(plan); ### Interpolating a typed expression -Interpolating another `Expression` (a comparison, a field reference, or nested raw) splices that expression's AST into the fragment. It is **not** a string concatenation of rendered SQL: the interpolated expression lowers to its own AST node, so it stays parameterized and type-checked. +You can interpolate another expression, such as a comparison, a column reference, or another raw fragment. Interpolated values are always sent as query parameters, never pasted into the SQL. Do not build a column or table name from user input with a template; `fns.raw` cannot parameterize an identifier. ```ts const plan = db.sql.public.user @@ -81,7 +83,7 @@ const rows = await runtime.query(plan); ### Binding a bare value with `param()` -A bare JavaScript value can be interpolated directly, and the adapter picks a `codecId` for it from its JavaScript type: `number` becomes `pg/int4@1`, `pg/int8number@1`, or `pg/float8@1` depending on its range; `bigint` becomes `pg/int8@1`; `string` becomes `pg/text@1`; `boolean` becomes `pg/bool@1`; `Uint8Array` becomes `pg/bytea@1`. Wrap the value in `param(value, { codecId })` when you want a different `codecId`, or when the value's type is not in that list. Import `param` from `@prisma/orm-postgres/relational-core/expression`. +A bare JavaScript value can be interpolated directly, and Prisma ORM picks a type id for it from the value's JavaScript type. A whole `number` within the 32-bit signed range, `-2147483648` to `2147483647`, becomes `pg/int4@1`. A larger whole number becomes `pg/int8number@1`, up to JavaScript's safe integer limit of `9007199254740991`. Every other number, including any fractional one, becomes `pg/float8@1`. A `bigint` becomes `pg/int8@1`, a `string` becomes `pg/text@1`, a `boolean` becomes `pg/bool@1`, and a `Uint8Array` becomes `pg/bytea@1`. Wrap the value in `param(value, { codecId })` when you want a different type id, or when the value's type is not in that list. Import `param` from `@prisma/orm-postgres/relational-core/expression`. ```ts import { param } from '@prisma/orm-postgres/relational-core/expression'; @@ -115,9 +117,9 @@ const rows = await runtime.query(plan); ### The client-level `db.raw.sql` tag -`db.raw` is the client's raw SQL API, beside `db.sql` and `db.orm`. Its one key, `sql`, is the same tagged template as `fns.raw`. Unlike `fns.raw`, you can use it outside a builder callback, and it has the extra methods that turn a whole statement into a query you can run. +`db.raw` is the client's raw API, beside `db.sql` and `db.orm`. On PostgreSQL it holds `sql`, the same tagged template as `fns.raw`. On MongoDB it holds `collection(...)` instead, which the [MongoDB raw commands](#mongodb-raw-commands) section covers. Unlike `fns.raw`, `db.raw.sql` works outside a builder callback, and it has the extra methods that turn a whole statement into a query you can run. -Use `.returns(...)` to build a standalone `Expression` ahead of time and splice it into a builder query: +Use `.returns(...)` to build an expression ahead of time and use it inside a builder query: ```ts const serverNow = db.raw.sql`now()`.returns('pg/timestamptz-temporal@1'); @@ -128,7 +130,7 @@ const plan = db.sql.public.user const rows = await runtime.query(plan); ``` -Use `.returnsRow(spec)` to run a whole statement and get decoded rows back. Each entry in the spec is either a contract column, which brings its own value conversion, nullability, and TypeScript type, or an explicit `codecId` for a column the contract has no counterpart for: +Use `.returnsRow(spec)` to run a whole statement and get converted rows back. The spec names the columns of every returned row. Each entry is either a column taken from your contract, such as `db.sql.public.user.columns.id`, which brings its own conversion, nullability, and TypeScript type, or an explicit type id for a column your contract has no counterpart for. Writing `FROM "user"` with no schema works because PostgreSQL resolves the name through its search path; `public."user"` is the explicit form. ```ts const user = db.sql.public.user; @@ -140,7 +142,7 @@ const plan = db.raw.sql` LEFT JOIN "post" p ON p."userId" = u.id GROUP BY u.id, u.email ORDER BY count(p.id) DESC, u.email ASC - LIMIT ${limit} + LIMIT ${10} ` .returnsRow({ id: user.columns.id, @@ -153,13 +155,13 @@ const rows = await runtime.query(plan); // row.email is a string; row.postCount is a bigint ``` -Use `.affectedCount()` for a statement that returns no rows. It takes no spec, and the plan goes to `runtime.execute(...)`, which answers with the statement's row count: +Use `.affectedCount()` for a statement that returns no rows. It takes no spec. Pass the built query to `runtime.execute(...)`, which answers with the number of rows the statement changed. You can also run a built raw query inside a transaction: `tx.query(...)` and `tx.execute(...)` take the same built queries as the runtime does. ```ts const plan = db.raw.sql` UPDATE "post" SET priority = ${'urgent'} - WHERE "userId" IN (SELECT id FROM "user" WHERE kind = ${kind}) + WHERE "userId" IN (SELECT id FROM "user" WHERE kind = ${'admin'}) ` .affectedCount() .build(); @@ -168,14 +170,14 @@ const stats = await runtime.execute(plan); // stats.affectedRows ``` -A row-returning raw query composes: interpolate one into another template and its SQL and its parameters are spliced into the outer statement, in order, which is how you build a subquery or a CTE. The outer template declares its own row spec, and a column the inner query already declared can be inherited from its `.returns` record rather than restated: +A raw query that returns rows can go inside another one. Interpolate it into the outer template, and its SQL and its values are put into the outer statement in order. That is how you write a subquery or a CTE. The outer template declares its own columns. For a column the inner query already declared, read it back from the inner query's `.returns` rather than writing it out again. On a built raw query, `.returns` is a property: a record of the columns that query declared. That is a different thing from the `.returns(codecId)` method you call on a fragment. ```ts const authorsWithPosts = db.raw.sql` SELECT p."userId" AS "userId", count(*) AS "postCount" FROM "post" p GROUP BY p."userId" - HAVING count(*) >= ${minPosts} + HAVING count(*) >= ${3} `.returnsRow({ userId: post.columns.userId, postCount: 'pg/int8@1', @@ -195,62 +197,52 @@ const plan = db.raw.sql` .build(); ``` -A statement terminated with `.affectedCount()` is a plan handle, not a row source, so it cannot be interpolated. A mutation with `RETURNING` can be, because it produces rows. +You cannot interpolate a statement that ends with `.affectedCount()`, because it produces no rows. You can interpolate an `UPDATE ... RETURNING`, because it does. ### Unsupported interpolation -Interpolation accepts columns, typed expressions, `param(...)` values, row-returning raw queries, and the bare scalar types (`number`, `bigint`, `string`, `boolean`, `Uint8Array`). Interpolating anything else, such as a `Date`, throws synchronously with code `RUNTIME.RAW_SQL_UNSUPPORTED_INTERPOLATION`: +Interpolation accepts columns, expressions, `param(...)` values, raw queries that return rows, and the bare types `number`, `bigint`, `string`, `boolean`, and `Uint8Array`. Interpolating anything else, such as a `Date`, is rejected by TypeScript when you compile. In plain JavaScript there is nothing to reject it, so the tagged template throws as soon as you call it, with an error whose `code` is `RUNTIME.RAW_SQL_UNSUPPORTED_INTERPOLATION` and this message: ``` -... wrap this value in `param(...)` with an explicit codec ... +unsupported JS value type for raw-SQL interpolation: wrap this value in `param(...)` with an explicit codec ``` -TypeScript already rejects an unsupported value at compile time. If you need to bind a `Date` (or any value with no supported type), wrap it in [`param(value, { codecId })`](#binding-a-bare-value-with-param) with the right `codecId`. +To bind a `Date`, or any value whose type is not in the list, wrap it in [`param(value, { codecId })`](#binding-a-bare-value-with-param) with the right type id. ## MongoDB raw commands -`db.raw.collection(rootName)` returns a raw collection with nine methods for running MongoDB commands directly against a collection. The root name is the contract's lowercase plural root (`'users'`, `'posts'`), the same name the ORM client uses (`db.orm.users`). +`db.raw.collection(collectionName)` returns a raw collection with nine methods for running MongoDB commands directly against a collection. The collection name is the one your contract declares (`'users'`, `'posts'`), the same name the ORM client uses (`db.orm.users`). MongoDB has no schema segment, which is why there is no `public` here and none on `db.orm`. + +Each method builds a command. Call `.build()` on it, then run it with `(await db.runtime()).query(built)`. Every one of these methods goes through `query()`, including the writes, and a write yields a single result object as its one row. Raw filters hold native BSON values, so the examples below build ids with the driver's `ObjectId` class, imported with `import { ObjectId } from 'mongodb'`. -Each method returns a buildable command: call `.build()` to get a plan, then run it with `(await db.runtime()).query(plan)`. On MongoDB `db.runtime()` returns a promise, so you await it; on PostgreSQL it is synchronous. Because raw filters carry native BSON values, the examples below construct ids with the driver's `ObjectId` class: +The examples in this section run against the `users` / `posts` schema from the pipeline builder page. See its [example schema](/orm/reference/pipeline-builder#example-schema) for the full model definitions. They use two ids declared once: ```ts -import { ObjectId } from 'mongodb'; +const aliceId = '6539a1f2c3d4e5f60718293a'; +const bobId = '6539a1f2c3d4e5f60718293b'; ``` -The examples in this section run against the `users` / `posts` schema from the pipeline builder page. See its [example schema](/orm/reference/pipeline-builder#example-schema) for the full model definitions. - -:::warning[Raw command results are undecoded] -As noted in the page-level warning at the top, raw command results are native BSON. `_id` and other ObjectId-valued fields come back as raw driver `ObjectId` instances, and write methods return the driver's own result objects rather than converted documents. Compare an `ObjectId` with `String(...)`, and read the result keys (`insertedId`, `matchedCount`, `deletedCount`) directly. -::: - ### `aggregate()` -Run a raw aggregation pipeline against a collection. The generic type parameter types the yielded rows; the pipeline stages are raw MongoDB documents. +Run a raw aggregation pipeline against a collection. The type parameter says what each returned row looks like. The pipeline stages are raw MongoDB documents. ```ts -const plan = db.raw - .collection('posts') - .aggregate<{ _id: unknown; title: string }>([{ $match: { title: 'Hello world' } }]) - .build(); +const plan = db.raw.collection('posts').aggregate<{ _id: unknown; title: string }>([{ $match: { title: 'Hello world' } }]).build(); const rows = await (await db.runtime()).query(plan); -// rows[0].title === 'Hello world' -// rows[0]._id is a raw ObjectId — String(rows[0]._id) is the hex string +// rows[0].title === 'Hello world'; every _id is a raw ObjectId, so use String(...) to compare ``` ### `insertOne()` and `insertMany()` -Insert one or many documents. Both return the driver's own insert result, with `ObjectId` instances for the generated ids. +Insert one or many documents. Both return the driver's own insert result, with `ObjectId` instances for the generated ids. `insertMany()`'s `insertedIds` is a plain array of `ObjectId`, in the order you passed the documents. ```ts -const onePlan = db.raw - .collection('users') - .insertOne({ name: 'Dave', email: 'dave@example.com', role: 'author' }) - .build(); +const users = db.raw.collection('users'); +const onePlan = users.insertOne({ name: 'Dave', email: 'dave@example.com', role: 'author' }).build(); const [oneResult] = await (await db.runtime()).query(onePlan); // { insertedId: } -const manyPlan = db.raw - .collection('users') +const manyPlan = users .insertMany([ { name: 'Eve', email: 'eve@example.com', role: 'author' }, { name: 'Frank', email: 'frank@example.com', role: 'author' }, @@ -260,19 +252,14 @@ const [manyResult] = await (await db.runtime()).query(manyPlan); // { insertedCount: 2, insertedIds: [, ] } ``` -`insertMany()`'s `insertedIds` is a plain array of `ObjectId`, reshaped from the native driver's index-keyed map. - ### `updateOne()` and `updateMany()` -Update one or every matching document. The first argument is a raw filter document; the second is either an update document (`{ $set: ... }`) or an aggregation pipeline (an array of stages). Both return `{ matchedCount, modifiedCount, upsertedCount, upsertedId }`. +Update one or every matching document. The first argument is a raw filter document. The second is either an update document such as `{ $set: ... }` or an aggregation pipeline, which is an array of stages. Both return `{ matchedCount, modifiedCount, upsertedCount, upsertedId }`. #### Update document form ```ts -const plan = db.raw - .collection('users') - .updateOne({ _id: new ObjectId(aliceId) }, { $set: { bio: 'Updated bio' } }) - .build(); +const plan = db.raw.collection('users').updateOne({ _id: new ObjectId(aliceId) }, { $set: { bio: 'Updated bio' } }).build(); const [result] = await (await db.runtime()).query(plan); // { matchedCount: 1, modifiedCount: 1, ... } ``` @@ -282,10 +269,7 @@ const [result] = await (await db.runtime()).query(plan); An array update is a full aggregation-pipeline update, so a stage can reference the document's other fields. ```ts -const plan = db.raw - .collection('users') - .updateMany({ role: 'author' }, [{ $set: { bio: { $concat: ['bio for ', '$name'] } } }]) - .build(); +const plan = db.raw.collection('users').updateMany({ role: 'author' }, [{ $set: { bio: { $concat: ['bio for ', '$name'] } } }]).build(); const [result] = await (await db.runtime()).query(plan); // { matchedCount: 2, modifiedCount: 2, ... } ``` @@ -306,29 +290,35 @@ const [manyResult] = await (await db.runtime()).query(manyPlan); ### `findOneAndUpdate()` -Atomically update a matching document and return an image of it. The third argument accepts `{ upsert }`; when no document matches and `upsert` is `true`, the update inserts one. +Atomically update a matching document and return the document as it was before the update. When no document matches and `upsert` is `true`, the update inserts one. #### Remarks -- The MongoDB command AST carries `sort` and `returnDocument` fields, but the public `findOneAndUpdate()` wrapper does **not** expose them. Its options argument is `{ upsert?: boolean }` and nothing else, so TypeScript rejects an options literal that names either key, and a value that reaches the call untyped is dropped without a run-time error. A caller who needs either must build the raw command directly with `new RawFindOneAndUpdateCommand(...)` and run it through `db.query.rawCommand(...)`, bypassing this collection wrapper. -- Because `returnDocument` is never forwarded, the native driver's own default applies, which is `'before'` (the pre-update image). This has a surprising consequence for the classic upsert-counter pattern, shown below. +- `findOneAndUpdate()` accepts only `{ upsert }` as its third argument. It cannot sort, and it cannot give you the document as it is after the update. TypeScript rejects an options object that names `sort` or `returnDocument`, and an untyped value that names either is dropped with no error at run time. +- Because `returnDocument` is never sent, the driver applies its own default, which is the document as it was before the update. The counter below shows what that costs you. + +To sort, or to read the document after the update, build the command yourself and run it through `db.query.rawCommand(...)`. The constructor takes the collection name, the filter, and the update, then `upsert`, `sort`, and `returnDocument`: + +```ts +import { RawFindOneAndUpdateCommand } from '@prisma/orm-mongo/query-ast/execution'; + +const command = new RawFindOneAndUpdateCommand('users', { _id: new ObjectId(aliceId) }, { $inc: { count: 1 } }, true, { count: -1 }, 'after'); +const [updated] = await (await db.runtime()).query(db.query.rawCommand(command)); +``` ##### Upsert counter pattern -Because the wrapper always uses the driver's `'before'` default, the **first** call to an upsert counter returns an **empty** result: the upsert creates a brand-new document, so there is no pre-image to return, the driver returns `null`, and the query yields no row. Read the counter value on the second and later calls, each of which returns the pre-image of its own increment. +The **first** call to an upsert counter returns an **empty** result. The upsert creates a brand-new document, so there is no earlier version to return, the driver returns `null`, and the query yields no row. Read the counter value on the second call and later ones, each of which returns the version before its own increment. ```ts const filter = { _id: 'pageViews' }; const update = { $inc: { count: 1 }, $setOnInsert: { _id: 'pageViews' } }; const counter = db.raw.collection('users'); const runtime = await db.runtime(); - -const first = await runtime.query(counter.findOneAndUpdate(filter, update, { upsert: true }).build()); -const second = await runtime.query(counter.findOneAndUpdate(filter, update, { upsert: true }).build()); -const third = await runtime.query(counter.findOneAndUpdate(filter, update, { upsert: true }).build()); -// first === [] — no pre-image exists on the insert -// second === [{ ..., count: 1 }] — the pre-image after the first increment -// third === [{ ..., count: 2 }] +const bump = () => runtime.query(counter.findOneAndUpdate(filter, update, { upsert: true }).build()); +const first = await bump(); // [], because the insert has no earlier version +const second = await bump(); // [{ ..., count: 1 }], the version before the second increment +const third = await bump(); // [{ ..., count: 2 }] ``` ### `findOneAndDelete()` @@ -343,14 +333,23 @@ const [deleted] = await (await db.runtime()).query(plan); ## `db.query.rawCommand()` -`db.query.rawCommand(command)` runs a raw MongoDB aggregate command through the [pipeline builder](/orm/reference/pipeline-builder), bypassing the typed AST. Its rows come back as `unknown`; you type them yourself. +`db.query.rawCommand(command)` runs a raw MongoDB aggregate command through the [pipeline builder](/orm/reference/pipeline-builder), with no translation of the command you pass. Unlike `aggregate()` it takes no type parameter, so its rows come back as `unknown` and you cast them yourself. ```ts import { RawAggregateCommand } from '@prisma/orm-mongo/query-ast/execution'; const plan = db.query.rawCommand(new RawAggregateCommand('posts', [{ $count: 'total' }])); -const rows = await (await db.runtime()).query(plan); +const rows = (await (await db.runtime()).query(plan)) as { total: number }[]; // [{ total: 2 }] ``` -`rawCommand()` is the escape hatch for anything the typed pipeline builder can't express, including `_id` equality filters (a real `ObjectId` in a raw pipeline document matches correctly) and `$redact` with `$$KEEP` / `$$PRUNE`. For the full treatment, including the working `_id`-filter pattern, see the pipeline builder's [`rawCommand()`](/orm/reference/pipeline-builder#rawcommand) section. +`rawCommand()` is the escape hatch for anything the typed pipeline builder can't express. Two of those are `_id` equality filters and `$redact` with `$$KEEP` / `$$PRUNE`. Filtering by `_id` works here because a real `ObjectId` in a raw pipeline document matches: + +```ts +const plan = db.query.rawCommand( + new RawAggregateCommand('posts', [{ $match: { _id: new ObjectId('6539a1f2c3d4e5f60718293c') } }]), +); +const rows = await (await db.runtime()).query(plan); +``` + +For the full treatment see the pipeline builder's [`rawCommand()`](/orm/reference/pipeline-builder#rawcommand) section. 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 784c637134d..0f534727519 100644 --- a/apps/docs/content/docs/orm/reference/sql-query-builder.mdx +++ b/apps/docs/content/docs/orm/reference/sql-query-builder.mdx @@ -6,9 +6,9 @@ metaTitle: Prisma ORM SQL query builder reference metaDescription: 'Reference for the Prisma ORM SQL query builder''s select, mutation, and grouped query methods.' --- -The SQL query builder gives you table-level, SQL-shaped methods for building typed queries: `select()`, `innerJoin()`, `groupBy()`, and the rest. It's the lower-level escape hatch that is available alongside the [ORM client](/orm/reference/orm-client). Reach for it when you need a join, aggregate, or SQL feature the ORM client doesn't expose, or when you want direct control over the generated SQL. +The SQL query builder gives you table-level, SQL-shaped methods for building typed queries: `select()`, `innerJoin()`, `groupBy()`, and the rest. It is closer to SQL than the [ORM client](/orm/reference/orm-client) is, and the same `db` gives you both. Reach for it when you need a join, aggregate, or SQL feature the ORM client doesn't expose, or when you want direct control over the generated SQL. -This page documents every method, its options, and its return type. Where a method works only on some databases, or has a caveat that only shows up when the query runs, the Remarks call that out. For a task-oriented guide to when and how to reach for the builder, see [Advanced queries](/orm/fundamentals/advanced-queries). +This page documents every method, its options, and its return type. Some methods work only on some databases, and some have a caveat that shows up only when the query runs. Each method's **Remarks** section says so. For a task-oriented guide to when and how to reach for the builder, see [Advanced queries](/orm/fundamentals/advanced-queries). :::note[This is the SQL-family builder] The SQL query builder targets SQL databases; PostgreSQL and SQLite are supported today. MongoDB has no SQL builder: use the [ORM client](/orm/reference/orm-client) for MongoDB queries, and the [pipeline builder](/orm/reference/pipeline-builder) for aggregation pipelines. @@ -16,7 +16,7 @@ The SQL query builder targets SQL databases; PostgreSQL and SQLite are supported ## Example schema -All examples on this page run against the following schema. The select and mutation examples use this `user` / `post` / `post_tag` / `tag` schema; the grouped-query examples use a separate `customer` / `order` schema, shown under [Grouped queries](#grouped-queries). +All examples on this page run against the following schema. The select and mutation examples use the `user`, `post`, `post_tag`, and `tag` tables. The grouped-query examples use a separate `customer` and `order` schema, shown under [Grouped queries](#grouped-queries). No example uses `Address`, `Task`, `Bug`, or `Feature`. They are here only so that this schema matches the one on the [ORM client reference](/orm/reference/orm-client), and you can ignore them.
@@ -128,11 +128,11 @@ model Feature { ## Entry points -You build queries from the client's `sql` property, which knows your contract and how your queries are run. Create a Postgres client with `postgres(...)`, then reach tables through `db.sql.public`. Table accessors are keyed by table name, not model name. A table name is the model name with a lowercase first letter unless the model sets `@@map`, so the `User` model mapped to `user` is reached as `db.sql.public.user`, and the model for the `Post`/`Tag` join table, mapped to `post_tag`, is reached as `db.sql.public.post_tag`. Column names are the field names unless a field sets `@map`. `public` is the PostgreSQL schema, the namespace your tables live in unless you set another one. +You build queries from the client's `sql` property. Create a Postgres client with `postgres(...)`, then reach tables through `db.sql.public`. The client reads your contract, the `contract.prisma` file that replaced `schema.prisma`. Running `npx prisma contract emit` compiles the contract into the two files every example here imports: `contract.json`, read when your app runs, and `contract.d.ts`, read by TypeScript. [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7) has the packages to install and the commands to run. Table accessors are keyed by table name, not model name. A table name is the model name with a lowercase first letter unless the model sets `@@map`, so the `User` model mapped to `user` is reached as `db.sql.public.user`, and the model for the `Post`/`Tag` join table, mapped to `post_tag`, is reached as `db.sql.public.post_tag`. Column names are the field names unless a field sets `@map`. `public` is the PostgreSQL schema, the namespace your tables live in unless you set another one. ### The `db.sql` builder [#the-dbsql-facet] -Create the client with `postgres(...)`, take a runtime from `db.runtime()`, and build queries off `db.sql.public`. On PostgreSQL `db.runtime()` is synchronous, so there is nothing to await. A `select()`, `insert()`, `update()`, or `delete()` call starts a query; you finish it with [`build()`](#build) and run the built query with `runtime.query(...)`. +Create the client with `postgres(...)` and build queries off `db.sql.public`. `db.runtime()` is the connection that runs a built query. On PostgreSQL it is synchronous, so do not await it. A `select()`, `insert()`, `update()`, or `delete()` call starts a query. Finish it with [`build()`](#build), which returns the built query, and run that query with `runtime.query(...)`. ```ts import postgres from '@prisma/orm-postgres/runtime'; @@ -146,19 +146,19 @@ const plan = db.sql.public.user.select('id', 'email').build(); const users = await runtime.query(plan); ``` -`db.sql` holds the result of an internal `sql({ context, rawCodecInferer })` call, connected to the client's way of running queries and to the adapter's rule for how values are stored and read. You call `sql(...)` directly only when you're building your own database wrapper instead of using the builder on the `postgres()` client. Pass your `context` (from your database client) and a `rawCodecInferer` (a fallback rule for how to store and read raw expressions that have no column to take that from): +`db.sql` is ready to use as soon as you have a client, so most code needs nothing more. The `sql(...)` function from `@prisma/orm-postgres/builder/runtime` builds the same query builder around your own database client instead, from that client's `db.context`: ```ts import { sql } from '@prisma/orm-postgres/builder/runtime'; -const publicSql = sql({ context, rawCodecInferer: { inferCodec: () => 'pg/text@1' } }).public; +const publicSql = sql({ context: db.context, rawCodecInferer: { inferCodec: () => 'pg/text@1' } }).public; ``` -This page reaches tables through the client's builder and refers to the root as `db.sql.public`. +SQLite works the same way. Create the client with `sqlite(...)` from `@prisma/orm-sqlite/runtime`. SQLite has no schemas, so there is no `public` in the path and the same table is `db.sql.user`. The rest of this page shows PostgreSQL and writes the root as `db.sql.public`. ### Table access -Every table on `db.sql.public` exposes the query-building methods. A `select()`, `insert()`, `update()`, or `delete()` call starts a query; you finish it with [`build()`](#build) and run the built query with `runtime.query(...)`. +Every table on `db.sql.public` exposes the query-building methods. ```ts db.sql.public.user.select('id', 'email'); // start a SELECT @@ -167,7 +167,7 @@ db.sql.public.tag.insert([{ id, label: 'typescript' }]); // start an INSERT ### Aliasing a query with `.as()` -A completed `SELECT` query can be used as a subquery source by calling `.as(alias)` on it. The aliased query becomes a join source you can pass to `innerJoin()`, `outerLeftJoin()`, and the other join methods. See [Subquery via `.as()`](#subquery-via-as). +Call `.as(alias)` on a finished `SELECT` query to use that query as a subquery. The aliased query is then something you can pass to `innerJoin()`, `outerLeftJoin()`, and the other join methods. See [Subquery via `.as()`](#subquery-via-as). ```ts const highPriorityPosts = db.sql.public.post @@ -177,14 +177,16 @@ const highPriorityPosts = db.sql.public.post ``` :::warning[`.as()` is not for lateral joins] -`.as()` returns a plain join source. A [`lateralJoin()`](#lateraljoin) callback must return the query chain directly, not the result of `.as()`. See that method's Remarks. +Use `.as()` only for subqueries you pass to `innerJoin()`, `outerLeftJoin()`, `outerRightJoin()`, or `outerFullJoin()`. Inside a [`lateralJoin()`](#lateraljoin) callback, return the query chain itself and do not call `.as()` on it. See that method's Remarks. ::: ## SELECT queries These methods build and refine a `SELECT`. They chain, and you resolve the query with [`build()`](#build) followed by `runtime.query(...)`. -The `where()`, `select()`, `orderBy()`, `update()`, and other callback forms receive two arguments: the `f` argument, one property per column (after a join, columns are grouped under their table name, such as `f.post.id`), and a function bag `fns` (the [expression helpers](#expressions-and-functions)). +The `where()`, `select()`, `orderBy()`, `update()`, and other callback forms receive two arguments. The first is `f`, which has one property per column the query can name at that point. After a join, columns are grouped under their table name, such as `f.post.id`. The second is `fns`, an object of [expression helpers](#expressions-and-functions) such as `fns.eq()` and `fns.raw`. + +The set of names the query can use is its scope. It starts as the columns of the table you began with, and every join adds the joined table's columns to it. ### `select()` @@ -196,11 +198,13 @@ Project a row down to a chosen set of columns or computed expressions. | Form | Signature | Description | |---|---|---| -| Column names | `select('col', 'col2', ...)` | Keep the named columns. Each name resolves against the current scope; an unknown name throws an error whose `code` is `ORM.COLUMN_UNKNOWN`. | +| Column names | `select('col', 'col2', ...)` | Keep the named columns. A name that is not in the query's scope throws an error whose `code` is `ORM.COLUMN_UNKNOWN`, at the moment you call `select()`. | | Aliased expression | `select(alias, (f, fns) => expr)` | Add one computed column under `alias`. | | Object of expressions | `select((f, fns) => ({ alias: expr, ... }))` | Add several computed columns at once. The row type is inferred from the object's shape. | -A computed expression's result type comes from `.returns(...)` on `fns.raw`, or from the operation's own declared return type. See [`fns.raw` and `.returns()`](#fnsraw-and-returns). +Every form adds to what the query already returns. Calling `select()` a second time never replaces the first call's columns, whichever form you use. + +A computed expression's result type comes from `.returns(...)` on `fns.raw`, or from the operation's own declared return type. A type id such as `pg/int4@1` is the PostgreSQL type plus a version, always `@1` today. [Raw queries](/orm/reference/raw-queries) shows the ones a bare JavaScript value gets, and you can take one from a column: `db.sql.public.user.columns.id`. See [`fns.raw` and `.returns()`](#fnsraw-and-returns). #### Return type @@ -215,7 +219,7 @@ A computed expression's result type comes from `.returns(...)` on `fns.raw`, or ```ts const plan = db.sql.public.user.select('id', 'email').build(); const rows = await runtime.query(plan); -// rows[0] is { id, email } — no displayName +// rows[0] is { id, email }, with no displayName ``` ##### Add an aliased computed column @@ -336,16 +340,16 @@ Correlate a per-row subquery against the outer row: for each outer row, the join #### Remarks -- **Availability:** PostgreSQL only. On an adapter without lateral joins, such as SQLite, TypeScript types `lateralJoin` as `never`, so the call does not compile; calling it anyway throws an error whose `code` is `ORM.CAPABILITY_MISSING`. `outerLateralJoin()` is the left-join form: same arguments, same availability, and the subquery's columns are nullable. -- The callback receives a `lateral` builder. Start its subquery with `lateral.from(otherTable)`, then chain the usual `SELECT` methods. The subquery can filter on the outer row's columns (`f.user.id`). -- Return the query chain **directly** from the callback. Do **not** call `.as(...)` on it: `lateralJoin()`'s own first argument already names the derived table, and `.as(...)` returns the wrong shape (a bare join source), which throws `subquery.getRowFields is not a function`. -- When the outer table and the lateral table share a column name (for example both have `id` and `createdAt`), the merged scope **drops** the ambiguous names. Reach them through their table name instead (`f.post.id`, `f.post.createdAt`); a bare `select('id')` or `orderBy((f) => f.createdAt, ...)` throws because the ambiguous name isn't in scope. +- **Availability:** PostgreSQL only. Not available on SQLite, where the call does not compile. If you reach it anyway, it throws an error whose `code` is `ORM.CAPABILITY_MISSING` at the moment you call `lateralJoin()`. `outerLateralJoin()` is the left-join form: same arguments, same availability, and the subquery's columns are nullable. +- The callback receives a `lateral` builder. Start its subquery with `lateral.from(otherTable)`, then chain the usual `SELECT` methods. The subquery can filter on the outer row's columns, such as `f.user.id`. +- Return the query chain **directly** from the callback. Do **not** call `.as(...)` on it. The first argument of `lateralJoin()` already names the subquery, and a chain with `.as(...)` on the end throws `subquery.getRowFields is not a function`. +- After the join, a column name that both tables have must be written under its table, such as `f.post.id` or `f.post.createdAt`. A bare `select('id')` or `orderBy((f) => f.createdAt, ...)` throws, because a name both tables have is not in the query's scope on its own. #### Options | Name | Type | Required | Description | |---|---|---|---| -| `alias` | `string` | Yes | Names the derived table. Address its columns as `f..
` in later `select()`/`where()` calls. | +| `alias` | `string` | Yes | Names the subquery. Address its columns as `f..` in later `select()`/`where()` calls. | | `build` | `(lateral) => SelectQuery` | Yes | Builds the correlated subquery. Return the query chain directly. | #### Return type @@ -377,7 +381,7 @@ const rows = await runtime.query(plan); ### `orderBy()` -Sort the result set by a column, a computed expression, or with explicit direction and null placement. +Sort the result set by a column or a computed expression, in a direction you choose. #### Options @@ -387,7 +391,8 @@ Sort the result set by a column, a computed expression, or with explicit directi |---|---|---|---| | `key` | Column name (`string`) or `(f, fns) => Expression` | Yes | The column or computed value to sort by. | | `options.direction` | `'asc' \| 'desc'` | No | Sort direction. | -| `options.nulls` | `'first' \| 'last'` | No | TypeScript accepts it, but nothing reads it: no `NULLS FIRST` or `NULLS LAST` reaches the SQL, so the database's own null ordering applies. | + +TypeScript also accepts a `nulls` option, but the builder ignores it and no `NULLS FIRST` or `NULLS LAST` reaches the SQL. Do not use it. #### Return type @@ -414,16 +419,6 @@ const plan = db.sql.public.user const rows = await runtime.query(plan); ``` -##### Control direction and null placement - -```ts -const plan = db.sql.public.post - .select('id', 'embedding') - .orderBy('embedding', { direction: 'asc', nulls: 'first' }) // nulls adds nothing to the SQL - .build(); -const rows = await runtime.query(plan); -``` - ### `distinct()` De-duplicate identical projected rows (`SELECT DISTINCT`). @@ -450,8 +445,8 @@ Keep the first row per distinct key, according to the query's `orderBy()` (`DIST #### Remarks -- **Availability:** PostgreSQL only. `DISTINCT ON` is a PostgreSQL extension to SQL. On SQLite TypeScript types `distinctOn` as `never`, so the call does not compile; calling it anyway throws an error whose `code` is `ORM.CAPABILITY_MISSING`. -- Pair it with an `orderBy()` on the same key(s) to control which row is kept per key. This ordering requirement is not type-enforced; supply it yourself for a meaningful result. +- **Availability:** PostgreSQL only. `DISTINCT ON` is a PostgreSQL extension to SQL. Not available on SQLite, where the call does not compile. If you reach it anyway, it throws an error whose `code` is `ORM.CAPABILITY_MISSING` at the moment you call `distinctOn()`. +- Put the `distinctOn()` columns first in `orderBy()`, then the column that decides which row wins. Nothing checks this for you, and without it you cannot predict which row you get. The order in which you call `distinctOn()` and `orderBy()` does not matter. #### Options @@ -513,7 +508,7 @@ const rows = await runtime.query(plan); ### Subquery via `.as()` -Alias a completed `SELECT` query with `.as(alias)` to use it as a join source. The aliased query can be passed to any join method as the `other` argument. +Call `.as(alias)` on a finished `SELECT` query to use it as a subquery. Pass the result to any join method as the `other` argument. `SELECT` queries and [grouped queries](#grouped-queries) have `.as()`. Queries started with `insert()`, `update()`, or `delete()` do not, so you cannot join against the rows they return. #### Options @@ -525,7 +520,7 @@ Alias a completed `SELECT` query with `.as(alias)` to use it as a join source. T | Return type | Example | Description | |---|---|---| -| Join source | `db.sql.public.post.select(...).as('hp')` | A subquery usable as a join `other`. This is not a buildable query; join it into an outer query first. | +| Subquery | `db.sql.public.post.select(...).as('hp')` | A subquery you pass as a join's `other` argument. You cannot call `build()` on it. Join it into an outer query first. | #### Examples @@ -580,8 +575,8 @@ model Order { -:::note[Aggregates decode as numbers, with lossless variants beside them] -`count()`, `sum()` over an integer column, and `avg()` over an integer column all return a JavaScript `number`. For `count()` and integer `sum()`, a value outside the safe-integer range, ±(2^53 − 1), raises `RUNTIME.DECODE_FAILED` rather than answering with a rounded value (`avg()` is computed as `float8` and has no such guard), so reach for the lossless form when the magnitude is real: `countBigInt()` returns a `bigint`, `sumBigInt(field)` returns a `bigint`, and `avgDecimal(field)` returns an exact decimal string. `min()` and `max()` return the input column's own type. +:::note[Aggregates return numbers, with exact variants beside them] +`count()`, `sum()` over an integer column, and `avg()` over an integer column all return a JavaScript `number`. If a `count()` or integer `sum()` result is larger than 2^53 - 1, the query fails with an error whose `code` is `RUNTIME.DECODE_FAILED`. Use `countBigInt()` or `sumBigInt(field)` instead when totals can get that large. `avg()` does not fail this way. It is computed as a floating-point `number` and loses precision instead, so use `avgDecimal(field)` when you need an exact figure. `min()` and `max()` return the input column's own type. ::: ### `groupBy()` @@ -605,7 +600,11 @@ Group rows by one or more columns or by a computed expression, producing one row ##### Group by a column with a count +Every example from here on calls `runtime`, and names the built query `plan`. + ```ts +const runtime = db.runtime(); + const plan = db.sql.public.order .select('customerId') .select('orderCount', (f, fns) => fns.count(f.id)) @@ -613,7 +612,7 @@ const plan = db.sql.public.order .orderBy('customerId', { direction: 'asc' }) .build(); const rows = await runtime.query(plan); -// e.g. { customerId: acmeId, orderCount: 5 } +// e.g. { customerId: '', orderCount: 5 } ``` ##### Group by a computed value @@ -626,16 +625,18 @@ const plan = db.sql.public.order .build(); const rows = await runtime.query(plan); // rows === [{ yearPlaced: '2024', orderCount: 10 }] -// yearPlaced is a string: Prisma ORM does not convert what a raw expression returns, and EXTRACT returns numeric. +// yearPlaced is the string '2024', not the number 2024. See `.returns()` below. ``` +You can also group by an alias you gave a selected value, such as `.groupBy('yearPlaced')`. A name that is neither a column in scope nor an alias you selected throws an error whose `code` is `ORM.COLUMN_UNKNOWN`, at the moment you call `groupBy()`. Prisma ORM does not check that every other selected column is grouped or aggregated. PostgreSQL rejects the query when it runs. + ### `having()` Filter groups by an aggregate comparison. Build the predicate from the aggregate functions (`fns.count`, `fns.sum`, `fns.avg`, `fns.min`, `fns.max`) and the comparison functions, comparing against a JavaScript literal. #### Remarks -- PostgreSQL evaluates the comparison server-side. Compare against a plain JavaScript number: `fns.gt(fns.count(), 1)`, not `1n`. +- Compare against a plain JavaScript number, as in `fns.gt(fns.count(), 1)`. Do not pass a `bigint` literal such as `1n`. #### Options @@ -684,13 +685,13 @@ const minMaxPlan = db.sql.public.order const avgRows = await runtime.query(avgPlan); const minMaxRows = await runtime.query(minMaxPlan); -// avgAmount is 300; avgDecimal(f.amount) would give the exact '300.0000000000000000' -// minAmount / maxAmount are 100 / 500 +// avgAmount is 300, and avgDecimal(f.amount) would give the exact '300.0000000000000000' +// minAmount is 100 and maxAmount is 500 ``` ### Ordering and limiting a grouped query -A `GroupedQuery` supports the same `orderBy()`, `limit()`, `offset()`, `distinct()`, and `distinctOn()` methods as a `SelectQuery`. Sort by an aggregate alias to order groups. +A `GroupedQuery` supports the same `orderBy()`, `limit()`, `offset()`, `distinct()`, and [`distinctOn()`](#distincton) methods as a `SelectQuery`. Sort by an aggregate alias to order groups. The alias must be one you selected in this query. Any other name throws an error whose `code` is `ORM.COLUMN_UNKNOWN`, at the moment you call `orderBy()`. #### Examples @@ -718,14 +719,17 @@ Insert one or more rows in a single statement. #### Remarks -- `insert()` always takes an **array**. A single-row insert is a one-element array; there is no separate single-row overload. -- Multiple rows are inserted in one `INSERT ... VALUES (...), (...)` statement, not one round-trip per row. +- `insert()` always takes an **array**. A single-row insert is a one-element array. There is no separate single-row overload. An empty array throws an error whose `code` is `ORM.MUTATION_DATA_MISSING`. +- Leave a column out and the contract's default for it is applied, such as `@default(uuid())` or `@default(now())`. The value is generated when you call `build()`. +- There is no `ON CONFLICT` and no upsert here. For "insert or update", use the ORM client's [`upsert()`](/orm/reference/orm-client#upsert) or write the statement with [`db.raw.sql`](/orm/reference/raw-queries). #### Options | Name | Type | Required | Description | |---|---|---|---| -| `rows` | Array of row objects | Yes | The rows to insert. Each value is sent as a query parameter, never pasted into the SQL; how it is stored and read comes from the target column. | +| `rows` | Array of row objects | Yes | The rows to insert. Every column is optional. | + +Each value is sent as a query parameter, never pasted into the SQL. The column it goes into decides how it is stored and read, so you never annotate an insert value. #### Return type @@ -738,18 +742,16 @@ Insert one or more rows in a single statement. ##### Insert a single row ```ts -const plan = db.sql.public.tag.insert([{ id: crypto.randomUUID(), label: 'single-row-tag' }]).build(); +const plan = db.sql.public.tag.insert([{ label: 'single-row-tag' }]).build(); await runtime.execute(plan); +// id is filled in from the contract's @default(uuid()) ``` ##### Insert multiple rows in one statement ```ts const plan = db.sql.public.tag - .insert([ - { id: crypto.randomUUID(), label: 'multi-row-a' }, - { id: crypto.randomUUID(), label: 'multi-row-b' }, - ]) + .insert([{ label: 'multi-row-a' }, { label: 'multi-row-b' }]) .build(); await runtime.execute(plan); ``` @@ -760,7 +762,7 @@ Return columns from the rows affected by an `insert()`, `update()`, or `delete() #### Remarks -- **Availability:** PostgreSQL and SQLite. Both support SQL's `RETURNING`. On an adapter for a database without it, TypeScript types `returning` as `never`, so the call does not compile; calling it anyway throws an error whose `code` is `ORM.CAPABILITY_MISSING`. +- **Availability:** PostgreSQL and SQLite. Both support SQL's `RETURNING`. On a database without it, TypeScript rejects the call. If you get past the type check, for example by casting, the call throws an error whose `code` is `ORM.CAPABILITY_MISSING`. #### Options @@ -779,15 +781,14 @@ Return columns from the rows affected by an `insert()`, `update()`, or `delete() ##### Return the inserted row ```ts -const id = crypto.randomUUID(); -const plan = db.sql.public.tag.insert([{ id, label: 'returned-tag' }]).returning('id', 'label').build(); +const plan = db.sql.public.tag.insert([{ label: 'returned-tag' }]).returning('id', 'label').build(); const rows = await runtime.query(plan); -// rows === [{ id, label: 'returned-tag' }] +// rows === [{ id: '', label: 'returned-tag' }] ``` ### `update()` -Update matched rows. Set columns with a values object, or derive new values from existing columns with an expression callback. Gate the update with `where()`, and use [`returning()`](#returning) to get the updated rows. +Update matched rows. Set columns with a values object, or derive new values from existing columns with an expression callback. Choose the rows with `where()`, and use [`returning()`](#returning) to get the updated rows. `where()` is optional. An `update()` with no `where()` updates every row in the table, and nothing warns you. #### Options @@ -811,11 +812,11 @@ Update matched rows. Set columns with a values object, or derive new values from ```ts const plan = db.sql.public.user .update({ displayName: 'Bobby' }) - .where((f, fns) => fns.eq(f.id, bobId)) + .where((f, fns) => fns.eq(f.email, 'bob@example.com')) .returning('id', 'displayName') .build(); const rows = await runtime.query(plan); -// rows === [{ id: bobId, displayName: 'Bobby' }] +// one row, with displayName now 'Bobby' ``` ##### Derive a value from an existing column @@ -823,16 +824,16 @@ const rows = await runtime.query(plan); ```ts const plan = db.sql.public.user .update((f, fns) => ({ displayName: fns.raw`UPPER(${f.displayName})`.returns('pg/text@1') })) - .where((f, fns) => fns.eq(f.id, carolId)) + .where((f, fns) => fns.eq(f.email, 'carol@example.com')) .returning('id', 'displayName') .build(); const rows = await runtime.query(plan); -// rows === [{ id: carolId, displayName: 'CAROL' }] +// one row, with displayName now 'CAROL' ``` ### `delete()` -Delete matched rows. Gate the delete with `where()`, and use [`returning()`](#returning) to get the deleted rows back. +Delete matched rows. `delete()` takes no arguments. Choose the rows with `where()`, and use [`returning()`](#returning) to get the deleted rows back. `where()` is optional. A `delete()` with no `where()` deletes every row in the table, and nothing warns you. #### Return type @@ -847,29 +848,28 @@ Delete matched rows. Gate the delete with `where()`, and use [`returning()`](#re ```ts const plan = db.sql.public.tag .delete() - .where((f, fns) => fns.eq(f.id, id)) + .where((f, fns) => fns.eq(f.label, 'to-delete')) .returning('id', 'label') .build(); const rows = await runtime.query(plan); -// rows === [{ id, label: 'to-delete' }] +// one row, the tag that was deleted ``` ### `param()` -Say explicitly how a raw value is stored and read when it has no column to take that from, for example a literal interpolated into `fns.raw`. +Give a value its type by hand, for a value with no column to take the type from, such as a literal inside `fns.raw`. #### Remarks - Import `param` from `@prisma/orm-postgres/relational-core/expression`. -- You rarely need `param()`. Values passed to `insert()`, `update()`, and comparison functions such as `fns.eq(f.col, value)` are already sent as query parameters, never pasted into the SQL, and how each is stored and read comes from the target column. `param()` is the manual escape hatch for a bound value with no neighboring column to take that from. -- There is **no** `build({ params })` form. Parameter values are embedded into the query where they're supplied. See [`build()`](#build). +- You rarely need `param()`. Values passed to `insert()`, `update()`, and comparison functions such as `fns.eq(f.col, value)` are already sent as query parameters, never pasted into the SQL, and each takes its type from the column it is used with. `param()` is for the values that have no such column. #### Options | Name | Type | Required | Description | |---|---|---|---| | `value` | `T` | Yes | The value to bind. | -| `opts.codecId` | `string` | Yes | The type id to encode the value with, for example `'pg/text@1'`. | +| `opts.codecId` | `string` | Yes | The type id to send the value as, for example `'pg/text@1'`. | #### Return type @@ -895,7 +895,7 @@ const rows = await runtime.query(plan); ## Expressions and functions -Callback forms receive a function bag `fns` alongside the `f` argument. `fns` holds the built-in expression helpers you use to build comparisons, boolean combinators, aggregates, and raw SQL. +Callback forms receive `fns` alongside the `f` argument. `fns` is an object of helper functions: comparisons, boolean combinations, aggregates, and raw SQL. ### Built-in functions @@ -909,20 +909,20 @@ These are fixed: | Existence | `exists`, `notExists` | | Raw SQL | `raw` | -The aggregates are not a fixed list. They come from the aggregate map in the contract written by `prisma contract emit`, which each target and extension contributes to, so which ones exist depends on your database. PostgreSQL contributes eight: +Which aggregate functions exist depends on your database. PostgreSQL gives you these eight: | Function | Returns | |---|---| | `count()` / `count(field)` | `number`. With no argument it counts rows; with a field it counts that field's non-null values | -| `countBigInt()` / `countBigInt(field)` | `bigint`, the lossless form of `count` | +| `countBigInt()` / `countBigInt(field)` | `bigint`, the exact form of `count` | | `sum(field)` | `number` over an integer column; over any other column, the type PostgreSQL's own `sum` produces for it | -| `sumBigInt(field)` | `bigint`, the lossless form of `sum` over an integer column | +| `sumBigInt(field)` | `bigint`, the exact form of `sum` over an integer column | | `avg(field)` | `number` over an integer or float column | -| `avgDecimal(field)` | An exact decimal string, the lossless form of `avg` | -| `min(field)` | The input column's own type; a `varchar` column widens to `text` | -| `max(field)` | The input column's own type; a `varchar` column widens to `text` | +| `avgDecimal(field)` | An exact decimal string, the exact form of `avg` | +| `min(field)` | The input column's own type. A `varchar` column is typed as `text` | +| `max(field)` | The input column's own type. A `varchar` column is typed as `text` | -Any further function is registered dynamically: `ilike` is registered by the Postgres adapter for textual columns, while functions like `cosineDistance` come from an extension pack (pgvector) registered for your contract; without the matching adapter or extension pack, those functions are absent. +On PostgreSQL you also get `ilike` for text columns. `cosineDistance` comes with pgvector, so it is there only if your contract uses pgvector. :::note[No `COALESCE` or `CAST` helpers] There is no `fns.coalesce` or `fns.cast`. Express `COALESCE`, `CAST`, and any other SQL function you need with [`fns.raw`](#fnsraw-and-returns) and an explicit `.returns(...)` call. @@ -930,18 +930,18 @@ There is no `fns.coalesce` or `fns.cast`. Express `COALESCE`, `CAST`, and any ot ### `fns.raw` and `.returns()` -Write a raw SQL fragment as a tagged template. Interpolate columns, typed expressions, and bare JavaScript values with `${...}`; each interpolation is parameterized. For a bare value, the builder picks the `codecId` from its JavaScript type. Wrap it in `param(...)` with an explicit `codecId` when you want a different one (see the [raw queries reference](/orm/reference/raw-queries#binding-a-bare-value-with-param)). Call `.returns(codecId)` to declare the fragment's result type. +Write a raw SQL fragment as a tagged template. Interpolate columns, typed expressions, and bare JavaScript values with `${...}`. Each interpolation is sent as a query parameter, never pasted into the SQL. For a bare value, the builder picks the type id from its JavaScript type. [Binding a bare value with `param()`](/orm/reference/raw-queries#binding-a-bare-value-with-param) lists which JavaScript type becomes which type id, and shows how to choose a different one with `param(...)`. Call `.returns(codecId)` to declare the fragment's result type. #### Options | Name | Type | Required | Description | |---|---|---|---| | SQL fragment | Tagged template | Yes | The raw SQL, with `${...}` interpolations for columns and values. | -| `.returns(codecId)` | `string` | Yes for a projected or compared value | Declares the result type, for example `'pg/int4@1'`, `'pg/text@1'`, `'pg/bool@1'`. | +| `.returns(codecId)` | `string` | Yes for a selected value, a compared value, or a fragment used on its own as a `where()` predicate | Declares the result type, for example `'pg/int4@1'`, `'pg/text@1'`. A fragment used as a `where()` predicate declares `'pg/bool@1'`. | #### Remarks -- `.returns(codecId)` is a **compile-time type annotation only**. It does not cast the value in SQL or change how the driver decodes it. A raw expression PostgreSQL computes as `numeric` still arrives as a string even when annotated `'pg/int4@1'`, because Prisma ORM does not convert the values a raw fragment returns (see [Group by a computed value](#group-by-a-computed-value)). +- **`.returns()` only tells TypeScript what type to expect.** It does not change the value you actually get back. If PostgreSQL computes the value as `numeric`, you get a string at run time even though you annotated `'pg/int4@1'`. See [Group by a computed value](#group-by-a-computed-value). - Use `fns.raw` for any SQL feature without a dedicated helper: `COALESCE`, `CAST`, `LENGTH`, `UPPER`, `EXTRACT`, and so on. #### Examples @@ -955,15 +955,6 @@ const plan = db.sql.public.user const rows = await runtime.query(plan); ``` -##### `COALESCE` via `fns.raw` - -```ts -const plan = db.sql.public.order - .select('amountOrZero', (f, fns) => fns.raw`COALESCE(${f.amount}, 0)`.returns('pg/int4@1')) - .build(); -const rows = await runtime.query(plan); -``` - ## Compiling and executing ### `build()` @@ -972,7 +963,7 @@ Compile a query so you can run it. #### Remarks -- `build()` takes **zero arguments** on every query type: select, insert, update, delete, and grouped. Parameter values are embedded into the built query where they're supplied (inside `insert([...])`, a `where()` callback, or a [`param()`](#param) call), not passed to `build()`. There is no `build({ params })` form. +- `build()` takes **zero arguments** on every query type: select, insert, update, delete, and grouped. You supply parameter values where you write them, inside `insert([...])`, a `where()` callback, or a [`param()`](#param) call. There is no `build({ params })` form. #### Return type @@ -982,7 +973,7 @@ Compile a query so you can run it. ### Running a built query [#executing-a-plan] -Run a built query with `runtime.query(plan)`, where `runtime` is `db.runtime()`. It returns an `AsyncIterableResult`: `await` it for an array of rows (`Row[]`), or `for await` over it to take one row at a time. `runtime.execute(plan)` is the separate call for writes that return no rows; it resolves to `{ affectedRows }`. +Run a built query with `runtime.query(...)`, where `runtime` is `db.runtime()`. The rule is: use `query` for anything that returns rows, which means a `SELECT` or a write with `returning()`. Use `execute` for a write with no `returning()`. `query` returns an `AsyncIterableResult`. `await` it for an array of rows (`Row[]`), or `for await` over it to take one row at a time. `execute` resolves to `{ affectedRows }`. ```ts const plan = db.sql.public.user.select('id', 'email').build(); @@ -995,7 +986,7 @@ Recover a built query's row type at the type level. #### Remarks -- Import `ResultType` from `@prisma/orm-postgres/components/runtime`. It's a family-agnostic utility, not specific to the SQL builder. +- Import `ResultType` from `@prisma/orm-postgres/components/runtime`. It works with the ORM client as well as the SQL builder. - `ResultType` is **one row**, 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 [Model and result types](/orm/reference/orm-client#model-and-result-types). @@ -1014,4 +1005,8 @@ const rows = await runtime.query(plan); // Row[] ### Streaming vs. collecting -`await runtime.query(plan)` collects every row into an array. This is the common case and what every example on this page uses. For a large result set, loop the same result with `for await` to take one row at a time. Each result is read once, one way: see [`AsyncIterableResult`](/orm/reference/orm-client#asynciterableresult). Since `8.0.0-rc.10` a buffered query hands its pooled connection back before you read the rows, so a paused loop no longer blocks other queries; a cursor stream keeps its connection until it finishes. +`await runtime.query(plan)` collects every row into an array. This is the common case and what every example on this page uses. You can also loop the same result with `for await` to handle one row at a time. + +`postgres(...)` turns cursors off in the driver it builds, so every query loads every row into memory first. `await` and `for await` read that same in-memory result, so `for await` does not lower the memory a large result uses. The connection goes back to the pool before you read the rows. + +Each result is read once, one way. Re-awaiting a result you already awaited is safe and gives you the same array. Switching between `await` and `for await`, or looping a second time with `for await`, throws an error whose `code` is `RUNTIME.ITERATOR_CONSUMED`. See [`AsyncIterableResult`](/orm/reference/orm-client#asynciterableresult). diff --git a/apps/docs/content/docs/orm/reference/transactions-and-runtime.mdx b/apps/docs/content/docs/orm/reference/transactions-and-runtime.mdx index 28052a8de3d..a77eb51705a 100644 --- a/apps/docs/content/docs/orm/reference/transactions-and-runtime.mdx +++ b/apps/docs/content/docs/orm/reference/transactions-and-runtime.mdx @@ -6,7 +6,7 @@ metaTitle: Prisma ORM transactions and runtime reference metaDescription: 'Reference for the Prisma ORM client lifecycle, transactions, prepared statements, and execution options.' --- -This page documents the client lifecycle (create, connect, close), transactions, prepared statements, and per-query execution options across PostgreSQL and MongoDB. Availability is stated per method: where the two databases differ, or where a method exists on only one of them, the Remarks say so. Transactions are fully supported on PostgreSQL and not shipped on MongoDB. +This page documents the client lifecycle (create, connect, close), transactions, prepared statements, and per-query execution options across PostgreSQL and MongoDB. Availability is stated per method: where the two databases differ, or where a method exists on only one of them, the Remarks say so. Transactions are fully supported on PostgreSQL. They are not available on MongoDB; see [Transactions (MongoDB)](#transactions-mongodb) for what to do instead. For a task-oriented walkthrough, see the Fundamentals guide to [Transactions](/orm/fundamentals/transactions). For the ways to query inside a transaction, see the [ORM client reference](/orm/reference/orm-client), the [SQL query builder reference](/orm/reference/sql-query-builder), and the [raw queries reference](/orm/reference/raw-queries). @@ -20,29 +20,40 @@ Create a PostgreSQL client. #### Remarks -- Pass the contract with `contractJson` (a JSON contract you import) or `contract` (a contract value). Supply exactly one. -- Bind to a database in one of three ways: a connection string via `url`, an existing `pg` `Pool` or `Client` instance via `pg`, or a `binding` object that names its kind (`{ kind: 'url' | 'pgPool' | 'pgClient', ... }`). Pass exactly one. When you pass a `pg` instance, you own its lifecycle; when you pass a `url`, the client creates and owns the pool. -- When you pass a `url`, the pool it creates uses `connectionTimeoutMillis: 20000` and `idleTimeoutMillis: 30000`. Override them with `poolOptions`. `poolOptions` has no effect on a pool you supply yourself. -- If your contract uses types from an extension pack, pass the pack in `extensions` (for example `extensions: [pgvector]`). +- Your contract is the `contract.prisma` file that replaced `schema.prisma`. Running `npx prisma contract emit` compiles it into two files: `contract.json`, read at run time, and `contract.d.ts`, read by TypeScript. A new project keeps all three in `src/prisma/`. [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7) lists the packages to install and the commands to run. +- Import `contract.json` and pass it as `contractJson`, as every example below does. Pass `contract` instead only when you already hold a contract object in your own code, because Prisma ORM converts it to the same JSON. Supply exactly one. +- A binding tells the client which database to use. Pass exactly one of `url`, `pg`, and `binding`, or leave all three out and pass one to [`connect()`](#connect). The `binding` form is the same three choices in one object: `{ kind: 'url', url }`, `{ kind: 'pgPool', pool }`, or `{ kind: 'pgClient', client }`. +- When you pass a `pg` pool or client, you own it and you close it. When you pass a `url`, the client creates the pool and closes it. The pool it creates uses `connectionTimeoutMillis: 20000` and `idleTimeoutMillis: 30000`. Override them with `poolOptions`. `poolOptions` has no effect on a pool you supply yourself. +- Some types come from a PostgreSQL extension, such as a `pgvector.Vector(1536)` type in your `contract.prisma`. Pass that extension's package in `extensions`: `import pgvector from '@prisma/orm-extension-pgvector/runtime'`, then `extensions: [pgvector]`. +- Prisma ORM keeps a record in your database of which contract the database was set up for. With `verifyMarker: 'onFirstUse'`, the default, the first query checks that record, logs a warning if it is missing or does not match, and runs the query anyway. `verifyMarker: false` skips the check. See [How migrations work](/orm/migrations/how-migrations-work). - `postgres(options)` does not open a connection. The client opens one on first use, so [`connect()`](#connect) is optional: call it to open the connection up front and fail early. #### Options +Pass exactly one of `url`, `pg`, and `binding`. + | Name | Type | Required | Description | |---|---|---|---| | `contractJson` / `contract` | JSON contract or contract value | Yes | The contract. Supply exactly one. | -| `url` | `string` | One binding | A PostgreSQL connection string. | -| `pg` | `Pool` or `Client` from the `pg` package | One binding | An existing `pg` instance to bind to. You own its lifecycle. | +| `url` | `string` | See note | A PostgreSQL connection string. | +| `pg` | `Pool` or `Client` from the `pg` package | See note | An existing `pg` instance to use. You close it yourself. | +| `binding` | `{ kind: 'url' \| 'pgPool' \| 'pgClient', ... }` | See note | The same three choices in one object. | | `poolOptions` | `{ connectionTimeoutMillis?: number; idleTimeoutMillis?: number }` | No | Timeouts for the pool the client creates from `url` (defaults `20000` / `30000`). | -| `extensions` | Array of extension packs | No | Runtime extension packs your contract's types need. | -| `middleware` | Array of middleware | No | Middleware applied to every execution. | -| `verifyMarker` | `'onFirstUse'` or `false` | No | Whether the runtime reads the contract marker row on the first query and warns when it does not match. Defaults to `'onFirstUse'`. | +| `extensions` | Array of extension packages | No | The packages for PostgreSQL extensions your contract's types need. | +| `middleware` | Array of middleware | No | Code that runs around every query. See [How middleware works](/orm/middleware/how-middleware-works). | +| `verifyMarker` | `'onFirstUse'` or `false` | No | Whether the first query checks that the database was set up for this contract. Defaults to `'onFirstUse'`. | #### Return type | Return type | Example | Description | |---|---|---| -| `PostgresClient` | `postgres({ contractJson, url })` | An unconnected client exposing `.orm`, `.sql`, `.raw`, `.enums`, `.nativeEnums`, `.contract`, `.context`, `connect()`, `runtime()`, `transaction()`, `prepare()`, and `close()`. | +| `PostgresClient` | `postgres({ contractJson, url })` | A client that has not connected yet. Its members are listed below. | + +- `.orm` holds your models by model name. See the [ORM client reference](/orm/reference/orm-client). +- `.sql` is the SQL query builder, keyed by table name. See the [SQL query builder reference](/orm/reference/sql-query-builder). `.raw` writes raw SQL as a template string. See the [raw queries reference](/orm/reference/raw-queries). +- `.enums` holds the enum values from your contract, by PostgreSQL schema: `db.enums.public.Role`. `.nativeEnums` holds the enum types defined in PostgreSQL itself. +- `.contract` is the contract Prisma ORM read from `contractJson`, as an object. `.context` is internal: the query builders read it, and you do not call it. +- [`connect()`](#connect), [`runtime()`](#runtime), [`transaction()`](#dbtransactioncallback), [`prepare()`](#dbpreparedeclaration-callback), and [`close()`](#close) are documented on this page. #### Examples @@ -75,10 +86,15 @@ Open a connection and return a runtime. #### Remarks -- Returns a `Promise`. The runtime is what runs built queries directly (`runtime.query(...)` for rows, `runtime.execute(...)` for writes that return none), opens connections (`runtime.connection()`), prepares statements (`runtime.prepare(...)`), and reports telemetry (`runtime.telemetry()`). -- `connect()` takes an optional binding (`{ url }`, `{ pg }`, or `{ binding }`), for when you did not give `postgres(...)` one. -- Calling `connect()` once the client has already connected rejects with an error whose `code` is `DRIVER.ALREADY_CONNECTED`. -- Calling `connect()` after the client has been closed rejects with an error whose `code` is `DRIVER.NOT_CONNECTED` (`Postgres client is closed`). +- Returns a `Promise`. The runtime is the connected object that runs queries. It has five calls: + - `runtime.query(built)` runs a built query and returns its rows. A built query is the object you get from `.build()` at the end of a builder or raw chain. See [`runtime.query()`](#runtimequery). + - `runtime.execute(built)` runs a built query that returns no rows, such as an insert, and returns `{ affectedRows }`. + - `runtime.connection()` takes one connection out of the pool so that several queries run on the same connection. See [Manual connection and transaction control](#manual-connection-and-transaction-control). + - `runtime.prepare(...)` prepares a statement you run many times with different values. See [Prepared statements (PostgreSQL)](#prepared-statements-postgresql). + - `runtime.telemetry()` reports how the most recent query went. See [`runtime.telemetry()`](#runtimetelemetry). +- `connect()` accepts a binding, so a client you created without one can be given its database here. +- Every error carries a `code`: to handle one, catch the error and compare `error.code` with the code's string. +- Calling `connect()` once the client has already connected rejects with an error whose `code` is `DRIVER.ALREADY_CONNECTED`. Calling `connect()` after the client has been closed rejects with an error whose `code` is `DRIVER.NOT_CONNECTED` (`Postgres client is closed`). #### Return type @@ -92,6 +108,7 @@ Open a connection and return a runtime. ```typescript const runtime = await db.connect(); +// `public` is the PostgreSQL schema; `tag` is the table behind your `Tag` model const tags = await runtime.query(db.sql.public.tag.select('id', 'label').build()); ``` @@ -101,7 +118,7 @@ Get the current runtime synchronously. #### Remarks -- On PostgreSQL, `runtime()` is **synchronous**: it returns the `Runtime` directly, not a promise. This differs from MongoDB, whose `runtime()` returns a `Promise` (see [MongoDB `runtime()`](#runtime-1)). +- On PostgreSQL, `runtime()` is **synchronous**: it returns the `Runtime` directly, not a promise. You can call it before connecting, because the client connects on first use. #### Return type @@ -109,23 +126,13 @@ Get the current runtime synchronously. |---|---|---| | `Runtime` | `const runtime = db.runtime()` | The runtime, returned synchronously. | -#### Examples - -##### Get the runtime without awaiting - -```typescript -const runtime = db.runtime(); -``` - ### `close()` Close the client and release its pool. #### Remarks -- Returns a `Promise`. -- After `close()`, `connect()` rejects and `runtime()` throws an error whose `code` is `DRIVER.NOT_CONNECTED` (`Postgres client is closed`). -- When you bound the client with `pg` (your own pool or client), close that instance yourself; the client only owns pools it created from a `url`. +- Returns a `Promise`. It waits for a connection already being opened, then releases the pool it created. After `close()`, `connect()` rejects and `runtime()` throws an error whose `code` is `DRIVER.NOT_CONNECTED` (`Postgres client is closed`). When you bound the client with `pg` (your own pool or client), close that instance yourself, because the client only owns pools it created from a `url`. #### Return type @@ -139,8 +146,7 @@ Close the client and release its pool. ```typescript const db = postgres({ contractJson, url: process.env.DATABASE_URL }); -await db.connect(); -// ... run queries ... +const tags = await db.runtime().query(db.sql.public.tag.select('id', 'label').build()); await db.close(); ``` @@ -150,9 +156,8 @@ The client implements `Symbol.asyncDispose`, so `await using` closes it automati #### Remarks -- Disposal fires at the end of the **block** the `await using` declaration lives in, not on the next line. Scope the client to the block where you need it. -- After the block exits, the client is closed the same way `close()` closes it: a later `connect()` rejects with an error whose `code` is `DRIVER.NOT_CONNECTED`. -- `await using` requires `@types/node` (which augments `SymbolConstructor` with `asyncDispose`) or a `lib` that declares the symbol. +- Disposal fires at the end of the **block** the `await using` declaration lives in, not on the next line. Scope the client to the block where you need it. After the block exits, the client is closed the same way `close()` closes it: a later `connect()` rejects with an error whose `code` is `DRIVER.NOT_CONNECTED`. +- `await using` needs `@types/node` installed, or `"lib": ["esnext.disposable"]` in your `tsconfig.json`. #### Examples @@ -161,8 +166,7 @@ The client implements `Symbol.asyncDispose`, so `await using` closes it automati ```typescript { await using db = postgres({ contractJson, url: process.env.DATABASE_URL }); - await db.connect(); - // ... run queries ... + const tags = await db.runtime().query(db.sql.public.tag.select('id', 'label').build()); } // db is closed here ``` @@ -176,27 +180,36 @@ Create a MongoDB client. #### Remarks -- Pass the contract with `contractJson` or `contract` (supply exactly one). -- Bind to a database in one of four ways: `url` (a `mongodb://` connection string, with an optional `dbName` to select or override the database, the form the examples on this page use), `uri` plus an explicit `dbName`, `mongoClient` (an existing `MongoClient` from the `mongodb` package) plus an explicit `dbName`, or a `binding` object that names its kind. Pass exactly one. -- **Client ownership**: with `url` or `uri`, Prisma ORM creates the underlying `MongoClient` and closes it on `close()`. With `mongoClient`, you supplied the client, so Prisma ORM does **not** close it: `close()` is a no-op on your client, and you close it yourself. This is what lets you share one `MongoClient` between Prisma ORM and driver-level code. -- `mongo(options)` does not open a connection. The runtime is built lazily on first use, or explicitly via [`connect()`](#connect-1). +- Pass the contract with `contractJson` or `contract` (supply exactly one), the same way as on PostgreSQL. +- A binding tells the client which database to use. Pass exactly one of `url`, `uri` plus `dbName`, `mongoClient` plus `dbName`, and `binding`, or leave all four out and pass one to [`connect()`](#connect-1). The `binding` form is the same choices in one object: `{ kind: 'url', url, dbName }` or `{ kind: 'mongoClient', client, dbName }`. +- With `url`, the database name comes from the path of the connection string, as in `mongodb://host:27017/app`. Add `dbName` to override that name, or when the string has no name in its path. +- With `uri`, Prisma ORM never reads the database name from the string, so `dbName` is required. That is the only difference between `uri` and `url`. +- **Client ownership**: with `url` or `uri`, Prisma ORM creates the underlying `MongoClient` and closes it on `close()`. With `mongoClient`, you supplied the client, so Prisma ORM does **not** close it: `close()` leaves your client open and you close it yourself. That is what lets you use one `MongoClient` both through Prisma ORM and in code you write against the `mongodb` package directly. +- `mongo(options)` does not open a connection. The runtime is built on first use, or explicitly via [`connect()`](#connect-1). #### Options +Pass exactly one of `url`, `uri` plus `dbName`, `mongoClient` plus `dbName`, and `binding`. + | Name | Type | Required | Description | |---|---|---|---| | `contractJson` / `contract` | JSON contract or contract value | Yes | The contract. Supply exactly one. | -| `url` | `string` | One binding | A `mongodb://` or `mongodb+srv://` string; combine with an optional `dbName` to select the database. | -| `uri` + `dbName` | `string` + `string` | One binding | A connection string plus an explicit database name. | -| `mongoClient` + `dbName` | `MongoClient` + `string` | One binding | An existing `MongoClient` you own, plus the database name. | -| `middleware` | Array of middleware | No | Middleware applied to every execution. | -| `mode` | `'strict'` or `'permissive'` | No | The mode middleware reads on every execution. Defaults to `'strict'`. | +| `url` | `string` | See note | A `mongodb://` or `mongodb+srv://` string, with the database name in its path. | +| `uri` + `dbName` | `string` + `string` | See note | The same kind of string, plus the database name, which is required here. | +| `mongoClient` + `dbName` | `MongoClient` + `string` | See note | An existing `MongoClient` you own, plus the database name. | +| `binding` | `{ kind: 'url' \| 'mongoClient', ... }` | See note | The same choices in one object. | +| `middleware` | Array of middleware | No | Code that runs around every query. See [How middleware works](/orm/middleware/how-middleware-works). | +| `mode` | `'strict'` or `'permissive'` | No | Passed through to your middleware, which decides what to do with it. Defaults to `'strict'`. | #### Return type | Return type | Example | Description | |---|---|---| -| `MongoClient` | `mongo({ contractJson, url, dbName })` | An unconnected client exposing `.orm`, `.query`, `.raw`, `.contract`, `.enums`, `.context`, `connect()`, `runtime()`, and `close()`. | +| `MongoClient` | `mongo({ contractJson, url, dbName })` | Prisma ORM's `MongoClient` type, not the driver's. A client that has not connected yet. | + +- `.orm` holds your models by model name, with no schema segment: `db.orm.users`. See the [ORM client reference](/orm/reference/orm-client). +- `.query` is the pipeline builder, which builds MongoDB aggregation pipelines. See the [pipeline builder reference](/orm/reference/pipeline-builder). `.raw` sends raw MongoDB commands. See the [raw queries reference](/orm/reference/raw-queries). +- `.enums` holds the enum values from your contract, `.contract` is the contract as an object, and `.context` is internal. [`connect()`](#connect-1), [`runtime()`](#runtime-1), and [`close()`](#close-1) are documented below. #### Examples @@ -229,9 +242,8 @@ Open a connection and return a runtime. #### Remarks -- Returns a `Promise`. -- Calling `connect()` when the client is already connected rejects with an error whose `code` is `DRIVER.ALREADY_CONNECTED`. The first connection can happen implicitly, the moment any query builds the runtime, so a later explicit `connect()` collides the same way. PostgreSQL raises the same code from the same situation. -- `connect()` accepts a binding, so you can defer the choice of database: `mongo({ contractJson })` then `await db.connect({ url, dbName })`. +- Returns a `Promise`. Calling `connect()` when the client is already connected rejects with an error whose `code` is `DRIVER.ALREADY_CONNECTED`. Running any query connects the client too, so if you then call `connect()`, it fails for the same reason. PostgreSQL raises the same code from the same situation. +- `connect()` accepts a binding, so a client you created without one can be given its database here. - After the client has been closed, `connect()` rejects with an error whose `code` is `DRIVER.NOT_CONNECTED` (`Mongo client is closed`). #### Return type @@ -255,8 +267,7 @@ Get the runtime. #### Remarks -- On MongoDB, `runtime()` returns a **`Promise`**: you must `await` it. The underlying driver connection is built lazily and asynchronously on first use. PostgreSQL's `runtime()` is synchronous and returns a `Runtime` directly, so do not treat the two as the same shape: `await db.runtime()` on MongoDB, `db.runtime()` on PostgreSQL. - +- On MongoDB, `runtime()` returns a **`Promise`**: you must `await` it. The connection to MongoDB is opened on first use, which is why this call is asynchronous. PostgreSQL's `runtime()` is synchronous, so write `await db.runtime()` on MongoDB and `db.runtime()` on PostgreSQL. - `MongoRuntime` has `query`, `execute`, and `close` only. It has no `connection()`, `prepare()`, or `telemetry()` (see [Transactions (MongoDB)](#transactions-mongodb) and [Execution options and results](#execution-options-and-results)). #### Return type @@ -265,14 +276,6 @@ Get the runtime. |---|---|---| | `Promise` | `const runtime = await db.runtime()` | The runtime, resolved asynchronously. | -#### Examples - -##### Await the runtime - -```typescript -const runtime = await db.runtime(); -``` - ### `runtime.query()` Run a built query through the runtime. @@ -280,8 +283,8 @@ Run a built query through the runtime. #### Remarks - `(await db.runtime()).query(built)` runs any built MongoDB query, including one built by the [pipeline builder](/orm/reference/pipeline-builder) (`db.query`). The client itself has no `execute()` method. -- Returns an [`AsyncIterableResult`](#asynciterableresult): `await` it for an array, or `for await` to stream rows. -- `runtime.execute(built)` is the sibling call for writes that return no rows; it resolves the statement's statistics. +- Returns an [`AsyncIterableResult`](#asynciterableresult), which holds the rows the query returns: `await` it for an array, or `for await` to read the rows one at a time as they arrive. +- `runtime.execute(built)` is the sibling call for writes that return no rows. It returns `{ affectedRows }`. #### Return type @@ -294,6 +297,7 @@ Run a built query through the runtime. ##### Run a query built by the pipeline builder [#run-a-pipeline-builder-plan] ```typescript +// 'posts' is the collection name, which is your model name with a lowercase first letter const built = db.query.from('posts').build(); const posts = await (await db.runtime()).query(built); ``` @@ -304,9 +308,7 @@ Close the client. #### Remarks -- Returns a `Promise`. -- After `close()`, any further use rejects with an error whose `code` is `DRIVER.NOT_CONNECTED` (`Mongo client is closed`). That covers `db.runtime()` and ORM access such as `db.orm.users.first()`, since both route through the same runtime. -- When you supplied a `mongoClient`, `close()` does not close your client (see [`mongo(options)`](#mongooptions)). +- Returns a `Promise`. After `close()`, any further use rejects with an error whose `code` is `DRIVER.NOT_CONNECTED` (`Mongo client is closed`). That covers `db.runtime()` and ORM access such as `db.orm.users.first()`, since both go through the same runtime. When you supplied a `mongoClient`, `close()` does not close your client (see [`mongo(options)`](#mongooptions)). #### Return type @@ -329,8 +331,7 @@ The MongoDB client implements `Symbol.asyncDispose` too, so `await using` closes #### Remarks -- Disposal fires at the end of the block, exactly as on PostgreSQL. -- After the block exits, a later `connect()` rejects with an error whose `code` is `DRIVER.NOT_CONNECTED`. +- Disposal fires at the end of the block, exactly as on PostgreSQL. After the block exits, a later `connect()` rejects with an error whose `code` is `DRIVER.NOT_CONNECTED`. #### Examples @@ -343,10 +344,9 @@ The MongoDB client implements `Symbol.asyncDispose` too, so `await using` closes // ... run queries ... } // db is closed here ``` - ## Transactions (PostgreSQL) -A transaction groups several writes into one unit: they all commit together, or they all roll back. Prisma ORM offers three levels of control, from the high-level `db.transaction(...)` call down to a manual connection you commit yourself. +A transaction groups several writes into one unit: they all commit together, or they all roll back. Use `db.transaction(...)` in application code. Use `withTransaction(...)` when you hold a runtime but no client. Open a connection and drive the transaction yourself when you need the same connection between statements. ### `db.transaction(callback)` @@ -354,12 +354,14 @@ Run a callback inside a transaction. The transaction commits when the callback r #### Remarks -- Inside the callback, query through the `tx` handle, not `db`: `tx.orm` for models, `tx.sql` with `tx.query(...)` (rows) or `tx.execute(...)` (writes that return none) for queries built with the SQL builder, and `tx.enums` and `tx.nativeEnums` for enum members. Every call on `tx` uses the same transaction connection; queries on `db` run outside the transaction. -- `db.transaction(...)` takes the callback and nothing else. There are no `isolationLevel`, `timeout`, or `maxWait` options, and `tx` has no `transaction()` method, so transactions do not nest. +- Query through `tx`, not `db`. `tx.orm` holds your models. Build queries with `tx.sql`, then run them with `tx.query(...)` to get rows back, or `tx.execute(...)` when the write returns no rows. `tx.enums` holds the members of the enums declared in your contract, and `tx.nativeEnums` holds the members of the PostgreSQL enum types in your database. Every call on `tx` uses the same transaction connection, and queries on `db` run outside the transaction. +- `db.transaction(...)` takes the callback and nothing else. There are no `isolationLevel`, `timeout`, or `maxWait` options. Nothing retries a failed transaction for you, so call `db.transaction(...)` again yourself when you want a retry. +- To bound the queries inside a transaction, pass a `signal` to each `tx.query(...)` and `tx.execute(...)` call. See [`RuntimeExecuteOptions`](#runtimeexecuteoptions). +- `tx` has no `transaction()` method, so transactions do not nest. When a function you call opens its own transaction today, pass `tx` down to that function instead. - `tx.sql` is a full SQL builder, keyed the same way as `db.sql`: use `tx.sql.public.
`, where `public` is the PostgreSQL schema. - Reads inside the transaction see the transaction's own uncommitted writes. - The callback's return value passes through as the result of `db.transaction(...)`. -- Do not let an [`AsyncIterableResult`](#asynciterableresult) escape the callback unread. A result you return and then read after the transaction has ended rejects with `RUNTIME.TRANSACTION_CLOSED` (see the Remark below). +- Do not let an [`AsyncIterableResult`](#asynciterableresult) escape the callback unread. A result you return and then read after the transaction has ended rejects with an error whose `code` is `RUNTIME.TRANSACTION_CLOSED` (see the Remark below). To act on a code, catch the error and compare `error.code` with the string. Every code is listed in the [error reference](/orm/reference/error-reference). #### Options @@ -450,12 +452,12 @@ The callback form does something the array form never could: one query's result ### `withTransaction(runtime, callback)` -A lower-level transaction helper you import directly, without going through the client. +A transaction helper you import directly. Use it when you hold a runtime but no client. #### Remarks - Imported from `@prisma/orm-postgres/family-runtime`. -- The callback receives a bare transaction context whose only methods are `query` and `execute`. Unlike `db.transaction(...)`'s `tx`, it has no `.orm` or `.sql`; build queries with the client's `db.sql` (or a standalone SQL builder) and run them through `tx.query(...)` for rows or `tx.execute(...)` for writes that return none. +- The callback receives a transaction handle whose only methods are `query` and `execute`. Unlike `db.transaction(...)`'s `tx`, it has no `.orm` or `.sql`. Build queries with the client's `db.sql` and run them through `tx.query(...)` for rows or `tx.execute(...)` for writes that return no rows. - Commits on return, rolls back on throw, the same as `db.transaction(...)`. #### Options @@ -478,7 +480,7 @@ A lower-level transaction helper you import directly, without going through the ```typescript import { withTransaction } from '@prisma/orm-postgres/family-runtime'; -const runtime = await db.connect(); +const runtime = await db.connect(); // opens the connection and returns the runtime await withTransaction(runtime, async (tx) => { await tx.execute(db.sql.public.tag.insert([{ id: crypto.randomUUID(), label: 'wt-commit-1' }]).build()); await tx.execute(db.sql.public.tag.insert([{ id: crypto.randomUUID(), label: 'wt-commit-2' }]).build()); @@ -502,29 +504,30 @@ try { ### Manual connection and transaction control -The lowest level: acquire a connection, open a transaction on it, commit or roll back yourself, and return the connection to the pool. +The lowest level: acquire a connection, open a transaction on it, commit or roll back yourself, and return the connection to the pool. Use it when you need the same connection between statements. #### Remarks -- `runtime.connection()` returns a dedicated connection. `connection.transaction()` opens a transaction on it. Run built queries with `transaction.query(...)` for rows or `transaction.execute(...)` for writes that return none, then call `transaction.commit()` or `transaction.rollback()`. +- `runtime.connection()` returns a dedicated connection. `connection.transaction()` opens a transaction on it. Run built queries with `transaction.query(...)` for rows or `transaction.execute(...)` for writes that return no rows, then call `transaction.commit()` or `transaction.rollback()`. - Always `release()` the connection when done, to return it to the pool. -- `connection.destroy(reason)` evicts that one connection from the pool instead of reusing it: Prisma ORM passes a truthy error to `pg`'s `PoolClient.release(err)`, which tells the pool to drop the client. It does not close the whole pool or the runtime; the runtime opens a replacement connection on the next query. Use `release()` for the normal path and `destroy()` only to discard a connection you no longer trust. `reason` is optional and advisory: the pool only reads whether it is set. +- `connection.destroy(reason)` throws that connection away instead of returning it to the pool. It does not close the pool or the runtime. The runtime opens a replacement connection on the next query. Use `release()` for the normal path and `destroy()` only to discard a connection you no longer trust. `reason` is optional and the pool never reads its value, so plain `destroy()` is enough. #### Options -| Name | Type | Required | Description | -|---|---|---|---| -| `runtime.connection()` | none | — | Returns a `Promise` of a dedicated connection. | -| `connection.transaction()` | none | — | Returns a `Promise` of a transaction on that connection. | -| `transaction.commit()` / `transaction.rollback()` | none | — | Commit or discard the transaction. | -| `connection.release()` | none | — | Return the connection to the pool. | -| `connection.destroy(reason)` | `Error`, optional | — | Evict this connection from the pool. | +| Method | Type | Description | +|---|---|---| +| `runtime.connection()` | none | Returns a `Promise` of a dedicated connection. | +| `connection.transaction()` | none | Returns a `Promise` of a transaction on that connection. | +| `transaction.commit()` / `transaction.rollback()` | none | Commit or discard the transaction. | +| `connection.release()` | none | Return the connection to the pool. | +| `connection.destroy(reason)` | `Error`, optional | Evict this connection from the pool. | #### Examples ##### Commit manually, then release ```typescript +const runtime = db.runtime(); const connection = await runtime.connection(); const transaction = await connection.transaction(); await transaction.execute( @@ -550,19 +553,17 @@ await connection.release(); ```typescript const connection = await runtime.connection(); -await connection.destroy(new Error('connection no longer trusted')); +await connection.destroy(); // the runtime stays healthy and opens a replacement connection on the next query ``` ## Transactions (MongoDB) :::note[MongoDB transactions are not available in Prisma ORM yet] -There is no `db.transaction(...)` on the MongoDB client, and the MongoDB runtime has no `connection()`, `prepare()`, or `telemetry()`: it has `query`, `execute`, and `close` and nothing else. Single-document write operations are atomic on their own; multi-document transactions through Prisma ORM are not shipped. - -To run a multi-document MongoDB transaction today, use the MongoDB driver directly, sharing one `MongoClient` between Prisma ORM and your driver code. See the [Fundamentals transactions guide](/orm/fundamentals/transactions#transactions-on-mongodb) for the driver-session pattern. +There is no `db.transaction(...)` on the MongoDB client, and the MongoDB runtime has no `connection()`, `prepare()`, or `telemetry()`: it has `query`, `execute`, and `close` and nothing else. Single-document write operations are atomic on their own. Multi-document transactions through Prisma ORM are not shipped. ::: -Transactions for MongoDB are planned, not shipped: a `db.transaction(fn)` method with `tx.orm` and `tx.query`, backed by a driver session. As designed, it will require a MongoDB replica set and must not silently degrade to non-transactional execution on a standalone server. Treat all of this as planned design, not current behavior. None of it exists in the shipped packages today. +Multi-document transactions for MongoDB are planned, but nothing on this page works on MongoDB today. To run one now, share one `MongoClient` between Prisma ORM and your driver code and use a driver session, as the [Fundamentals transactions guide](/orm/fundamentals/transactions#transactions-on-mongodb) shows. ## Prepared statements (PostgreSQL) @@ -574,8 +575,8 @@ Prepare a statement off a runtime. #### Remarks -- The declaration maps each parameter name to a type id (for example `{ label: 'pg/text@1' }`). -- The callback receives the declared `params` and returns a built query. It takes a **single** argument (`(params) => plan`); build the query with a SQL builder you already hold in scope (such as `db.sql`). +- The declaration maps each parameter name to a type id, for example `{ label: 'pg/text@1' }`. A type id is the `codecId` your contract gives that column, so read the ids out of your `contract.json`. See [The contract artifact](/orm/contract-authoring/the-contract-artifact). +- The callback takes one argument, the declared `params`, and returns the built query. Build it with a SQL builder you already hold, such as `db.sql`. - A declared parameter that the callback never uses is rejected at prepare time with an error whose `code` is `RUNTIME.PREPARE_UNUSED_PARAM`, with `details.unused` listing the offending names. The rejection happens at `prepare()`, before any execution. - The resulting `PreparedStatement` runs via `ps.query(target, params)`; see [`PreparedStatement.query`](#preparedstatementquerytarget-params). @@ -597,12 +598,9 @@ Prepare a statement off a runtime. ##### Prepare and run a statement ```typescript +const runtime = db.runtime(); const ps = await runtime.prepare({ label: 'pg/text@1' }, (params) => - db.sql.public.tag - .select('id', 'label') - .where((f, fns) => fns.eq(f.label, params.label)) - .limit(1) - .build(), + db.sql.public.tag.select('id', 'label').where((f, fns) => fns.eq(f.label, params.label)).limit(1).build(), ); const typescript = await ps.query(runtime, { label: 'typescript' }); @@ -614,11 +612,7 @@ const missing = await ps.query(runtime, { label: 'does-not-exist' }); ```typescript await runtime.prepare({ label: 'pg/text@1', unused: 'pg/int4@1' }, (params) => - db.sql.public.tag - .select('id', 'label') - .where((f, fns) => fns.eq(f.label, params.label)) - .limit(1) - .build(), + db.sql.public.tag.select('id', 'label').where((f, fns) => fns.eq(f.label, params.label)).limit(1).build(), ); // rejects with RUNTIME.PREPARE_UNUSED_PARAM, details: { unused: ['unused'] } ``` @@ -629,7 +623,7 @@ Prepare a statement on the client itself. #### Remarks -- Same result as `runtime.prepare(...)`, but the callback receives **two** arguments, `(sql, params)`, because the client has no closed-over SQL builder for you to reach for. Use the injected `sql` to build the query: `sql.public.
`. +- Same result as `runtime.prepare(...)`, but off the client the SQL builder is handed to you as the first argument. The callback takes **two** arguments, `(sql, params)`. Build the query with `sql.public.
`. - `prepare()` starts the connection itself, so calling `db.connect()` afterwards rejects with an error whose `code` is `DRIVER.ALREADY_CONNECTED`. Reach for the runtime with `db.runtime()` instead, which returns the one `prepare()` already built. #### Options @@ -651,11 +645,7 @@ Prepare a statement on the client itself. ```typescript const ps = await db.prepare({ label: 'pg/text@1' }, (sql, params) => - sql.public.tag - .select('id', 'label') - .where((f, fns) => fns.eq(f.label, params.label)) - .limit(1) - .build(), + sql.public.tag.select('id', 'label').where((f, fns) => fns.eq(f.label, params.label)).limit(1).build(), ); const rows = await ps.query(db.runtime(), { label: 'typescript' }); @@ -667,7 +657,16 @@ Run a prepared statement against a target, with the parameter values. #### Remarks -- A query that returns rows prepares into a `PreparedStatement`, whose consumption method is `query(...)`. A raw query ending with `.affectedCount()` prepares into a `PreparedExecution` instead, consumed with `execute(target, params)`, which resolves the statement's statistics. +- A query that returns rows prepares into a `PreparedStatement`, which you run with `query(...)`. A raw SQL statement ending in `.affectedCount()` prepares into a `PreparedExecution` instead, which you run with `execute(target, params)`. That call resolves to `{ affectedRows }`, the number of rows the statement changed: + + ```typescript + const archive = await runtime.prepare({}, () => + db.raw.sql`UPDATE "tag" SET label = ${'archived'} WHERE label = ${'stale'}`.affectedCount().build(), + ); + + const { affectedRows } = await archive.execute(runtime, {}); + ``` + - The `target` is explicit: it can be a runtime, a connection, or a transaction. One prepared statement runs against any of them. - A single `PreparedStatement` prepared once runs against both a runtime target and a transaction target, seeing the transaction's writes when run inside it and the committed state when run against the runtime afterward. @@ -682,7 +681,7 @@ Run a prepared statement against a target, with the parameter values. | Return type | Example | Description | |---|---|---| -| `AsyncIterableResult` | `await ps.query(runtime, { label })` | The statement's rows. | +| `AsyncIterableResult` | `await ps.query(runtime, { label })` | The statement's rows. `await` the result for an array. | #### Examples @@ -692,11 +691,7 @@ Run a prepared statement against a target, with the parameter values. import { withTransaction } from '@prisma/orm-postgres/family-runtime'; const ps = await runtime.prepare({ label: 'pg/text@1' }, (params) => - db.sql.public.tag - .select('id', 'label') - .where((f, fns) => fns.eq(f.label, params.label)) - .limit(1) - .build(), + db.sql.public.tag.select('id', 'label').where((f, fns) => fns.eq(f.label, params.label)).limit(1).build(), ); const insertedId = await withTransaction(runtime, async (tx) => { @@ -711,7 +706,7 @@ const committed = await ps.query(runtime, { label: 'ps-both-targets' }); // runs ## Execution options and results -Every runtime `query(...)` and `execute(...)` call accepts a `RuntimeExecuteOptions` object as its second argument. +`runtime.query(...)`, `runtime.execute(...)`, `tx.query(...)`, `tx.execute(...)`, `connection.query(...)`, and `connection.execute(...)` all take a `RuntimeExecuteOptions` object as their second argument. A prepared statement takes it as a third argument, after `target` and `params`. ### `RuntimeExecuteOptions` @@ -720,15 +715,15 @@ Per-query options for cancellation and scope. #### Remarks - `signal` is an `AbortSignal` for per-query cancellation. A signal that is **already aborted** when you call `query(...)` short-circuits before any query runs, rejecting with an error whose `code` is `RUNTIME.ABORTED` and whose `details.phase` is `'stream'`. The error's `cause` is the signal's reason, exactly as you passed it to `controller.abort(...)`. -- The runtime also checks the signal between rows, and passes it into the calls that read each value. An abort that lands after rows have started arriving ends the stream with the same `RUNTIME.ABORTED` error, with a `details.phase` of `'stream'`, `'encode'`, or `'decode'` depending on where it landed. -- `scope` is `'runtime'`, `'connection'`, or `'transaction'`. It is the label middleware reads to tell a top-level query from one inside a dedicated connection or a transaction. The runtime sets it for you; setting it yourself changes only that label, not where the query runs. +- An abort that lands after rows have started arriving ends the stream with the same `RUNTIME.ABORTED` error. `details.phase` says where it landed: `'stream'` while rows are being read, `'encode'` while a parameter is being encoded, and `'decode'` while a returned value is being decoded. +- `scope` is `'runtime'`, `'connection'`, or `'transaction'`. It is the label [middleware](/orm/middleware/how-middleware-works) sees for the query, and the runtime sets it for you, so do not set it yourself. #### Options | Name | Type | Required | Description | |---|---|---|---| | `signal` | `AbortSignal` | No | Per-query cancellation signal. | -| `scope` | `'runtime' \| 'connection' \| 'transaction'` | No | The scope label middleware reads for this query. | +| `scope` | `'runtime' \| 'connection' \| 'transaction'` | No | The label middleware sees for this query, which the runtime sets for you. | #### Examples @@ -752,7 +747,7 @@ Read telemetry about the most recent query. - PostgreSQL only. `telemetry()` does not exist on the MongoDB runtime. - `telemetry()` returns `null` on a freshly-connected runtime, before any query has run. -- After a query, it returns an object of the shape `{ lane, target: 'postgres', fingerprint, outcome, durationMs? }`, where `outcome` is `'success'` or `'runtime-error'`. It reflects only the **most recent** query, not a running history. +- After a query, it returns an object of the shape `{ lane, target: 'postgres', fingerprint, outcome, durationMs? }`. `lane` names which API built the query, such as `orm-client` for `db.orm` or `raw` for `db.raw.sql`. `fingerprint` is a hash of the query text with the literal values replaced, so every run of the same query shares one fingerprint. `outcome` is `'success'` or `'runtime-error'`. The object reflects only the **most recent** query, not a running history. #### Return type @@ -775,8 +770,8 @@ const after = runtime.telemetry(); ### `AsyncIterableResult` -The calls that run a read query (`all()`, `createAll()`, and the runtime's `query(...)`) return an `AsyncIterableResult`: `await` it to collect an array, or `for await` to stream rows one at a time. +`all()`, `createAll()`, and the runtime's `query(...)` return an `AsyncIterableResult`. `all()` and `query(...)` read rows, and `createAll()` writes rows and returns the ones it wrote. `await` the result to collect an array, or `for await` it to take rows one at a time. -On PostgreSQL, a query run through the client buffers its rows and hands its pooled connection back before you read them, so a result you hold or iterate slowly no longer keeps a connection checked out. A connection or transaction you opened yourself is unaffected: it stays yours until you release or commit it. +On PostgreSQL, a result you hold or read slowly does not keep a pooled connection checked out, because a query run through the client loads every row into memory first and hands its connection back before you read them. A connection or transaction you opened yourself is unaffected: it stays yours until you release or commit it. -A result is consumed once per mode. Re-`await`ing a buffered result is safe (it returns the cached array), but switching between awaiting and iterating a consumed result throws an error whose `code` is `RUNTIME.ITERATOR_CONSUMED`. For the full single-consumption rules, shared identically by PostgreSQL and MongoDB, see [`AsyncIterableResult`](/orm/reference/orm-client#asynciterableresult) in the ORM client reference. +Each result is read once, one way. Re-`await`ing a result you already awaited is safe and returns the same array. Switching between `await` and `for await`, or looping a second time with `for await`, throws an error whose `code` is `RUNTIME.ITERATOR_CONSUMED`. For the full single-consumption rules, shared identically by PostgreSQL and MongoDB, see [`AsyncIterableResult`](/orm/reference/orm-client#asynciterableresult) in the ORM client reference. diff --git a/apps/docs/cspell.json b/apps/docs/cspell.json index c80a3c186cf..e1eb0c6d4d7 100644 --- a/apps/docs/cspell.json +++ b/apps/docs/cspell.json @@ -470,7 +470,9 @@ "Initialising", "dbsql", "txexecute", - "txsql" + "txsql", + "ltrim", + "rtrim" ], "patterns": [ { diff --git a/docs/orm-docs-audit/c21-orm-reference/conventions.md b/docs/orm-docs-audit/c21-orm-reference/conventions.md index 3cc56bc1c2f..69d9ba86bb8 100644 --- a/docs/orm-docs-audit/c21-orm-reference/conventions.md +++ b/docs/orm-docs-audit/c21-orm-reference/conventions.md @@ -17,7 +17,7 @@ Verified facts (rc.9 source at wip/prisma-src/). Use these words on every page; - 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. +- Type names in raw SQL (`pg/int4@1`): "the PostgreSQL type plus a version, always `@1` today"; prefer taking the type from the table: `db.sql.public.user.columns.id`. There is no published list; the raw-queries page shows the ids a bare JavaScript value gets. - Errors: "an error with code `X`" (it is `error.code`). - Do not use: terminal, codec, envelope, facade, lane, junction model/table (say "join table" / "model for the join table"), surface, plan, "rides", "fan out", "drop one level", "field proxy" (say "the `p` argument"), "lambda" (say "callback"). - `npx prisma skills sync` exists in the published CLI (8.0.0-rc.13, newer than the rc.9 source tree): "in an existing project, run `npx prisma skills sync`". Do not drop it because the rc.9 tree lacks it. From 4f44e1fb389df3e45285f6446a600f94b38c60f1 Mon Sep 17 00:00:00 2001 From: willbot Date: Sun, 13 Sep 2026 09:04:10 +0200 Subject: [PATCH 06/14] docs(orm8): orm/reference pages, reader-review round two applied Flat line budget. Definitions moved to first use (contract, runtime, binding, scope, the .node rule as a table), the type-id convention stated once per page with the common ids, expr() shown with its import, the MongoDB client created in the pipeline and raw examples, contract defaults dropped from insert examples, withTransaction() described by its real use. Facts unchanged except where a reader's question had a verified answer (build once and run once for generated defaults; .returns() takes a string or { codecId, nullable }, not a column; both raw aggregate routes pass an ObjectId through). Pages total 3,434 lines. Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- .../docs/content/docs/orm/reference/index.mdx | 8 +- .../docs/orm/reference/pipeline-builder.mdx | 176 ++++++++-------- .../docs/orm/reference/raw-queries.mdx | 153 +++++++------- .../docs/orm/reference/sql-query-builder.mdx | 188 +++++++++--------- .../reference/transactions-and-runtime.mdx | 119 ++++++----- .../pipeline-builder.part1.round2.md | 101 ++++++++++ .../pipeline-builder.part2.round2.md | 119 +++++++++++ .../raw-queries-and-index.round2.md | 169 ++++++++++++++++ .../sql-query-builder.part1.round2.md | 97 +++++++++ .../sql-query-builder.part2.round2.md | 86 ++++++++ .../transactions-and-runtime.part1.round2.md | 89 +++++++++ .../transactions-and-runtime.part2.round2.md | 88 ++++++++ 12 files changed, 1070 insertions(+), 323 deletions(-) create mode 100644 docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part1.round2.md create mode 100644 docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part2.round2.md create mode 100644 docs/orm-docs-audit/c21-orm-reference/raw-queries-and-index.round2.md create mode 100644 docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part1.round2.md create mode 100644 docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part2.round2.md create mode 100644 docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part1.round2.md create mode 100644 docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part2.round2.md diff --git a/apps/docs/content/docs/orm/reference/index.mdx b/apps/docs/content/docs/orm/reference/index.mdx index d3876affa9f..6352925c38c 100644 --- a/apps/docs/content/docs/orm/reference/index.mdx +++ b/apps/docs/content/docs/orm/reference/index.mdx @@ -6,20 +6,20 @@ metaTitle: Prisma ORM API reference metaDescription: 'Reference index for the Prisma ORM client, SQL query builder, pipeline builder, raw queries, and runtime APIs.' --- -Prisma ORM gives you three ways to write a query. The **ORM client** is at `db.orm`. It has model methods such as `where()`, `create()`, and `include()`, and it works on both PostgreSQL and MongoDB. The **SQL query builder** is at `db.sql`. It has table methods that match SQL, such as `select()`, `innerJoin()`, and `groupBy()`, and today it works on PostgreSQL only. The **pipeline builder** is at `db.query`. It builds MongoDB aggregation pipelines. +Prisma ORM gives you three ways to write a query. In Prisma ORM 7 you wrote `findMany({ where, include })`. Now each of those arguments is a method you chain: `.where(...).include(...)`. See [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7), which maps each argument to its method. The **ORM client** is at `db.orm`. It has model methods such as `where()`, `create()`, and `include()`, and it works on both PostgreSQL and MongoDB. The **SQL query builder** is at `db.sql`. It has table methods that match SQL, such as `select()`, `innerJoin()`, and `groupBy()`, and it works on PostgreSQL only. The **pipeline builder** is at `db.query`. It builds MongoDB aggregation pipelines. It is not the same as `runtime.query()`, which runs a built query. -Use the ORM client for everyday reads and writes across your models, including loading related rows with `include()`. Use the SQL query builder when the result you want is not a model row, such as a projection across two tables or an aggregate over a join. Use the pipeline builder for MongoDB aggregation. When none of them can express the query, drop to a [raw query](/orm/reference/raw-queries). For connecting and disconnecting the client, transactions, and prepared statements, see [Transactions and runtime](/orm/reference/transactions-and-runtime). In Prisma ORM 7 you passed `findMany({ where, include })` arguments; those arguments are chained methods now, and [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7) maps each one. +Use the ORM client for everyday reads and writes across your models, including loading related rows with `include()`. Use the SQL query builder when the result you want is not a model row, such as a few columns drawn from two joined tables, or an aggregate over a join. Use the pipeline builder for MongoDB aggregation. When none of them can express the query, drop to a [raw query](/orm/reference/raw-queries). For connecting and disconnecting the client, transactions, and prepared statements, see [Transactions and runtime](/orm/reference/transactions-and-runtime). In the example below, the ORM client runs when you `await` it. The SQL query builder and the pipeline builder do not run on their own: each produces a built query with `.build()`, which you then run with `runtime.query(...)`. Watch the casing: `User` is the model name and `user` is the table name. ```ts +const runtime = db.runtime(); const admins = await db.orm.public.User.where((u) => u.kind.eq('admin')).all(); - const plan = db.sql.public.user.select('id', 'email').where((f, fns) => fns.eq(f.kind, 'admin')).build(); const rows = await runtime.query(plan); ``` ## How database differences are documented -Every method on a reference page has the same four parts: Remarks, Options, Return type, and Examples. The raw queries page uses prose instead. If a method works differently on PostgreSQL and MongoDB, or exists on only one of them, its Remarks say so. For a walkthrough of reading and writing data rather than a method list, see [Reading data](/orm/fundamentals/reading-data), [Writing data](/orm/fundamentals/writing-data), [Relations and joins](/orm/fundamentals/relations-and-joins), [Transactions](/orm/fundamentals/transactions), and [Advanced queries](/orm/fundamentals/advanced-queries). +Every method on a reference page has the same four parts: Remarks, Options, Return type, and Examples. The raw queries page is shorter and has fewer tables. If a method works differently on PostgreSQL and MongoDB, or exists on only one of them, its Remarks say so. For a walkthrough of reading and writing data rather than a method list, see [Reading data](/orm/fundamentals/reading-data), [Writing data](/orm/fundamentals/writing-data), [Relations and joins](/orm/fundamentals/relations-and-joins), [Transactions](/orm/fundamentals/transactions), and [Advanced queries](/orm/fundamentals/advanced-queries). }> diff --git a/apps/docs/content/docs/orm/reference/pipeline-builder.mdx b/apps/docs/content/docs/orm/reference/pipeline-builder.mdx index 7c596333902..692ad030825 100644 --- a/apps/docs/content/docs/orm/reference/pipeline-builder.mdx +++ b/apps/docs/content/docs/orm/reference/pipeline-builder.mdx @@ -8,9 +8,7 @@ metaDescription: 'Reference for the Prisma ORM MongoDB pipeline builder''s stage The pipeline builder gives you a typed way to build MongoDB aggregation pipelines. You reach it through `db.query`, chain aggregation stages onto a starting collection, and finish the chain with the call that builds the query. It is the MongoDB counterpart to the [SQL query builder](/orm/reference/sql-query-builder): a lower-level API for the queries the [ORM client](/orm/reference/orm-client) doesn't express. -Everything the builder produces compiles to a MongoDB aggregation pipeline. There is no `find` and no `distinct`. To fetch a single document, filter and then `limit(1)`. Use the ORM client (`db.orm`) for everyday reads and writes across models and relations, and the pipeline builder when you need a stage the ORM client doesn't expose: grouping, `$lookup` joins with post-processing, computed projections, multi-stage transformations, or aggregation-time writes with `$out` / `$merge`. - -This page documents every stage, accumulator, expression helper, and write method, with the caveats that apply to each. Where a method has not been verified against a running MongoDB, the Remarks say so. +Everything the builder produces compiles to a MongoDB aggregation pipeline. There is no `find` and no `distinct`. To fetch a single document, filter and then `limit(1)`. Use the ORM client (`db.orm`) for everyday reads and writes across models and relations, and the pipeline builder when you need a stage the ORM client doesn't expose: grouping, `$lookup` joins with post-processing, computed projections, multi-stage transformations, or stages that write the results into a collection (`$out` and `$merge`). This page documents every stage, accumulator, expression helper, and write method. :::note[This is the MongoDB pipeline builder] The pipeline builder targets MongoDB only. PostgreSQL has no pipeline builder: use the [SQL query builder](/orm/reference/sql-query-builder) for PostgreSQL joins and aggregates, and the [ORM client](/orm/reference/orm-client) for everyday queries on either database. @@ -18,7 +16,7 @@ The pipeline builder targets MongoDB only. PostgreSQL has no pipeline builder: u ## Example schema -All examples on this page run against the schema below. It is the same MongoDB schema as the [ORM client](/orm/reference/orm-client) page, so examples carry over between the two pages. The `posts` collection holds two kinds of document, `Article` and `Tutorial`, told apart by the value of the `kind` field. No example on this page uses them. +All examples on this page run against the schema below. It is the same MongoDB schema as the [ORM client](/orm/reference/orm-client) page, so examples carry over between the two pages. The `posts` collection holds two kinds of document, `Article` and `Tutorial`, told apart by the value of the `kind` field. No example on this page uses them. A type id such as `mongo/string@1` is the stored type plus a version, always `@1` today.
@@ -81,9 +79,9 @@ model Tutorial { ## Entry points -Every pipeline starts with `db.query.from(root)` and ends with a call that turns the chain into a query you can run. Between them you chain stages. +Every pipeline starts with `db.query.from('posts')`, naming the collection to aggregate over. It ends with a call that turns the chain into a query you can run. Between them you chain stages. -`db` is the client you create with `mongo(...)`. See [Transactions and runtime](/orm/reference/transactions-and-runtime#mongooptions) for how to create it. Your contract is the `contract.prisma` file that replaced `schema.prisma`. Running `npx prisma contract emit` compiles it into two generated files: `contract.json`, read at run time, and `contract.d.ts`, read by TypeScript. [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7) has the setup steps. +`db` is the client you create with `mongo(...)`. See [Transactions and runtime](/orm/reference/transactions-and-runtime#mongooptions) for how to create it. Prisma ORM 8 renames `schema.prisma` to `contract.prisma`. Run `npx prisma contract emit` to generate `contract.json`, used at run time, and `contract.d.ts`, used for types. [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7) has the setup steps. ### `from()` @@ -91,11 +89,10 @@ Enter the pipeline builder on a collection. #### Remarks -- MongoDB only. `db.query` is the pipeline-builder entry point on the client. - `from()` takes the collection name, the same name you use on `db.orm` (`'posts'`, `'users'`), not the model name `Post`. The collection name is the model's `@@map` value, or the model name with a lowercase first letter when there is no `@@map`. - Passing an unknown collection name throws right away, with an error whose `code` is `ORM.MODEL_UNKNOWN` and whose message is `Unknown root: "". Valid roots: ...`. -- `from()` returns a builder you chain stages onto. Some methods are only available before you add stages, `insertOne` and `insertMany` for example. If a method is missing from autocomplete, you have already chained past it. -- You can build the same pipelines without a client. `import { mongoQuery } from '@prisma/orm-mongo/query-builder'`, then `mongoQuery({ contractJson }).from('posts')`, passing the `contract.json` you import. +- `from()` returns a builder you chain stages onto. Call `insertOne()` and `insertMany()` on `from()` directly, before any stage. +- You can build the same pipelines without a client. `import { mongoQuery } from '@prisma/orm-mongo/query-builder'`, then `mongoQuery({ contractJson }).from('posts')`. Import `contractJson` the same way the example below does. #### Options @@ -114,27 +111,29 @@ Enter the pipeline builder on a collection. ##### Enter the builder on a collection ```typescript +import mongo from '@prisma/orm-mongo/runtime'; +import type { Contract } from './contract.d'; +import contractJson from './contract.json' with { type: 'json' }; + +const db = mongo({ contractJson, url: process.env.MONGODB_URL, dbName: 'app' }); const runtime = await db.runtime(); const plan = db.query.from('posts').build(); const posts = await runtime.query(plan); ``` -Every later example on this page assumes you already have `runtime` from `await db.runtime()`. - ### Building and executing a pipeline -Nothing runs until you execute the pipeline. `build()` turns the chain into a built query. `runtime.query(...)` runs it. +Nothing runs until you execute the pipeline. `build()` turns the chain into a built query, which the examples hold in a variable named `plan`. `runtime.query(...)` runs it. Every later example reuses the `db` and `runtime` from the example above. #### Remarks -- `build()` and `aggregate()` are two names for the same method. This page uses `build()`. +- Call `build()`. `aggregate()` is the same method under another name. - On MongoDB, `db.runtime()` returns a promise, so you must await it: `const runtime = await db.runtime()`. Hold it in a variable and reuse it. -- `runtime.query(built)` runs any built query, including one from the pipeline builder. - `await` that call for an array of documents, or `for await` over it to take one document at a time. - There is no `db.execute` on the MongoDB client. -- **Read results are decoded.** Prisma ORM converts each field from what the driver returns into the type your contract gives it: `_id` becomes a hex string, and a date becomes the date type in your contract. -- Only `match()`, `sort()`, `limit()`, `skip()`, `sample()`, `project()`, `addFields()`, and `vectorSearch()` keep that. After any other stage, `lookup()` and `group()` included, documents come back exactly as MongoDB returns them. Both a looked-up sub-document's `_id` and the top-level `_id` are then the driver's own `ObjectId` values. Compare two of those with `String(a._id) === String(b._id)`. +- **After `lookup()` or `group()`, compare ids with `String(a._id) === String(b._id)`.** Both a looked-up sub-document's `_id` and the top-level `_id` are the driver's own `ObjectId` values there, not hex strings. +- **Read results are decoded.** Prisma ORM converts each field from what the driver returns into the type your contract gives it: `_id` becomes a hex string, and a date becomes the date type in your contract. Decoding only survives these stages: `match()`, `sort()`, `limit()`, `skip()`, `sample()`, `project()`, `addFields()`, and `vectorSearch()`. After any other stage, including `lookup()` and `group()`, documents come back raw. #### Examples @@ -156,9 +155,9 @@ Filter documents by a predicate. #### Remarks - `match()` takes a callback whose `f` argument has one property per field, and returns a filter expression (`(f) => f.kind.eq('tutorial')`). -- The value you pass is compared exactly as you wrote it, with no conversion into the stored type. That is what you want for most scalar fields. It is why an `_id` filter never matches: your hex string is compared as a string against a stored `ObjectId`. See the warning below. +- Prisma ORM sends your value to MongoDB unchanged. It does not turn a string into an `ObjectId`, a date, or anything else. That is what you want for most scalar fields. It is why an `_id` filter never matches: your hex string is compared as a string against a stored `ObjectId`. See the warning below. - Leaf fields expose these filter operators: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`, `exists`, and `type`. They work like the MongoDB query operators of the same name (`$ne`, `$gt`, `$in`, `$exists`, `$type`, and so on). -- For an aggregation-expression predicate (comparing computed values rather than a field against a constant), wrap it in `expr(...)`: `match((f) => expr(fn.gt(fn.year(f.createdAt), fn.literal(2023))))`. See [Expression helpers](#expression-helpers). +- To compare computed values rather than a field against a constant, wrap the comparison in `expr(...)`: `match((f) => expr(fn.gt(fn.year(f.createdAt), fn.literal(2023))))`. `fn` holds the helpers that build expressions, and `expr()` wraps one so `match()` accepts it. The `fn.*` helpers take expressions, not plain values, so wrap every constant in `fn.literal()`. See [Expression helpers](#expression-helpers). #### Options @@ -197,9 +196,14 @@ const recent = await runtime.query(plan); ``` :::warning[`_id` equality filters do not work through the pipeline builder] -Filtering by `_id` equality inside `match()` **never matches any document**. Neither a hex string (`f._id.eq('507f...')`) nor the driver's own `ObjectId` matches. The hex-string form passes TypeScript, so taking an `_id` you read back and passing it to `match()` compiles cleanly and then returns zero documents. +Filtering by `_id` equality inside `match()` **never matches any document**. Neither a hex string (`f._id.eq('507f...')`) nor the driver's own `ObjectId` matches. The hex-string form passes TypeScript, so it compiles cleanly and then returns zero documents. There is no supported way to filter by `_id` equality through the typed pipeline builder. To fetch one document by its id, use the [ORM client](/orm/reference/orm-client): + +```typescript +const postId = '6650f1c2a1b2c3d4e5f60003'; +const post = await db.orm.posts.where({ _id: postId }).first(); +``` -There is currently no supported way to filter by `_id` equality through the typed pipeline builder. The escape hatch is [`rawCommand()`](#rawcommand), which places a real `ObjectId` directly in a raw pipeline document and matches correctly, or the [ORM client](/orm/reference/orm-client), which handles `_id` encoding for you (`db.orm.posts.where({ _id })`). +For an `_id` filter inside a pipeline, use [`rawCommand()`](#rawcommand), which places a real `ObjectId` directly in a raw pipeline document. ::: ### `sort()` @@ -352,8 +356,7 @@ Join documents from another collection (`$lookup`). - `lookup()` takes a callback that builds the join with `from(root).on((local, foreign) => ({ local, foreign })).as(name)`: the other collection, the two fields to match on, and the name of the output array field. - The object returned by `on()` has two fixed keys, `local` and `foreign`. Write `({ local: local.authorId, foreign: foreign._id })`. -- The joined documents arrive in an array under the `as` name. -- A joined sub-document's `_id` is the driver's own `ObjectId`, not a hex string. Compare two of them with `String(a._id) === String(b._id)`. +- The joined documents arrive in an array under the `as` name. Compare their ids with `String(a._id) === String(b._id)`. #### Options @@ -414,7 +417,6 @@ Reshape each document. ```typescript const plan = db.query.from('posts').project('title', 'kind').build(); const trimmed = await runtime.query(plan); -// each document has title, kind, and _id, but no content ``` ##### Callback form @@ -455,7 +457,7 @@ Unroll an array field into one document per element. ##### Unroll an array field -This example assumes a `tags String[]` field on `Post`, which the example schema above does not have. +This example needs a `tags String[]` field on `Post`, which the example schema does not have. ```typescript const plan = db.query @@ -475,7 +477,7 @@ Group documents by a key and compute per-group aggregates. #### Remarks - `group()` takes a callback that receives **only** the `f` argument, one property per field (`(f) => ...`), and returns a spec object. The spec's `_id` sets the grouping key; every other key must be an **accumulator**. -- Accumulators come from the imported `acc` namespace, **not** a second callback argument: `import { acc } from '@prisma/orm-mongo/query-builder'`, then `acc.count()`, `acc.push(f.title)`. The callback takes one argument; there is no `(f, acc) => ...` form. +- `acc` holds the accumulators `group()` accepts, such as a count or a sum. Import it and use it inside the callback, **not** as a second callback argument: `import { acc } from '@prisma/orm-mongo/query-builder'`, then `acc.count()`, `acc.push(f.title)`. The callback takes one argument; there is no `(f, acc) => ...` form. - A `_id: null` key groups the whole collection into a single bucket. - A non-accumulator value for a non-`_id` key throws as soon as you call `group()`, with an error whose `code` is `ORM.ARGUMENT_INVALID`: `group() field "" must use an accumulator (e.g. acc.sum(), acc.count()). Got "" expression.` A `null` value for a key other than `_id` throws the same way. @@ -570,7 +572,6 @@ const plan = db.query .replaceRoot((f) => fn.arrayElemAt(f.author, fn.literal(0))) .build(); const authors = await runtime.query(plan); -// each document is the promoted author sub-document ``` ### `count()` @@ -639,9 +640,8 @@ Keep or prune documents (and sub-documents) based on an expression. #### Remarks - `redact()` maps to MongoDB's `$redact`, whose expression must evaluate to one of the system variables `$$KEEP`, `$$DESCEND`, or `$$PRUNE`. -- There is no expression helper for a system variable, and `fn.literal(...)` cannot stand in. The server rejects both `fn.literal('KEEP')` and `fn.literal('$$KEEP')`. -- Write `f.rawPath('$KEEP')` instead. The builder adds one more `$`, so the server sees `$$KEEP`. -- Choose between the two with `fn.cond(...)`, as in the example below. +- `f.rawPath(path)` names a field or a MongoDB system variable by its raw string, with no checking against your model. For `$$KEEP` write `f.rawPath('$KEEP')`. There is no expression helper for a system variable, and `fn.literal(...)` cannot stand in. The server rejects both `fn.literal('KEEP')` and `fn.literal('$$KEEP')`. +- Every `fn.*` call returns a wrapper. `.node` is the raw expression inside it. `match()` takes the wrapper through `expr(...)`. `fn.cond()`, `redact()`, and the option-object stages take `.node`. - For anything more involved, use [`rawCommand()`](#rawcommand), where you write the `$redact` stage with `$$KEEP` and `$$PRUNE` directly. #### Options @@ -670,26 +670,27 @@ const plan = db.query ) .build(); const tutorials = await runtime.query(plan); +// fn.cond() takes its condition as a raw expression, so that argument ends in .node. The other two take wrappers, which f.rawPath() returns. ``` ### Option-object stages -Several stages take a single MongoDB stage options object rather than a typed `(f) => ...` callback. These map closely to the underlying MongoDB stage documents. +Several stages take a single MongoDB stage options object rather than a typed `(f) => ...` callback. Each one carries the same meaning it has in MongoDB. #### Remarks -- `bucket({ groupBy, boundaries, default_?, output? })` sorts documents into the ranges you name. -- `bucketAuto({ groupBy, buckets, output?, granularity? })` sorts them into a given number of equal-sized ranges. -- `geoNear({ near, distanceField, spherical?, maxDistance?, minDistance?, query?, key?, distanceMultiplier?, includeLocs? })`. -- `graphLookup({ from, startWith, connectFromField, connectToField, as, maxDepth?, depthField?, restrictSearchWithMatch? })`. -- `setWindowFields({ partitionBy?, sortBy?, output })`. -- `densify({ field, partitionByFields?, range })`. -- `fill({ partitionBy?, partitionByFields?, sortBy?, output })`. +- `bucket({ groupBy, boundaries, default_?, output? })` sorts documents into the ranges you name. `default` is a reserved word in JavaScript, so the key is `default_`. `groupBy` is a raw expression. +- `bucketAuto({ groupBy, buckets, output?, granularity? })` sorts them into a given number of ranges, chosen so each holds about the same number of documents. `groupBy` is a raw expression. +- `geoNear({ near, distanceField, spherical?, maxDistance?, minDistance?, query?, key?, distanceMultiplier?, includeLocs? })` sorts documents by their distance from a point and writes that distance into `distanceField`. `near` is a raw expression. +- `graphLookup({ from, startWith, connectFromField, connectToField, as, maxDepth?, depthField?, restrictSearchWithMatch? })` follows links from document to document through one collection and collects what it reaches into `as`. `startWith` is a raw expression. +- `setWindowFields({ partitionBy?, sortBy?, output })` computes a value for each document from the documents around it, such as a running total. `partitionBy` is a raw expression. +- `densify({ field, partitionByFields?, range })` adds documents to fill the gaps in a sequence of numbers or dates. +- `fill({ partitionBy?, partitionByFields?, sortBy?, output })` replaces missing or null values with a value you choose. `partitionBy` is a raw expression. - `facet(facets)` takes an object mapping each output field name to an array of stages. - `unionWith(collection, pipeline?)` takes a collection name and an optional array of stages. - `geoNear`, `densify`, and `fill` have not been verified against a running MongoDB. -- These options take raw expressions, not the typed `(f) => ...` callback the other stages use. Build a raw expression with the `fn.*` helpers and pass its `.node` property, or name a field with `MongoAggFieldRef.of('price')`. The fields that work this way are `bucket.groupBy`, `bucketAuto.groupBy`, `graphLookup.startWith`, `setWindowFields.partitionBy`, `fill.partitionBy`, and `geoNear.near`. -- The stages you pass to `facet` and `unionWith` are classes too: `MongoMatchStage`, `MongoCountStage`, `MongoSortStage`, `MongoLimitStage`, and the rest. Import them, and `MongoAggFieldRef`, from `@prisma/orm-mongo/query-ast/execution`. +- Build a raw expression with the `fn.*` helpers and pass its `.node` property, or name a field with `MongoAggFieldRef.of('price')`. +- The stages you pass to `facet` and `unionWith` are classes: `MongoMatchStage`, `MongoProjectStage`, `MongoSortStage`, `MongoLimitStage`, `MongoSkipStage`, `MongoLookupStage`, `MongoUnwindStage`, `MongoGroupStage`, `MongoAddFieldsStage`, `MongoReplaceRootStage`, `MongoCountStage`, `MongoSortByCountStage`, `MongoSampleStage`, `MongoRedactStage`, `MongoOutStage`, `MongoUnionWithStage`, `MongoBucketStage`, `MongoBucketAutoStage`, `MongoGeoNearStage`, `MongoFacetStage`, `MongoGraphLookupStage`, `MongoMergeStage`, `MongoSetWindowFieldsStage`, `MongoDensifyStage`, `MongoFillStage`, `MongoSearchStage`, `MongoSearchMetaStage`, and `MongoVectorSearchStage`. Import them, and `MongoAggFieldRef`, from `@prisma/orm-mongo/query-ast/execution`. #### Examples @@ -698,7 +699,6 @@ Several stages take a single MongoDB stage options object rather than a typed `( ```typescript const plan = db.query.from('posts').unionWith('users').build(); const combined = await runtime.query(plan); -// posts followed by users ``` ##### Run several sub-pipelines with `facet()` @@ -723,7 +723,7 @@ const [facets] = await runtime.query(plan); ##### Sort into ranges with `bucket()` -`duration` is a field on `Tutorial`, so posts without it fall into the `Other` bucket. +`MongoAggFieldRef.of()` takes any field path as a string and does not check it against your models. `duration` is a field on `Tutorial`, so posts without it fall into the `Other` bucket. ```typescript import { MongoAggFieldRef } from '@prisma/orm-mongo/query-ast/execution'; @@ -746,11 +746,11 @@ const buckets = await runtime.query(plan); #### Remarks -- These stages **require MongoDB Atlas**. The builder does not refuse them elsewhere: your query builds fine, and the MongoDB server rejects it when you run it. +- These stages need MongoDB Atlas. The query builds, and MongoDB rejects it when it runs. - `search(config, index?)` takes an Atlas Search operator object, for example `{ text: { query: 'hello', path: 'title' } }`, and an optional index name. - `searchMeta(config, index?)` takes the same two arguments and returns counts and facets instead of documents. - `vectorSearch({ index, path, queryVector, numCandidates, limit, filter? })` takes the index name, the field holding the vectors, the vector to search for as an array of numbers, how many candidates to consider, how many documents to return, and an optional filter object. -- None of the three has been verified against a running Atlas deployment. Test them against your own before relying on them. +- Test all three against your own Atlas deployment before relying on them. #### Return type @@ -760,7 +760,7 @@ const buckets = await runtime.query(plan); ## Accumulators -Accumulators compute per-group values inside a [`group()`](#group) stage. Import them from `@prisma/orm-mongo/query-builder`: +Accumulators compute per-group values inside a [`group()`](#group) stage. `_id: null` in the examples below means one group holding every document. Import them from `@prisma/orm-mongo/query-builder`: ```typescript import { acc } from '@prisma/orm-mongo/query-builder'; @@ -781,16 +781,16 @@ The builder exposes nineteen accumulators: | `acc.addToSet(expr)` | `addToSet(expression)` | Array of distinct values. | | `acc.firstN({ input, n })` | `firstN({ input, n })` | First `n` values. | | `acc.lastN({ input, n })` | `lastN({ input, n })` | Last `n` values. | -| `acc.maxN({ input, n })` | `maxN({ input, n })` | Top `n` values by value. | -| `acc.minN({ input, n })` | `minN({ input, n })` | Bottom `n` values by value. | -| `acc.top({ output, sortBy })` | `top({ output, sortBy })` | Single document by a sort order. | -| `acc.bottom({ output, sortBy })` | `bottom({ output, sortBy })` | Single document by the reverse sort order. | -| `acc.topN({ output, sortBy, n })` | `topN({ output, sortBy, n })` | Top `n` documents by a sort order. | -| `acc.bottomN({ output, sortBy, n })` | `bottomN({ output, sortBy, n })` | Bottom `n` documents by a sort order. | +| `acc.maxN({ input, n })` | `maxN({ input, n })` | The `n` largest values. | +| `acc.minN({ input, n })` | `minN({ input, n })` | The `n` smallest values. | +| `acc.top({ output, sortBy })` | `top({ output, sortBy })` | The `output` value from the document that sorts first under `sortBy`. | +| `acc.bottom({ output, sortBy })` | `bottom({ output, sortBy })` | The `output` value from the document that sorts last under `sortBy`. | +| `acc.topN({ output, sortBy, n })` | `topN({ output, sortBy, n })` | The `output` values from the first `n` documents under `sortBy`. | +| `acc.bottomN({ output, sortBy, n })` | `bottomN({ output, sortBy, n })` | The `output` values from the last `n` documents under `sortBy`. | | `acc.stdDevPop(expr)` | `stdDevPop(expression)` | Population standard deviation. | | `acc.stdDevSamp(expr)` | `stdDevSamp(expression)` | Sample standard deviation. | -`firstN` is shown below. `lastN`, `maxN`, and `minN` take the same arguments. `top`, `bottom`, `topN`, and `bottomN` take `output` and `sortBy` instead. `output` is the expression whose value you want back from each chosen document. `sortBy` is a plain object such as `{ createdAt: 1 }`, where `1` sorts ascending and `-1` sorts descending. Wherever a signature takes `n`, wrap the number: write `n: fn.literal(2)`, not `n: 2`. `acc.first()` and `acc.last()` only mean something when a `sort()` comes before the `group()`. +`firstN` is shown below. `lastN`, `maxN`, and `minN` take the same arguments. `top`, `bottom`, `topN`, and `bottomN` take `output` and `sortBy` instead. `output` is the expression whose value you want back from each chosen document. `sortBy` is a plain object such as `{ createdAt: 1 }`, where `1` sorts ascending and `-1` sorts descending. Wherever a signature takes `n`, wrap the number: write `n: fn.literal(2)`, not `n: 2`. `fn.literal` comes from the [expression helpers](#expression-helpers) below. Put a `sort()` before the `group()` when you use `acc.first()` or `acc.last()`. Without one, the document MongoDB calls first is arbitrary and can change between runs. #### Examples @@ -812,8 +812,6 @@ const [{ total, counted }] = await runtime.query(plan); ##### `avg()`, `min()`, and `max()` ```typescript -import { acc, fn } from '@prisma/orm-mongo/query-builder'; - const plan = db.query .from('posts') .group((f) => ({ _id: null, earliest: acc.min(f.createdAt), avgYear: acc.avg(fn.year(f.createdAt)) })) @@ -825,8 +823,6 @@ const [stats] = await runtime.query(plan); ##### `first()` and `last()` ```typescript -import { acc } from '@prisma/orm-mongo/query-builder'; - const plan = db.query .from('posts') .sort({ createdAt: 1 }) @@ -839,8 +835,6 @@ const [{ firstTitle, lastTitle }] = await runtime.query(plan); ##### `push()` and `addToSet()` ```typescript -import { acc } from '@prisma/orm-mongo/query-builder'; - const plan = db.query .from('posts') .group((f) => ({ _id: null, allTitles: acc.push(f.title), distinctKinds: acc.addToSet(f.kind) })) @@ -851,15 +845,13 @@ const [{ allTitles, distinctKinds }] = await runtime.query(plan); ##### `firstN()` (an `N`-variant) ```typescript -import { acc, fn } from '@prisma/orm-mongo/query-builder'; - const plan = db.query .from('posts') .sort({ createdAt: 1 }) .group((f) => ({ _id: null, firstTwo: acc.firstN({ input: f.title, n: fn.literal(2) }), - newestTitle: acc.top({ output: f.title, sortBy: { createdAt: -1 } }), // output is what you get back + newestTitle: acc.top({ output: f.title, sortBy: { createdAt: -1 } }), // newestTitle is the title string, not a document })) .build(); const [{ firstTwo, newestTitle }] = await runtime.query(plan); @@ -874,7 +866,7 @@ Expression helpers (`fn.*`) build the computed values used inside stages like `a import { fn } from '@prisma/orm-mongo/query-builder'; ``` -Each helper is the camelCase name of the MongoDB aggregation operator without its `$` prefix, so `$toUpper` is `fn.toUpper` and `$dateToString` is `fn.dateToString`. Five have a different name because the plain one was taken: `fn.firstElem` for `$first`, `fn.lastElem` for `$last`, `fn.isIn` for `$in`, `fn.typeOf` for `$type`, and `fn.toString_` for `$toString`. Here is every helper. Anything missing from this list has no helper, including `$and`, `$or`, `$not`, `$switch`, `$ifNull`, `$map`, `$reduce`, and `$filter`. +Each helper is the camelCase name of the MongoDB aggregation operator without its `$` prefix, so `$toUpper` is `fn.toUpper` and `$dateToString` is `fn.dateToString`. Five have a different name: `in` and `typeof` are reserved words in JavaScript, `toString` is already a method on every JavaScript object, and `$first` and `$last` get the suffix `Elem`. So write `fn.firstElem` for `$first`, `fn.lastElem` for `$last`, `fn.isIn` for `$in`, `fn.typeOf` for `$type`, and `fn.toString_` for `$toString`. Here is every helper. Anything missing from this list has no helper, including `$and`, `$or`, `$not`, `$switch`, `$ifNull`, `$map`, `$reduce`, and `$filter`. | Group | Helpers | |---|---| @@ -889,23 +881,35 @@ Each helper is the camelCase name of the MongoDB aggregation operator without it | Objects | `objectToArray`, `arrayToObject`, `getField`, `setField` | | Constants and branching | `literal`, `cond` | -`fn.literal(value)` wraps a constant so MongoDB reads it as a value and not as the name of a field. Every `fn.*` call returns a wrapper object. Its `.node` property is the raw expression inside that wrapper. `match()` takes the wrapper itself, through `expr(...)`. `fn.cond()` and the stages that take an options object take `.node`. +`fn.literal(value)` wraps a constant so MongoDB reads it as a value and not as the name of a field. What you write at each call site differs: + +| Call site | What you pass | +|---|---| +| `project()`, `addFields()`, and the arguments of an accumulator | The `fn.*` value itself. | +| The condition argument of `fn.cond()`, `f.stage.set()`, `f.stage.replaceRoot()`, `f.stage.replaceWith()`, and the `groupBy`, `startWith`, and `partitionBy` options of `bucket()`, `bucketAuto()`, `graphLookup()`, `setWindowFields()`, and `fill()` | The `.node` property of the `fn.*` value. | +| `match()` | The `fn.*` value wrapped in `expr(...)`. | + +`expr()` is what lets a `match()` filter compare computed values. Import it beside `fn`: + +```typescript +import { expr, fn } from '@prisma/orm-mongo/query-builder'; + +const plan = db.query.from('posts').match((f) => expr(fn.gt(fn.year(f.createdAt), fn.literal(2024)))).build(); +const rows = await runtime.query(plan); +``` #### Remarks -- Some helpers take one object of named arguments rather than positional ones: `fn.dateToString({ date, format })`, `fn.trim({ input })`, `fn.regexMatch({ input, regex })`, `fn.convert({ input, to })`, and the rest of the date, regex, replace, and object helpers. -- **Inside `fn.cond()`, pass `.node`.** Write `fn.cond(fn.eq(a, b).node, thenExpr, elseExpr)`. Passing the `expr(...)` form there throws a `TypeError`. -- The `expr()` wrapper is only for `match()` filters. Inside `match()`, `expr(fn.gt(...))` is correct. Inside `fn.cond()` it is not. -- **`fn.toObjectId()` cannot take a `fn.literal()` value.** `fn.toObjectId(fn.literal(id))` throws, because a literal carries no field type to convert from. Give `fn.toObjectId()` an expression taken from the `f` argument instead. -- To turn a string id from your own code into an `ObjectId`, build the `ObjectId` yourself and put it in a raw pipeline with [`rawCommand()`](#rawcommand). The last example on this page does exactly that. +- The condition argument of `fn.cond()` takes `.node`. The then and else arguments do not. Passing an `expr(...)` value as the condition throws a `TypeError`. +- Some helpers take one object of named arguments rather than positional ones: `fn.dateDiff({ startDate, endDate, unit, timezone?, startOfWeek? })`, `fn.dateTrunc({ date, unit, binSize?, timezone?, startOfWeek? })`, `fn.dateAdd({ startDate, unit, amount, timezone? })`, `fn.dateSubtract` with the same keys as `fn.dateAdd`, `fn.dateToString({ date, format })`, `fn.trim({ input })`, `fn.regexMatch({ input, regex })`, `fn.zip({ inputs, useLongestLength?, defaults? })`, `fn.getField({ field, input? })`, `fn.setField({ field, input, value })`, and `fn.convert({ input, to, onError?, onNull? })`. In `fn.convert`, `to` is the MongoDB type name you want, written as an expression, such as `fn.literal('int')` or `fn.literal('objectId')`. +- `fn.slice(array, ...rest)` and `fn.range(start, end, step)` take positional arguments. So does every other helper not named above, in the order MongoDB documents for that operator. Each argument is an expression from the `f` argument or a `fn.literal(...)` value. +- **`fn.toObjectId()` cannot take a `fn.literal()` value.** `fn.toObjectId(fn.literal(id))` throws. To turn a string id from your own code into an `ObjectId`, build the `ObjectId` yourself and put it in a raw pipeline with [`rawCommand()`](#rawcommand). The last example on this page does exactly that. #### Examples ##### Arithmetic in a projection ```typescript -import { fn } from '@prisma/orm-mongo/query-builder'; - const plan = db.query .from('posts') .project((f) => ({ title: 1, computed: fn.add(fn.literal(1), fn.multiply(fn.literal(2), fn.literal(3))) })) @@ -917,8 +921,6 @@ const rows = await runtime.query(plan); ##### String composition ```typescript -import { fn } from '@prisma/orm-mongo/query-builder'; - const plan = db.query .from('posts') .project((f) => ({ shout: fn.concat(fn.toUpper(f.title), fn.literal('!')) })) @@ -929,8 +931,6 @@ const rows = await runtime.query(plan); ##### Branch with `cond()` ```typescript -import { fn } from '@prisma/orm-mongo/query-builder'; - const plan = db.query .from('posts') .project((f) => ({ @@ -947,8 +947,9 @@ Append an arbitrary raw pipeline stage the typed methods don't cover. #### Remarks -- `pipe(stage)` appends a raw stage that you construct yourself, such as `new MongoMatchStage(...)` from `@prisma/orm-mongo/query-ast/execution`. It keeps the row type you already had. `pipe(stage)` lets you declare a different one. -- After a `pipe()` stage the rows come back exactly as MongoDB returns them, whatever the TypeScript type claims. Dates and ids are raw driver values. +- `pipe(stage)` appends a raw stage that you construct yourself, and keeps the row type you already had. `@prisma/orm-mongo/query-ast/execution` exports one class per MongoDB stage: `MongoMatchStage`, `MongoProjectStage`, `MongoSortStage`, `MongoLimitStage`, `MongoSkipStage`, `MongoLookupStage`, `MongoUnwindStage`, `MongoGroupStage`, `MongoAddFieldsStage`, `MongoReplaceRootStage`, `MongoCountStage`, `MongoSortByCountStage`, `MongoSampleStage`, `MongoRedactStage`, `MongoOutStage`, `MongoUnionWithStage`, `MongoBucketStage`, `MongoBucketAutoStage`, `MongoGeoNearStage`, `MongoFacetStage`, `MongoGraphLookupStage`, `MongoMergeStage`, `MongoSetWindowFieldsStage`, `MongoDensifyStage`, `MongoFillStage`, `MongoSearchStage`, `MongoSearchMetaStage`, and `MongoVectorSearchStage`. The same module exports `MongoFieldFilter`, which builds the filter a `MongoMatchStage` takes. +- `pipe(stage)` declares the row type the rows have after that stage. `NewShape` is a type you write yourself, as at the end of the example below. +- After a `pipe()` stage the rows come back exactly as MongoDB returns them, whatever the TypeScript type claims. A date is a `Date` and an id is an `ObjectId` from the `mongodb` package. - Prefer a typed stage where one exists. `pipe()` adds one raw stage to a typed chain, and [`rawCommand()`](#rawcommand) replaces the whole command. #### Examples @@ -956,13 +957,16 @@ Append an arbitrary raw pipeline stage the typed methods don't cover. ##### Append a raw stage ```typescript -import { MongoFieldFilter, MongoMatchStage } from '@prisma/orm-mongo/query-ast/execution'; +import { MongoCountStage, MongoFieldFilter, MongoMatchStage } from '@prisma/orm-mongo/query-ast/execution'; const plan = db.query .from('posts') .pipe(new MongoMatchStage(MongoFieldFilter.eq('kind', 'tutorial'))) .build(); const rows = await runtime.query(plan); + +type Counted = { total: number }; +const countPlan = db.query.from('posts').pipe(new MongoCountStage('total')).build(); ``` ## Read methods [#read-terminals] @@ -975,8 +979,7 @@ Compile the pipeline into a query you can run. #### Remarks -- `build()` produces the built query. `aggregate()` is an alias: it returns the identical query and runs identically. -- Neither runs the query. Pass the result to `runtime.query(...)` to fetch documents. +- `build()` produces the built query. `aggregate()` is an alias that returns the identical query. Write `build()`. Neither runs the query: pass the result to `runtime.query(...)` to fetch documents. #### Return type @@ -992,7 +995,7 @@ Updater callbacks (the `(f) => [...]` argument to the update methods) must retur - Always return an array. Returning a bare operation, `f.bio.set('x')` rather than `[f.bio.set('x')]`, throws as soon as you call the write method. - An empty array throws an error whose `code` is `ORM.MUTATION_DATA_MISSING`: `Updater returned no operations. Return at least one update from the callback ...`. -- An updater is either all operator form or all pipeline form. Operator form calls an operator on a field, as in `f.bio.set(value)`. Pipeline form calls `f.stage.*` and can read other fields of the same document. You cannot mix the two in one updater. TypeScript rejects the mixed array, and at run time it throws an error whose `code` is `ORM.ARGUMENT_INVALID`: `Cannot mix ...`. +- An updater is either all operator form or all pipeline form. Operator form calls an operator on a field, as in `f.bio.set(value)`. Pipeline form calls `f.stage.*`, where `stage` is a literal property name, and can read other fields of the same document. You cannot mix the two in one updater. TypeScript rejects the mixed array, and at run time it throws an error whose `code` is `ORM.ARGUMENT_INVALID`: `Cannot mix ...`. - Applying the same operator to the same field twice in one updater throws an error whose `code` is `ORM.ARGUMENT_INVALID`: `Update spec collision: ...`. ### Root-level writes @@ -1002,8 +1005,8 @@ Updater callbacks (the `(f) => [...]` argument to the update methods) must retur #### Remarks - These return a built query. Run it with `runtime.query(...)`. -- Write results are result objects, not documents: `insertOne` gives `{ insertedId }`, `insertMany` gives `{ insertedIds, insertedCount }`, the update methods and `upsertOne` give `{ matchedCount, modifiedCount, upsertedCount, upsertedId }`, and the delete methods give `{ deletedCount }`. -- The document you pass to `insertOne` / `insertMany` is a plain record. The builder does not check it against your contract, and each value is stored exactly as you pass it, with no conversion to the type your contract gives that field. [`match()`](#match) filter values follow the same rule. `insertMany([])` throws an error whose `code` is `ORM.MUTATION_DATA_MISSING`. +- Write results are result objects, not documents: `insertOne` gives `{ insertedId }`, `insertMany` gives `{ insertedIds, insertedCount }`, the update methods and `upsertOne` give `{ matchedCount, modifiedCount, upsertedCount, upsertedId }`, and the delete methods give `{ deletedCount }`. A write returns one result object, as the single row of the result, which is why every example here reads it with `const [result] = ...`. +- The document you pass to `insertOne` / `insertMany` is a plain record. TypeScript requires no particular field, and you can leave `_id` out and let MongoDB assign one. The builder does not check the record against your contract, and each value is stored exactly as you pass it, with no conversion to the type your contract gives that field. So pass a JavaScript `Date` for a date field and an `ObjectId` from the `mongodb` package for an id field, because nothing turns a string into either. [`match()`](#match) filter values follow the same rule. `insertMany([])` throws an error whose `code` is `ORM.MUTATION_DATA_MISSING`. #### Examples @@ -1041,8 +1044,6 @@ const [result] = await runtime.query(plan); ##### `upsertOne()` on the root (filter, then updater) -On the root collection, `upsertOne()` takes a filter callback and an updater callback: - ```typescript const plan = db.query .from('users') @@ -1060,7 +1061,7 @@ After a `match()` filter, `updateMany`, `updateOne`, `deleteMany`, `deleteOne`, - The `match()` filter supplies the write's filter, so you do not repeat it. - After `match()`, `upsertOne()` takes the updater callback only (the filter comes from `match()`). - `updateOne` / `deleteOne` affect at most one matching document. `updateMany` / `deleteMany` affect all matches. Which document `updateOne` / `deleteOne` picks is up to the driver. Use `findOneAndUpdate()` after a `sort()` when the choice matters. -- `findOneAndUpdate()` and `findOneAndDelete()` return the document exactly as the driver gives it, so its `_id` is a raw `ObjectId` even though the TypeScript type says otherwise. +- `findOneAndUpdate()` and `findOneAndDelete()` return the document exactly as the driver gives it, so its `_id` is an `ObjectId` from the `mongodb` package even though the TypeScript type says `string`. - You cannot call `findOneAndUpdate()` or `findOneAndDelete()` after `skip()`, and TypeScript does not offer them there. If a cast gets one past TypeScript, it throws an error whose `code` is `ORM.OPERATION_UNSUPPORTED`. #### Examples @@ -1138,7 +1139,7 @@ Inside an updater callback, each operation targets a field. There are two mutual #### Remarks -- **Operator form**: call an operator on the `f` argument, one operator per field. There are fourteen, listed under the bullets. +- **Operator form**: call an operator on the `f` argument, one operator per field. There are fourteen, in the table below. - **Pipeline form**: aggregation-pipeline update stages through `f.stage.*`, for example `f.stage.set({ bio: f.name.node })`. The four stages are `set`, `unset`, `replaceRoot`, and `replaceWith`. Pipeline-form operations let an update read other fields of the same document. - The two forms cannot be mixed in a single updater (see the write-methods intro). An updater is entirely operator form or entirely pipeline form. @@ -1193,8 +1194,8 @@ await runtime.query(plan); #### Remarks - `out(collection)` writes the pipeline output into a destination collection, replacing its contents. A second argument, a plain string, names a different database: `out('users_snapshot', 'archive')`. -- `merge({ into })` streams the pipeline output into a target collection, merging with existing documents. `into` is a collection name or `{ db, coll }`. The options object also takes `on`, the field or fields to match on, plus `whenMatched` and `whenNotMatched`. -- `whenMatched` takes `'replace'`, `'keepExisting'`, `'merge'`, or `'fail'`, or an array of update stages. `whenNotMatched` takes `'insert'`, `'discard'`, or `'fail'`. The builder passes both to MongoDB unchecked. +- `merge({ into })` writes the pipeline output into a target collection, merging with existing documents. `into` is a collection name or `{ db, coll }`. The options object also takes `on`, the field or fields to match on, plus `whenMatched` and `whenNotMatched`. +- `whenMatched` takes `'replace'`, `'keepExisting'`, `'merge'`, or `'fail'`, or an array of update stages, which are `MongoAddFieldsStage`, `MongoProjectStage`, and `MongoReplaceRootStage` objects from `@prisma/orm-mongo/query-ast/execution`. `whenNotMatched` takes `'insert'`, `'discard'`, or `'fail'`. A misspelled value fails at the database rather than in TypeScript. - Both end the chain: they return a built query you run with `runtime.query(...)`, and produce no rows of their own. #### Examples @@ -1241,7 +1242,6 @@ const rows = await runtime.query(plan); ##### Filter by `_id` (the way that works) ```typescript -import { RawAggregateCommand } from '@prisma/orm-mongo/query-ast/execution'; import { ObjectId } from 'mongodb'; const plan = db.query.rawCommand( diff --git a/apps/docs/content/docs/orm/reference/raw-queries.mdx b/apps/docs/content/docs/orm/reference/raw-queries.mdx index 38dedc44f0c..77552a05f4d 100644 --- a/apps/docs/content/docs/orm/reference/raw-queries.mdx +++ b/apps/docs/content/docs/orm/reference/raw-queries.mdx @@ -1,18 +1,14 @@ --- title: Raw queries reference -description: 'Reference for Prisma ORM raw query escape hatches: PostgreSQL raw SQL and MongoDB raw commands.' +description: 'Reference for Prisma ORM raw queries: PostgreSQL raw SQL and MongoDB raw commands.' url: /orm/reference/raw-queries metaTitle: Prisma ORM raw queries reference -metaDescription: 'Reference for Prisma ORM raw query escape hatches: PostgreSQL raw SQL and MongoDB raw commands.' +metaDescription: 'Reference for Prisma ORM raw queries: PostgreSQL raw SQL and MongoDB raw commands.' --- -Raw queries are the escape hatches for queries the typed APIs can't express. Reach for a typed API first: the [ORM client](/orm/reference/orm-client) for everyday reads and writes, the [SQL query builder](/orm/reference/sql-query-builder) for PostgreSQL joins and aggregates, and the [pipeline builder](/orm/reference/pipeline-builder) for MongoDB aggregation. When none of them reaches the SQL clause or MongoDB command you need, drop down to raw. +Raw queries are the way out when the typed APIs can't express the query you need. Reach for a typed API first: the [ORM client](/orm/reference/orm-client) for everyday reads and writes, the [SQL query builder](/orm/reference/sql-query-builder) for PostgreSQL joins and aggregates, and the [pipeline builder](/orm/reference/pipeline-builder) for MongoDB aggregation. When none of them reaches the SQL clause or MongoDB command you need, drop down to raw. -This page covers two escape hatches: **PostgreSQL raw SQL** (the `fns.raw` tagged template for fragments and the `db.raw.sql` tag for whole statements) and **MongoDB raw commands** (`db.raw.collection(...)` and `db.query.rawCommand(...)`). Every example is transcribed from an executable test suite that runs against a live database. - -:::warning[Fragments and raw commands return raw driver values] -A type id such as `pg/text@1` tells Prisma ORM how to turn the database's bytes into a JavaScript value. Only a whole `db.raw.sql` statement converts its results for you. `.returnsRow(spec)` names a type id for every column, so you get real JavaScript values back. Nothing else on this page is converted. A `fns.raw` fragment's `.returns(codecId)` only tells TypeScript what type to expect, so the value is whatever the driver returns. For a `text` or `integer` column that value is already a JavaScript string or number, which is why the `upperEmail` example below is a plain string. A `numeric` column arrives as a string, and a date arrives as the driver's own value. On MongoDB, raw results are native BSON. `_id` fields are driver `ObjectId` instances rather than hex strings, and write methods return the driver's own result objects (`{ insertedId }`, `{ matchedCount, modifiedCount }`, `{ deletedCount }`). Converting the values is up to you: compare an `ObjectId` with `String(...)`, and read those result keys directly. -::: +This page covers two of these ways out: **PostgreSQL raw SQL** (the `fns.raw` tagged template for fragments and the `db.raw.sql` tag for whole statements) and **MongoDB raw commands** (`db.raw.collection(...)` and `db.query.rawCommand(...)`). ## PostgreSQL raw SQL @@ -22,22 +18,24 @@ A **raw fragment** is a piece of SQL you put inside a builder query. Write it as A **whole raw statement** is a complete SQL statement written with `db.raw.sql`. If it returns rows, end it with `.returnsRow(spec).build()` and pass the result to `runtime.query(...)`. If it does not, end it with `.affectedCount().build()` and pass the result to `runtime.execute(...)`. -The examples in this section run against the `user` / `post` schema from the SQL query builder page. See its [example schema](/orm/reference/sql-query-builder#example-schema) for the full model definitions. As on that page, create the client with `postgres(...)`, connect it for a runtime, and reach tables through `db.sql.public`, which is keyed by table name (the model name with a lowercase first letter unless the model sets `@@map`). On PostgreSQL you get the runtime from `await db.connect()`. On MongoDB you get it from `await db.runtime()` instead. +:::warning[Only `.returnsRow()` converts values for you] +A type id (`codecId` in the code) such as `pg/text@1` says how to turn the database's bytes into a JavaScript value. `.returnsRow(spec)` names a type id for every column of a whole `db.raw.sql` statement, so you get real JavaScript values back. A fragment's `.returns(...)` does not convert anything. It only tells TypeScript what type to expect, so the value is whatever the driver returns. For a `text` or an `integer` column the driver already gives you a JavaScript string or number. A `numeric` column arrives as a string, and a date arrives as the driver's own value. +::: + +The examples in this section run against the `user` / `post` schema from the SQL query builder page. See its [example schema](/orm/reference/sql-query-builder#example-schema) for the full model definitions. As on that page, create the client with `postgres(...)` and reach tables through `db.sql.public`, which is keyed by table name (the model name with a lowercase first letter unless the model sets `@@map`). `db.runtime()` gives you the connection that runs a built query. On PostgreSQL it gives you that connection directly. On MongoDB it returns a promise, so there you write `const runtime = await db.runtime();`. ```ts import postgres from '@prisma/orm-postgres/runtime'; import type { Contract } from './contract.d'; import contractJson from './contract.json' with { type: 'json' }; - const db = postgres({ contractJson, url: process.env.DATABASE_URL }); -const runtime = await db.connect(); - +const runtime = db.runtime(); const aliceId = '00000000-0000-4000-8000-000000000001'; ``` ### `fns.raw` in a projection -A `select()` callback receives two arguments: `f`, with one property per column, and `fns`, with the expression helpers. `fns.raw` is a tagged template. Write a SQL fragment, interpolate columns and values with `${...}`, and declare the fragment's result type with `.returns(codecId)`. `select()` has three forms, and the two used on this page are the aliased form below and the object form further down. See [`select()`](/orm/reference/sql-query-builder#select) for all three. +A `select()` callback receives two arguments: `f`, with one property per column, and `fns`, with the expression helpers. `fns.raw` is a tagged template. Write a SQL fragment, interpolate columns and values with `${...}`, and declare the fragment's result type with `.returns(typeId)`. See [`select()`](/orm/reference/sql-query-builder#select) for every form it accepts. ```ts const plan = db.sql.public.user @@ -49,11 +47,11 @@ const rows = await runtime.query(plan); // rows === [{ id: aliceId, upperEmail: 'ALICE@EXAMPLE.COM' }] ``` -`.returns(codecId)` only tells TypeScript what to expect. It does not cast the value in SQL and it does not change how the driver reads it. For a type id that is not in the short list below, take the type from the column instead: `db.sql.public.user.columns.id`. See the SQL query builder's [`fns.raw` and `.returns()`](/orm/reference/sql-query-builder#fnsraw-and-returns) for the full treatment. +`.returns(...)` only tells TypeScript what to expect. It does not cast the value in SQL and it does not change how the driver reads it. It takes a type id string, or an object of the form `{ codecId, nullable }`. It does not take a column, so `.returns(db.sql.public.user.columns.id)` is a type error. Only `.returnsRow(spec)` takes columns. See the [type ids](#binding-a-bare-value-with-param) for the ones you are most likely to write, and the SQL query builder's [`fns.raw` and `.returns()`](/orm/reference/sql-query-builder#fnsraw-and-returns) for the full treatment. ### `fns.raw` as a `where()` predicate -`where()`'s callback returns a boolean expression, the same thing the comparison helpers such as `fns.eq(...)` return. A raw fragment declared `.returns('pg/bool@1')` is one, so it satisfies `where()` on its own, with no `fns.eq(...)` wrapper. +`where()`'s callback returns a boolean expression, the same thing the comparison helpers such as `fns.eq(...)` return. A raw fragment that declares `.returns('pg/bool@1')` is itself a boolean expression, so you can pass it straight to `where()` without wrapping it in `fns.eq(...)`. ```ts const plan = db.sql.public.user @@ -66,16 +64,12 @@ const rows = await runtime.query(plan); ### Interpolating a typed expression -You can interpolate another expression, such as a comparison, a column reference, or another raw fragment. Interpolated values are always sent as query parameters, never pasted into the SQL. Do not build a column or table name from user input with a template; `fns.raw` cannot parameterize an identifier. +You can interpolate another expression, such as a comparison, a column reference, or another raw fragment. Interpolated values are always sent as query parameters, never pasted into the SQL. There is no way to send a column name or a table name as a parameter, so never build one from user input with a template. When the column is chosen at run time, pick it in your own code from a fixed set of columns you control, and choose the matching `fns.raw` fragment or `select()` call for each one. ```ts const plan = db.sql.public.user .select('id', 'kind') - .select('kindLabel', (f, fns) => - fns.raw`CASE WHEN ${fns.eq(f.kind, 'admin')} THEN 'admin' ELSE 'regular user' END`.returns( - 'pg/text@1', - ), - ) + .select('kindLabel', (f, fns) => fns.raw`CASE WHEN ${fns.eq(f.kind, 'admin')} THEN 'admin' ELSE 'regular user' END`.returns('pg/text@1')) .build(); const rows = await runtime.query(plan); // each row's kindLabel is 'admin' or 'regular user' @@ -83,7 +77,21 @@ const rows = await runtime.query(plan); ### Binding a bare value with `param()` -A bare JavaScript value can be interpolated directly, and Prisma ORM picks a type id for it from the value's JavaScript type. A whole `number` within the 32-bit signed range, `-2147483648` to `2147483647`, becomes `pg/int4@1`. A larger whole number becomes `pg/int8number@1`, up to JavaScript's safe integer limit of `9007199254740991`. Every other number, including any fractional one, becomes `pg/float8@1`. A `bigint` becomes `pg/int8@1`, a `string` becomes `pg/text@1`, a `boolean` becomes `pg/bool@1`, and a `Uint8Array` becomes `pg/bytea@1`. Wrap the value in `param(value, { codecId })` when you want a different type id, or when the value's type is not in that list. Import `param` from `@prisma/orm-postgres/relational-core/expression`. +Interpolate a plain `number`, `string`, `boolean`, `bigint`, or `Uint8Array` directly. For anything else, such as a `Date` or a decimal you want stored as `numeric`, wrap the value in `param(value, { codecId })` and name the type id yourself. Import `param` from `@prisma/orm-postgres/relational-core/expression`. + +A bare value that you do interpolate directly gets the type id Prisma ORM picks from its JavaScript type: + +| The value you interpolate | The type id it gets | +| --- | --- | +| a whole `number` from `-2147483648` to `2147483647` | `pg/int4@1` | +| a larger whole `number`, up to `9007199254740991` | `pg/int8number@1` | +| any other `number`, including a fractional one | `pg/float8@1` | +| a `bigint` | `pg/int8@1` | +| a `string` | `pg/text@1` | +| a `boolean` | `pg/bool@1` | +| a `Uint8Array` | `pg/bytea@1` | + +A type id is the PostgreSQL type name plus a version, always `@1` today. Four more you are likely to write are `pg/uuid@1`, `pg/numeric@1`, `pg/jsonb@1`, and `pg/timestamptz-temporal@1`. Those are the common ones, not the whole set. ```ts import { param } from '@prisma/orm-postgres/relational-core/expression'; @@ -104,12 +112,7 @@ The object form of `.returns()` declares a nullable result. Use it when a fragme ```ts const plan = db.sql.public.user .select('id') - .select('adminName', (f, fns) => - fns.raw`CASE WHEN ${fns.eq(f.kind, 'admin')} THEN ${f.displayName} END`.returns({ - codecId: 'pg/text@1', - nullable: true, - }), - ) + .select('adminName', (f, fns) => fns.raw`CASE WHEN ${fns.eq(f.kind, 'admin')} THEN ${f.displayName} END`.returns({ codecId: 'pg/text@1', nullable: true })) .build(); const rows = await runtime.query(plan); // adminName is the displayName for admins, null for everyone else @@ -130,7 +133,7 @@ const plan = db.sql.public.user const rows = await runtime.query(plan); ``` -Use `.returnsRow(spec)` to run a whole statement and get converted rows back. The spec names the columns of every returned row. Each entry is either a column taken from your contract, such as `db.sql.public.user.columns.id`, which brings its own conversion, nullability, and TypeScript type, or an explicit type id for a column your contract has no counterpart for. Writing `FROM "user"` with no schema works because PostgreSQL resolves the name through its search path; `public."user"` is the explicit form. +Use `.returnsRow(spec)` to run a whole statement and get converted rows back. The spec names the columns of every returned row. Each entry is either a column taken from your contract, such as `db.sql.public.user.columns.id`, which brings its own conversion, nullability, and TypeScript type, or an explicit type id for a column your contract has no counterpart for. ```ts const user = db.sql.public.user; @@ -144,33 +147,27 @@ const plan = db.raw.sql` ORDER BY count(p.id) DESC, u.email ASC LIMIT ${10} ` - .returnsRow({ - id: user.columns.id, - email: user.columns.email, - postCount: 'pg/int8@1', - }) + .returnsRow({ id: user.columns.id, email: user.columns.email, postCount: 'pg/int8@1' }) .build(); const rows = await runtime.query(plan); // row.email is a string; row.postCount is a bigint ``` -Use `.affectedCount()` for a statement that returns no rows. It takes no spec. Pass the built query to `runtime.execute(...)`, which answers with the number of rows the statement changed. You can also run a built raw query inside a transaction: `tx.query(...)` and `tx.execute(...)` take the same built queries as the runtime does. +Use `.affectedCount()` for a statement that returns no rows. It takes no spec. Pass the built query to `runtime.execute(...)`, which resolves to an object, `{ affectedRows }`, whose `affectedRows` is the number of rows the statement changed. You can also run a built raw query inside a transaction: [`db.transaction()`](/orm/reference/transactions-and-runtime#dbtransactioncallback) gives you a `tx`, and `tx.query(...)` and `tx.execute(...)` take the same built queries as `runtime.query(...)` and `runtime.execute(...)` do. ```ts const plan = db.raw.sql` UPDATE "post" SET priority = ${'urgent'} WHERE "userId" IN (SELECT id FROM "user" WHERE kind = ${'admin'}) -` - .affectedCount() - .build(); +`.affectedCount().build(); const stats = await runtime.execute(plan); // stats.affectedRows ``` -A raw query that returns rows can go inside another one. Interpolate it into the outer template, and its SQL and its values are put into the outer statement in order. That is how you write a subquery or a CTE. The outer template declares its own columns. For a column the inner query already declared, read it back from the inner query's `.returns` rather than writing it out again. On a built raw query, `.returns` is a property: a record of the columns that query declared. That is a different thing from the `.returns(codecId)` method you call on a fragment. +A raw query that returns rows can go inside another one. That is how you write a subquery or a CTE. Interpolate the object that `.returnsRow(spec)` gives you, before you call `.build()` on it, and its SQL and its values are put into the outer statement in order. Only the outer statement is built. The outer statement declares its own columns. For a column the inner query already declared, read it back from the inner object's `.returns` property, which is a record of the columns that query declared, rather than writing the type id out again. That property is a different thing from the `.returns(typeId)` method you call on a fragment. ```ts const authorsWithPosts = db.raw.sql` @@ -178,10 +175,7 @@ const authorsWithPosts = db.raw.sql` FROM "post" p GROUP BY p."userId" HAVING count(*) >= ${3} -`.returnsRow({ - userId: post.columns.userId, - postCount: 'pg/int8@1', -}); +`.returnsRow({ userId: post.columns.userId, postCount: 'pg/int8@1' }); const plan = db.raw.sql` WITH active AS (${authorsWithPosts}) @@ -189,19 +183,14 @@ const plan = db.raw.sql` FROM active JOIN "user" u ON u.id = active."userId" ORDER BY active."postCount" DESC, u.email ASC -` - .returnsRow({ - email: user.columns.email, - postCount: authorsWithPosts.returns.postCount, - }) - .build(); +`.returnsRow({ email: user.columns.email, postCount: authorsWithPosts.returns.postCount }).build(); ``` You cannot interpolate a statement that ends with `.affectedCount()`, because it produces no rows. You can interpolate an `UPDATE ... RETURNING`, because it does. ### Unsupported interpolation -Interpolation accepts columns, expressions, `param(...)` values, raw queries that return rows, and the bare types `number`, `bigint`, `string`, `boolean`, and `Uint8Array`. Interpolating anything else, such as a `Date`, is rejected by TypeScript when you compile. In plain JavaScript there is nothing to reject it, so the tagged template throws as soon as you call it, with an error whose `code` is `RUNTIME.RAW_SQL_UNSUPPORTED_INTERPOLATION` and this message: +Interpolation accepts columns, expressions, `param(...)` values, raw queries that return rows, and the bare types `number`, `bigint`, `string`, `boolean`, and `Uint8Array`. Interpolating anything else, such as a `Date`, is rejected by TypeScript when you compile. In plain JavaScript the tagged template throws as soon as you call it, with an error whose `code` is `RUNTIME.RAW_SQL_UNSUPPORTED_INTERPOLATION` and this message: ``` unsupported JS value type for raw-SQL interpolation: wrap this value in `param(...)` with an explicit codec @@ -211,24 +200,34 @@ To bind a `Date`, or any value whose type is not in the list, wrap it in [`param ## MongoDB raw commands -`db.raw.collection(collectionName)` returns a raw collection with nine methods for running MongoDB commands directly against a collection. The collection name is the one your contract declares (`'users'`, `'posts'`), the same name the ORM client uses (`db.orm.users`). MongoDB has no schema segment, which is why there is no `public` here and none on `db.orm`. +`db.raw.collection(collectionName)` returns an object with nine methods for running MongoDB commands directly against a collection. The collection name is the one your contract declares (`'users'`, `'posts'`), the same name the ORM client uses (`db.orm.users`). MongoDB has no schema segment, which is why there is no `public` here and none on `db.orm`. + +Each method builds a command. Call `.build()` on it, then run it with `runtime.query(built)`. Every one of these methods goes through `query()`, including the writes. A write comes back as an array holding one result object, which is why the examples read it with `const [result] = ...`. -Each method builds a command. Call `.build()` on it, then run it with `(await db.runtime()).query(built)`. Every one of these methods goes through `query()`, including the writes, and a write yields a single result object as its one row. Raw filters hold native BSON values, so the examples below build ids with the driver's `ObjectId` class, imported with `import { ObjectId } from 'mongodb'`. +:::warning[MongoDB raw results are native BSON values] +Nothing on the MongoDB side of this page is converted for you. An `_id` comes back as the driver's own `ObjectId` instance, not a hex string, so compare one with `String(...)`. Raw filters take native BSON values too, so build ids with the driver's `ObjectId` class. The write methods give you the driver's own result objects (`{ insertedId }`, `{ matchedCount, modifiedCount }`, `{ deletedCount }`), and you read those keys directly. +::: -The examples in this section run against the `users` / `posts` schema from the pipeline builder page. See its [example schema](/orm/reference/pipeline-builder#example-schema) for the full model definitions. They use two ids declared once: +The examples in this section run against the `users` / `posts` schema from the pipeline builder page. See its [example schema](/orm/reference/pipeline-builder#example-schema) for the full model definitions. Create the client with `mongo(...)`, get the connection that runs a built query from `await db.runtime()`, and declare the two example ids once: ```ts +import mongo from '@prisma/orm-mongo/runtime'; +import { ObjectId } from 'mongodb'; +import type { Contract } from './contract.d'; +import contractJson from './contract.json' with { type: 'json' }; +const db = mongo({ contractJson, url: process.env.MONGODB_URL, dbName: 'app' }); +const runtime = await db.runtime(); const aliceId = '6539a1f2c3d4e5f60718293a'; const bobId = '6539a1f2c3d4e5f60718293b'; ``` ### `aggregate()` -Run a raw aggregation pipeline against a collection. The type parameter says what each returned row looks like. The pipeline stages are raw MongoDB documents. +Run a raw aggregation pipeline against a collection. The type parameter says what each returned row looks like. The pipeline stages are raw MongoDB documents, sent as you wrote them, so a filter on `_id` with a real `ObjectId` matches. ```ts const plan = db.raw.collection('posts').aggregate<{ _id: unknown; title: string }>([{ $match: { title: 'Hello world' } }]).build(); -const rows = await (await db.runtime()).query(plan); +const rows = await runtime.query(plan); // rows[0].title === 'Hello world'; every _id is a raw ObjectId, so use String(...) to compare ``` @@ -239,16 +238,11 @@ Insert one or many documents. Both return the driver's own insert result, with ` ```ts const users = db.raw.collection('users'); const onePlan = users.insertOne({ name: 'Dave', email: 'dave@example.com', role: 'author' }).build(); -const [oneResult] = await (await db.runtime()).query(onePlan); +const [oneResult] = await runtime.query(onePlan); // { insertedId: } -const manyPlan = users - .insertMany([ - { name: 'Eve', email: 'eve@example.com', role: 'author' }, - { name: 'Frank', email: 'frank@example.com', role: 'author' }, - ]) - .build(); -const [manyResult] = await (await db.runtime()).query(manyPlan); +const manyPlan = users.insertMany([{ name: 'Eve', email: 'eve@example.com', role: 'author' }, { name: 'Frank', email: 'frank@example.com', role: 'author' }]).build(); +const [manyResult] = await runtime.query(manyPlan); // { insertedCount: 2, insertedIds: [, ] } ``` @@ -260,7 +254,7 @@ Update one or every matching document. The first argument is a raw filter docume ```ts const plan = db.raw.collection('users').updateOne({ _id: new ObjectId(aliceId) }, { $set: { bio: 'Updated bio' } }).build(); -const [result] = await (await db.runtime()).query(plan); +const [result] = await runtime.query(plan); // { matchedCount: 1, modifiedCount: 1, ... } ``` @@ -270,7 +264,7 @@ An array update is a full aggregation-pipeline update, so a stage can reference ```ts const plan = db.raw.collection('users').updateMany({ role: 'author' }, [{ $set: { bio: { $concat: ['bio for ', '$name'] } } }]).build(); -const [result] = await (await db.runtime()).query(plan); +const [result] = await runtime.query(plan); // { matchedCount: 2, modifiedCount: 2, ... } ``` @@ -280,11 +274,11 @@ Delete one or every matching document. Both return `{ deletedCount }`. ```ts const onePlan = db.raw.collection('users').deleteOne({ _id: new ObjectId(bobId) }).build(); -const [oneResult] = await (await db.runtime()).query(onePlan); +const [oneResult] = await runtime.query(onePlan); // { deletedCount: 1 } const manyPlan = db.raw.collection('users').deleteMany({ role: 'author' }).build(); -const [manyResult] = await (await db.runtime()).query(manyPlan); +const [manyResult] = await runtime.query(manyPlan); // { deletedCount: 2 } ``` @@ -294,27 +288,32 @@ Atomically update a matching document and return the document as it was before t #### Remarks -- `findOneAndUpdate()` accepts only `{ upsert }` as its third argument. It cannot sort, and it cannot give you the document as it is after the update. TypeScript rejects an options object that names `sort` or `returnDocument`, and an untyped value that names either is dropped with no error at run time. -- Because `returnDocument` is never sent, the driver applies its own default, which is the document as it was before the update. The counter below shows what that costs you. +- `findOneAndUpdate()` accepts only `{ upsert }` as its third argument. It cannot sort, and it cannot give you the document as it is after the update. TypeScript rejects `sort` or `returnDocument` in the options object. If you get past the type check, say by casting the object to `any`, those keys are ignored at run time with no error. + +To sort, or to read the document after the update, build the command yourself and run it through `db.query.rawCommand(...)`. `new RawFindOneAndUpdateCommand(...)` takes six arguments: -To sort, or to read the document after the update, build the command yourself and run it through `db.query.rawCommand(...)`. The constructor takes the collection name, the filter, and the update, then `upsert`, `sort`, and `returnDocument`: +- `collection`: the collection name. Required. +- `filter`: the filter document. Required. +- `update`: an update document, or an array of aggregation-pipeline stages. Required. +- `upsert`: a boolean. Optional, and `false` when you leave it out. +- `sort`: a record of field name to `1` or `-1`. Optional. +- `returnDocument`: `'before'` or `'after'`. Optional. Leave it out and you get the document as it was before the update. ```ts import { RawFindOneAndUpdateCommand } from '@prisma/orm-mongo/query-ast/execution'; const command = new RawFindOneAndUpdateCommand('users', { _id: new ObjectId(aliceId) }, { $inc: { count: 1 } }, true, { count: -1 }, 'after'); -const [updated] = await (await db.runtime()).query(db.query.rawCommand(command)); +const [updated] = await runtime.query(db.query.rawCommand(command)); ``` ##### Upsert counter pattern -The **first** call to an upsert counter returns an **empty** result. The upsert creates a brand-new document, so there is no earlier version to return, the driver returns `null`, and the query yields no row. Read the counter value on the second call and later ones, each of which returns the version before its own increment. +The **first** call to an upsert counter returns an **empty** result. The upsert creates a brand-new document, so there is no earlier version to return, the driver returns `null`, and the query yields no row. Read the counter value on the second call and later ones, each of which returns the version before its own increment. The example schema has no counter collection, so `users` stands in for one here. Its `_id` is the plain string `'pageViews'`, which MongoDB accepts: an `_id` can be any BSON value, and a raw command sends the string through unchanged. ```ts const filter = { _id: 'pageViews' }; const update = { $inc: { count: 1 }, $setOnInsert: { _id: 'pageViews' } }; const counter = db.raw.collection('users'); -const runtime = await db.runtime(); const bump = () => runtime.query(counter.findOneAndUpdate(filter, update, { upsert: true }).build()); const first = await bump(); // [], because the insert has no earlier version const second = await bump(); // [{ ..., count: 1 }], the version before the second increment @@ -327,7 +326,7 @@ Atomically delete a matching document and return it. The yielded row is the raw ```ts const plan = db.raw.collection('users').findOneAndDelete({ _id: new ObjectId(aliceId) }).build(); -const [deleted] = await (await db.runtime()).query(plan); +const [deleted] = await runtime.query(plan); // deleted is the removed document; deleted._id is a raw ObjectId ``` @@ -339,17 +338,17 @@ const [deleted] = await (await db.runtime()).query(plan); import { RawAggregateCommand } from '@prisma/orm-mongo/query-ast/execution'; const plan = db.query.rawCommand(new RawAggregateCommand('posts', [{ $count: 'total' }])); -const rows = (await (await db.runtime()).query(plan)) as { total: number }[]; +const rows = (await runtime.query(plan)) as { total: number }[]; // [{ total: 2 }] ``` -`rawCommand()` is the escape hatch for anything the typed pipeline builder can't express. Two of those are `_id` equality filters and `$redact` with `$$KEEP` / `$$PRUNE`. Filtering by `_id` works here because a real `ObjectId` in a raw pipeline document matches: +`rawCommand()` is the way out when the typed pipeline builder can't express a query. Two examples: filtering by `_id` equality, and `$redact` with `$$KEEP` / `$$PRUNE`. Both work here because the command you pass is sent unchanged, so a real `ObjectId` in a pipeline document matches: ```ts const plan = db.query.rawCommand( new RawAggregateCommand('posts', [{ $match: { _id: new ObjectId('6539a1f2c3d4e5f60718293c') } }]), ); -const rows = await (await db.runtime()).query(plan); +const rows = await runtime.query(plan); ``` For the full treatment see the pipeline builder's [`rawCommand()`](/orm/reference/pipeline-builder#rawcommand) section. 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 0f534727519..018fa4e0ad7 100644 --- a/apps/docs/content/docs/orm/reference/sql-query-builder.mdx +++ b/apps/docs/content/docs/orm/reference/sql-query-builder.mdx @@ -6,7 +6,7 @@ metaTitle: Prisma ORM SQL query builder reference metaDescription: 'Reference for the Prisma ORM SQL query builder''s select, mutation, and grouped query methods.' --- -The SQL query builder gives you table-level, SQL-shaped methods for building typed queries: `select()`, `innerJoin()`, `groupBy()`, and the rest. It is closer to SQL than the [ORM client](/orm/reference/orm-client) is, and the same `db` gives you both. Reach for it when you need a join, aggregate, or SQL feature the ORM client doesn't expose, or when you want direct control over the generated SQL. +The SQL query builder builds typed queries from a table, with one method per SQL clause: `select()`, `innerJoin()`, `groupBy()`, and the rest. It is closer to SQL than the [ORM client](/orm/reference/orm-client) is, and the same `db` gives you both. Reach for it when you need a join, aggregate, or SQL feature the ORM client doesn't expose, or when you want direct control over the generated SQL. This page documents every method, its options, and its return type. Some methods work only on some databases, and some have a caveat that shows up only when the query runs. Each method's **Remarks** section says so. For a task-oriented guide to when and how to reach for the builder, see [Advanced queries](/orm/fundamentals/advanced-queries). @@ -16,7 +16,7 @@ The SQL query builder targets SQL databases; PostgreSQL and SQLite are supported ## Example schema -All examples on this page run against the following schema. The select and mutation examples use the `user`, `post`, `post_tag`, and `tag` tables. The grouped-query examples use a separate `customer` and `order` schema, shown under [Grouped queries](#grouped-queries). No example uses `Address`, `Task`, `Bug`, or `Feature`. They are here only so that this schema matches the one on the [ORM client reference](/orm/reference/orm-client), and you can ignore them. +All examples on this page run against the following schema. The select and mutation examples use the `user`, `post`, `post_tag`, and `tag` tables. The grouped-query examples use a separate `customer` and `order` schema, shown under [Grouped queries](#grouped-queries). No example uses `Address`, `Task`, `Bug`, or `Feature`. They are here only so that this schema matches the one on the [ORM client reference](/orm/reference/orm-client), and you can ignore them. The `types` block, `@@type`, `@@discriminator`, and `@@base` are contract features documented under [Contract authoring](/orm/contract-authoring/psl-syntax), and no example on this page uses them.
@@ -128,11 +128,17 @@ model Feature { ## Entry points -You build queries from the client's `sql` property. Create a Postgres client with `postgres(...)`, then reach tables through `db.sql.public`. The client reads your contract, the `contract.prisma` file that replaced `schema.prisma`. Running `npx prisma contract emit` compiles the contract into the two files every example here imports: `contract.json`, read when your app runs, and `contract.d.ts`, read by TypeScript. [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7) has the packages to install and the commands to run. Table accessors are keyed by table name, not model name. A table name is the model name with a lowercase first letter unless the model sets `@@map`, so the `User` model mapped to `user` is reached as `db.sql.public.user`, and the model for the `Post`/`Tag` join table, mapped to `post_tag`, is reached as `db.sql.public.post_tag`. Column names are the field names unless a field sets `@map`. `public` is the PostgreSQL schema, the namespace your tables live in unless you set another one. +Before you can build queries, run `npx prisma contract emit`. It reads your contract, the `contract.prisma` file that replaced `schema.prisma`, and writes `contract.json` and `contract.d.ts` next to it. The examples below import both. + +Install `@prisma/orm-postgres`. [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7) has the rest of the setup steps. + +You build queries from the client's `sql` property. Create a Postgres client with `postgres(...)`, then reach tables through `db.sql.public`. `public` is the PostgreSQL schema your tables are in. + +Table accessors are keyed by table name, not model name. A table name is the model name with a lowercase first letter unless the model sets `@@map`, so the `User` model mapped to `user` is reached as `db.sql.public.user`, and the model for the `Post`/`Tag` join table, mapped to `post_tag`, is reached as `db.sql.public.post_tag`. Column names are the field names unless a field sets `@map`. ### The `db.sql` builder [#the-dbsql-facet] -Create the client with `postgres(...)` and build queries off `db.sql.public`. `db.runtime()` is the connection that runs a built query. On PostgreSQL it is synchronous, so do not await it. A `select()`, `insert()`, `update()`, or `delete()` call starts a query. Finish it with [`build()`](#build), which returns the built query, and run that query with `runtime.query(...)`. +`db.runtime()` returns the connection that runs a built query. Do not `await` it. Only `runtime.query()` is awaited. A `select()`, `insert()`, `update()`, or `delete()` call starts a query. Finish it with [`build()`](#build), which returns the built query, and pass that to `runtime.query(...)`. The examples name the built query `plan`. You can run the same built query more than once. ```ts import postgres from '@prisma/orm-postgres/runtime'; @@ -146,15 +152,9 @@ const plan = db.sql.public.user.select('id', 'email').build(); const users = await runtime.query(plan); ``` -`db.sql` is ready to use as soon as you have a client, so most code needs nothing more. The `sql(...)` function from `@prisma/orm-postgres/builder/runtime` builds the same query builder around your own database client instead, from that client's `db.context`: +`db.sql` is ready to use as soon as you have a client, so most code needs nothing more. If you wrap your own database client, the `sql(...)` function from `@prisma/orm-postgres/builder/runtime` builds the same query builder around that client. -```ts -import { sql } from '@prisma/orm-postgres/builder/runtime'; - -const publicSql = sql({ context: db.context, rawCodecInferer: { inferCodec: () => 'pg/text@1' } }).public; -``` - -SQLite works the same way. Create the client with `sqlite(...)` from `@prisma/orm-sqlite/runtime`. SQLite has no schemas, so there is no `public` in the path and the same table is `db.sql.user`. The rest of this page shows PostgreSQL and writes the root as `db.sql.public`. +SQLite works the same way. Create the client with `sqlite(...)` from `@prisma/orm-sqlite/runtime`. SQLite has no schemas, so there is no `public` in the path and the same table is `db.sql.user`. Of the SELECT methods on this page, `lateralJoin()`, `outerLateralJoin()`, and `distinctOn()` are not available on SQLite. Every other one is. The rest of this page shows PostgreSQL and writes the root as `db.sql.public`. ### Table access @@ -162,7 +162,7 @@ Every table on `db.sql.public` exposes the query-building methods. ```ts db.sql.public.user.select('id', 'email'); // start a SELECT -db.sql.public.tag.insert([{ id, label: 'typescript' }]); // start an INSERT +db.sql.public.tag.insert([{ label: 'typescript' }]); // start an INSERT ``` ### Aliasing a query with `.as()` @@ -182,15 +182,15 @@ Use `.as()` only for subqueries you pass to `innerJoin()`, `outerLeftJoin()`, `o ## SELECT queries -These methods build and refine a `SELECT`. They chain, and you resolve the query with [`build()`](#build) followed by `runtime.query(...)`. +These methods build and refine a `SELECT`. They chain, and you resolve the query with [`build()`](#build) followed by `runtime.query(...)`. `SelectQuery` in the tables below is the type they return. You never import it. -The `where()`, `select()`, `orderBy()`, `update()`, and other callback forms receive two arguments. The first is `f`, which has one property per column the query can name at that point. After a join, columns are grouped under their table name, such as `f.post.id`. The second is `fns`, an object of [expression helpers](#expressions-and-functions) such as `fns.eq()` and `fns.raw`. +The set of column names a query can use is its scope. It starts as the columns of the table you began with, and every join adds the joined table's columns to it. -The set of names the query can use is its scope. It starts as the columns of the table you began with, and every join adds the joined table's columns to it. +The `where()`, `select()`, `orderBy()`, `update()`, and other callback forms receive two arguments. The first is `f`, which has one property per column in scope. After a join, a column name that both tables have is on `f` only under its table name, such as `f.post.id`. The second is `fns`, an object of [expression helpers](#expressions-and-functions) such as `fns.eq()` and `fns.raw`. ### `select()` -Project a row down to a chosen set of columns or computed expressions. +Reduce each row to a chosen set of columns or computed expressions. #### Options @@ -202,9 +202,9 @@ Project a row down to a chosen set of columns or computed expressions. | Aliased expression | `select(alias, (f, fns) => expr)` | Add one computed column under `alias`. | | Object of expressions | `select((f, fns) => ({ alias: expr, ... }))` | Add several computed columns at once. The row type is inferred from the object's shape. | -Every form adds to what the query already returns. Calling `select()` a second time never replaces the first call's columns, whichever form you use. +Every form adds to what the query already returns. Calling `select()` a second time never replaces the first call's columns, whichever form you use. To return fewer columns, start a new query instead. -A computed expression's result type comes from `.returns(...)` on `fns.raw`, or from the operation's own declared return type. A type id such as `pg/int4@1` is the PostgreSQL type plus a version, always `@1` today. [Raw queries](/orm/reference/raw-queries) shows the ones a bare JavaScript value gets, and you can take one from a column: `db.sql.public.user.columns.id`. See [`fns.raw` and `.returns()`](#fnsraw-and-returns). +A computed expression's result type comes from `.returns(...)` on `fns.raw`, or from the operation's own declared return type. `.returns()` takes a type id, such as `'pg/int4@1'`. A type id is the PostgreSQL type name plus `@1`. The common ones are `pg/text@1`, `pg/int4@1`, `pg/int8@1`, `pg/float8@1`, `pg/bool@1`, `pg/uuid@1`, and `pg/timestamptz-temporal@1`. To reuse a column's own type, pass the column to `.returns()`, as in `.returns(db.sql.public.user.columns.id)`, or read the id itself from `db.sql.public.user.columns.id.codecId`. See [`fns.raw` and `.returns()`](#fnsraw-and-returns). #### Return type @@ -283,7 +283,7 @@ const rows = await runtime.query(plan); ### `innerJoin()` -Combine matching rows from two tables. After a join, address columns by their table name (`f.post.id`, `f.user.email`) to avoid ambiguity. +Combine matching rows from two tables. After a join, a column name that both tables have must be written under its table, such as `f.post.id` or `f.user.id`. A name only one table has stays on `f` directly. #### Options @@ -316,9 +316,9 @@ const rows = await runtime.query(plan); Keep unmatched rows from one or both sides, filling the missing side's columns with `null`. Each takes the same `(other, on)` arguments and returns a `SelectQuery` as [`innerJoin()`](#innerjoin) does. -- `outerLeftJoin()` keeps every left-table row; unmatched right columns are `null`. -- `outerRightJoin()` keeps every right-table row; unmatched left columns are `null`. -- `outerFullJoin()` keeps unmatched rows from both sides. +- `outerLeftJoin()` is SQL's `LEFT OUTER JOIN`. It keeps every left-table row, and unmatched right columns are `null`. +- `outerRightJoin()` is SQL's `RIGHT OUTER JOIN`. It keeps every right-table row, and unmatched left columns are `null`. +- `outerFullJoin()` is SQL's `FULL OUTER JOIN`. It keeps unmatched rows from both sides. #### Examples @@ -340,9 +340,9 @@ Correlate a per-row subquery against the outer row: for each outer row, the join #### Remarks -- **Availability:** PostgreSQL only. Not available on SQLite, where the call does not compile. If you reach it anyway, it throws an error whose `code` is `ORM.CAPABILITY_MISSING` at the moment you call `lateralJoin()`. `outerLateralJoin()` is the left-join form: same arguments, same availability, and the subquery's columns are nullable. -- The callback receives a `lateral` builder. Start its subquery with `lateral.from(otherTable)`, then chain the usual `SELECT` methods. The subquery can filter on the outer row's columns, such as `f.user.id`. -- Return the query chain **directly** from the callback. Do **not** call `.as(...)` on it. The first argument of `lateralJoin()` already names the subquery, and a chain with `.as(...)` on the end throws `subquery.getRowFields is not a function`. +- **Availability:** PostgreSQL only. Not available on SQLite, where the call does not compile. Called from plain JavaScript, or after a cast, it throws an error whose `code` is `ORM.CAPABILITY_MISSING` at the moment you call `lateralJoin()`. `outerLateralJoin()` is the left-join form: same arguments, same availability, and the subquery's columns are nullable. +- The callback receives a `lateral` builder. Start its subquery with `lateral.from(otherTable)`, then chain the usual `SELECT` methods. Inside the callback, the outer table's columns are on `f` under its table name, such as `f.user.id`, so the subquery can filter on the outer row. +- Return the query chain **directly** from the callback. Do **not** call `.as(...)` on it. The first argument of `lateralJoin()` already names the subquery. - After the join, a column name that both tables have must be written under its table, such as `f.post.id` or `f.post.createdAt`. A bare `select('id')` or `orderBy((f) => f.createdAt, ...)` throws, because a name both tables have is not in the query's scope on its own. #### Options @@ -445,7 +445,7 @@ Keep the first row per distinct key, according to the query's `orderBy()` (`DIST #### Remarks -- **Availability:** PostgreSQL only. `DISTINCT ON` is a PostgreSQL extension to SQL. Not available on SQLite, where the call does not compile. If you reach it anyway, it throws an error whose `code` is `ORM.CAPABILITY_MISSING` at the moment you call `distinctOn()`. +- **Availability:** PostgreSQL only. Not available on SQLite, where the call does not compile. Called from plain JavaScript, or after a cast, it throws an error whose `code` is `ORM.CAPABILITY_MISSING` at the moment you call `distinctOn()`. - Put the `distinctOn()` columns first in `orderBy()`, then the column that decides which row wins. Nothing checks this for you, and without it you cannot predict which row you get. The order in which you call `distinctOn()` and `orderBy()` does not matter. #### Options @@ -541,9 +541,9 @@ const rows = await runtime.query(plan); ## Grouped queries -Grouping starts with [`groupBy()`](#groupby), which turns a query into a `GroupedQuery`. A grouped query supports `having()` for group-level filtering, the aggregate functions, and the same `orderBy()`, `limit()`, `offset()`, and `distinct()` methods as a `SelectQuery`. +Grouping starts with [`groupBy()`](#groupby), which turns a select query into a `GroupedQuery`. Call it after your `select()` calls, because a grouped query has no `select()` of its own. A grouped query supports `having()` for group-level filtering, the aggregate functions, and the same `orderBy()`, `limit()`, `offset()`, and `distinct()` methods as a `SelectQuery`. -The examples below use a separate `customer` / `order` schema, where `order.amount` and `order.quantity` are numeric columns: +The examples below use a separate `customer` / `order` schema, where `order.amount` is an integer column:
@@ -551,10 +551,8 @@ The examples below use a separate `customer` / `order` schema, where `order.amou ```prisma model Customer { - id Uuid @id @default(uuid()) - name String - segment String - + id Uuid @id @default(uuid()) + name String orders Order[] @@map("customer") @@ -564,7 +562,6 @@ model Order { id Uuid @id @default(uuid()) customerId Uuid amount Int - quantity Int placedAt DateTime @default(now()) customer Customer @relation(fields: [customerId], references: [id]) @@ -575,10 +572,6 @@ model Order {
-:::note[Aggregates return numbers, with exact variants beside them] -`count()`, `sum()` over an integer column, and `avg()` over an integer column all return a JavaScript `number`. If a `count()` or integer `sum()` result is larger than 2^53 - 1, the query fails with an error whose `code` is `RUNTIME.DECODE_FAILED`. Use `countBigInt()` or `sumBigInt(field)` instead when totals can get that large. `avg()` does not fail this way. It is computed as a floating-point `number` and loses precision instead, so use `avgDecimal(field)` when you need an exact figure. `min()` and `max()` return the input column's own type. -::: - ### `groupBy()` Group rows by one or more columns or by a computed expression, producing one row per distinct group. @@ -600,7 +593,7 @@ Group rows by one or more columns or by a computed expression, producing one row ##### Group by a column with a count -Every example from here on calls `runtime`, and names the built query `plan`. +Every example below assumes `const runtime = db.runtime();`, and names the built query `plan`. ```ts const runtime = db.runtime(); @@ -609,12 +602,23 @@ const plan = db.sql.public.order .select('customerId') .select('orderCount', (f, fns) => fns.count(f.id)) .groupBy('customerId') - .orderBy('customerId', { direction: 'asc' }) .build(); const rows = await runtime.query(plan); // e.g. { customerId: '
', orderCount: 5 } ``` +:::note[Most aggregates give you a `number`] +`count()`, `sum()` over an integer column, and `avg()` over an integer column all return a JavaScript `number`. `min()` and `max()` return the input column's own type. +::: + +:::note[Large counts and sums need the `bigint` version] +A `count()` or integer `sum()` result larger than 2^53 - 1 fails with an error whose `code` is `RUNTIME.DECODE_FAILED`. Use `countBigInt()` or `sumBigInt(field)` when totals can get that large. +::: + +:::note[`avg()` loses precision instead of failing] +`avg()` is computed as a floating-point `number`, so a long decimal is rounded. Use `avgDecimal(field)` when you need the exact figure. +::: + ##### Group by a computed value ```ts @@ -625,10 +629,10 @@ const plan = db.sql.public.order .build(); const rows = await runtime.query(plan); // rows === [{ yearPlaced: '2024', orderCount: 10 }] -// yearPlaced is the string '2024', not the number 2024. See `.returns()` below. +// yearPlaced is the string '2024'. Write Number(row.yearPlaced) to get 2024. See `.returns()` below. ``` -You can also group by an alias you gave a selected value, such as `.groupBy('yearPlaced')`. A name that is neither a column in scope nor an alias you selected throws an error whose `code` is `ORM.COLUMN_UNKNOWN`, at the moment you call `groupBy()`. Prisma ORM does not check that every other selected column is grouped or aggregated. PostgreSQL rejects the query when it runs. +You can also group by an alias you gave a selected value, such as `.groupBy('yearPlaced')`. A name that is neither a column of the table you started from nor an alias you selected throws an error whose `code` is `ORM.COLUMN_UNKNOWN`, at the moment you call `groupBy()`. If you select a column you did not group or aggregate, the query fails when it runs, not when you build it. ### `having()` @@ -636,7 +640,8 @@ Filter groups by an aggregate comparison. Build the predicate from the aggregate #### Remarks -- Compare against a plain JavaScript number, as in `fns.gt(fns.count(), 1)`. Do not pass a `bigint` literal such as `1n`. +- Compare against a plain JavaScript number, as in `fns.gt(fns.count(), 1)`. Passing a `bigint` literal such as `1n` throws an error whose `code` is `RUNTIME.ENCODE_FAILED` when the query runs. +- `having()` has no column-name form, so write the aggregate again in the predicate. TypeScript does let you name a selected alias on the `f` argument here, but PostgreSQL does not accept an alias in `HAVING`, and the query then fails when it runs. #### Options @@ -668,30 +673,22 @@ const rows = await runtime.query(plan); ##### Compare with `count()`, `avg()`, `min()`, `max()` ```ts -const avgPlan = db.sql.public.order +const plan = db.sql.public.order .select('customerId') .select('avgAmount', (f, fns) => fns.avg(f.amount)) - .groupBy('customerId') - .having((f, fns) => fns.gt(fns.avg(f.amount), 100)) - .build(); - -const minMaxPlan = db.sql.public.order - .select('customerId') + .select('exactAvg', (f, fns) => fns.avgDecimal(f.amount)) .select('minAmount', (f, fns) => fns.min(f.amount)) .select('maxAmount', (f, fns) => fns.max(f.amount)) .groupBy('customerId') - .having((f, fns) => fns.eq(fns.min(f.amount), 100)) + .having((f, fns) => fns.and(fns.gt(fns.count(), 1), fns.gt(fns.avg(f.amount), 100))) .build(); - -const avgRows = await runtime.query(avgPlan); -const minMaxRows = await runtime.query(minMaxPlan); -// avgAmount is 300, and avgDecimal(f.amount) would give the exact '300.0000000000000000' -// minAmount is 100 and maxAmount is 500 +const rows = await runtime.query(plan); +// avgAmount is 300, exactAvg is the exact string '300.0000000000000000', minAmount is 100, maxAmount is 500 ``` ### Ordering and limiting a grouped query -A `GroupedQuery` supports the same `orderBy()`, `limit()`, `offset()`, `distinct()`, and [`distinctOn()`](#distincton) methods as a `SelectQuery`. Sort by an aggregate alias to order groups. The alias must be one you selected in this query. Any other name throws an error whose `code` is `ORM.COLUMN_UNKNOWN`, at the moment you call `orderBy()`. +A `GroupedQuery` supports the same `orderBy()`, `limit()`, `offset()`, `distinct()`, and [`distinctOn()`](#distincton) methods as a `SelectQuery`. Sort by an aggregate alias to order groups. The alias must be one you selected in this query. Any other name throws an error whose `code` is `ORM.COLUMN_UNKNOWN`, at the moment you call `orderBy()`. On a grouped query, `distinct()` and `distinctOn()` drop repeated rows from the groups you already have, which only matters when you select fewer columns than you grouped by. #### Examples @@ -711,7 +708,7 @@ const rows = await runtime.query(plan); ## Mutations -Mutations start from a table with `insert()`, `update()`, or `delete()`, and end with [`build()`](#build). Add [`returning()`](#returning) to get the affected rows back. +Mutations start from a table with `insert()`, `update()`, or `delete()`, and end with [`build()`](#build). Add [`returning()`](#returning) to get the affected rows back. The examples below go back to this page's main schema, so they use `db.sql.public.tag` and `db.sql.public.user` again. ### `insert()` @@ -720,7 +717,7 @@ Insert one or more rows in a single statement. #### Remarks - `insert()` always takes an **array**. A single-row insert is a one-element array. There is no separate single-row overload. An empty array throws an error whose `code` is `ORM.MUTATION_DATA_MISSING`. -- Leave a column out and the contract's default for it is applied, such as `@default(uuid())` or `@default(now())`. The value is generated when you call `build()`. +- Leave a column out and the contract's default for it is applied, such as `@default(uuid())` or `@default(now())`. The value is generated when you call `build()`, so build once and run once. Running the same built insert twice inserts the same generated value twice. Build it again for a new one. - There is no `ON CONFLICT` and no upsert here. For "insert or update", use the ORM client's [`upsert()`](/orm/reference/orm-client#upsert) or write the statement with [`db.raw.sql`](/orm/reference/raw-queries). #### Options @@ -729,7 +726,7 @@ Insert one or more rows in a single statement. |---|---|---|---| | `rows` | Array of row objects | Yes | The rows to insert. Every column is optional. | -Each value is sent as a query parameter, never pasted into the SQL. The column it goes into decides how it is stored and read, so you never annotate an insert value. +Each value is sent as a query parameter, never pasted into the SQL. The column it goes into decides how it is stored and read, so you never call `.returns()` or [`param()`](#param) on an insert value. #### Return type @@ -744,15 +741,13 @@ Each value is sent as a query parameter, never pasted into the SQL. The column i ```ts const plan = db.sql.public.tag.insert([{ label: 'single-row-tag' }]).build(); await runtime.execute(plan); -// id is filled in from the contract's @default(uuid()) +// `execute` runs a write that returns no rows, see [Running a built query](#executing-a-plan). id is filled in from @default(uuid()) ``` ##### Insert multiple rows in one statement ```ts -const plan = db.sql.public.tag - .insert([{ label: 'multi-row-a' }, { label: 'multi-row-b' }]) - .build(); +const plan = db.sql.public.tag.insert([{ label: 'multi-row-a' }, { label: 'multi-row-b' }]).build(); await runtime.execute(plan); ``` @@ -762,7 +757,7 @@ Return columns from the rows affected by an `insert()`, `update()`, or `delete() #### Remarks -- **Availability:** PostgreSQL and SQLite. Both support SQL's `RETURNING`. On a database without it, TypeScript rejects the call. If you get past the type check, for example by casting, the call throws an error whose `code` is `ORM.CAPABILITY_MISSING`. +- **Availability:** PostgreSQL and SQLite, the two databases the builder supports today. Both support SQL's `RETURNING`. #### Options @@ -774,7 +769,7 @@ Return columns from the rows affected by an `insert()`, `update()`, or `delete() | Return type | Example | Description | |---|---|---| -| Mutation query | `db.sql.public.tag.insert([...]).returning('id', 'label')` | The mutation query, now typed to resolve to the returned rows. | +| Mutation query | `db.sql.public.tag.insert([...]).returning('id', 'label')` | The mutation query, which now gives you back those columns from each affected row. Run it with `query`, not `execute`. | #### Examples @@ -799,6 +794,8 @@ Update matched rows. Set columns with a values object, or derive new values from | Values object | `update({ col: value, ... })` | Set columns to fixed values. | | Expression callback | `update((f, fns) => ({ col: expr }))` | Set columns to expressions computed from existing columns. | +Every value the callback returns must be an expression, so you cannot put a plain JavaScript value beside a computed one. To set a fixed value in the callback form, write it as a `fns.raw` fragment with `.returns(...)`. + #### Return type | Return type | Example | Description | @@ -863,6 +860,7 @@ Give a value its type by hand, for a value with no column to take the type from, - Import `param` from `@prisma/orm-postgres/relational-core/expression`. - You rarely need `param()`. Values passed to `insert()`, `update()`, and comparison functions such as `fns.eq(f.col, value)` are already sent as query parameters, never pasted into the SQL, and each takes its type from the column it is used with. `param()` is for the values that have no such column. +- `codecId` is the type id, the name this page uses everywhere else. [Binding a bare value with `param()`](/orm/reference/raw-queries#binding-a-bare-value-with-param) lists the type id each kind of JavaScript value gets, which is where to look for a date, a uuid, a boolean, or an array. #### Options @@ -888,7 +886,6 @@ const targetEmailDomain = param('%@example.com', { codecId: 'pg/text@1' }); const plan = db.sql.public.user .select('id', 'email') .where((f, fns) => fns.raw`${f.email} LIKE ${targetEmailDomain}`.returns('pg/bool@1')) - .orderBy('email', { direction: 'asc' }) .build(); const rows = await runtime.query(plan); ``` @@ -899,34 +896,42 @@ Callback forms receive `fns` alongside the `f` argument. `fns` is an object of h ### Built-in functions -These are fixed: +These exist on every database: | Category | Functions | |---|---| -| Comparison | `eq`, `ne`, `gt`, `gte`, `lt`, `lte` | -| Membership | `in`, `notIn` | -| Boolean | `and`, `or` | -| Existence | `exists`, `notExists` | +| Comparison | `eq(a, b)`, `ne(a, b)`, `gt(a, b)`, `gte(a, b)`, `lt(a, b)`, `lte(a, b)` | +| Membership | `in(expr, values)`, `notIn(expr, values)`, where `values` is an array or a built subquery | +| Boolean | `and(...predicates)`, `or(...predicates)` | +| Existence | `exists(subquery)`, `notExists(subquery)`, taking a select query you do not call `build()` on | | Raw SQL | `raw` | +```ts +const named = db.sql.public.user + .select('id', 'email') + .where((f, fns) => fns.in(f.email, ['bob@example.com', 'carol@example.com'])) + .build(); + +const authors = db.sql.public.user + .select('id', 'email') + .where((f, fns) => fns.exists(db.sql.public.post.select('id').where((inner, innerFns) => innerFns.eq(inner.userId, f.id)))) + .build(); +``` + Which aggregate functions exist depends on your database. PostgreSQL gives you these eight: | Function | Returns | |---|---| | `count()` / `count(field)` | `number`. With no argument it counts rows; with a field it counts that field's non-null values | -| `countBigInt()` / `countBigInt(field)` | `bigint`, the exact form of `count` | -| `sum(field)` | `number` over an integer column; over any other column, the type PostgreSQL's own `sum` produces for it | -| `sumBigInt(field)` | `bigint`, the exact form of `sum` over an integer column | +| `countBigInt()` / `countBigInt(field)` | `bigint`. The version of `count` that does not overflow | +| `sum(field)` | `number` over an integer column. Over a `numeric` column you get a string, and over a `float8` column a `number` | +| `sumBigInt(field)` | `bigint`. The version of `sum` over an integer column that does not overflow | | `avg(field)` | `number` over an integer or float column | -| `avgDecimal(field)` | An exact decimal string, the exact form of `avg` | +| `avgDecimal(field)` | An exact decimal string. The version of `avg` that does not round | | `min(field)` | The input column's own type. A `varchar` column is typed as `text` | | `max(field)` | The input column's own type. A `varchar` column is typed as `text` | -On PostgreSQL you also get `ilike` for text columns. `cosineDistance` comes with pgvector, so it is there only if your contract uses pgvector. - -:::note[No `COALESCE` or `CAST` helpers] -There is no `fns.coalesce` or `fns.cast`. Express `COALESCE`, `CAST`, and any other SQL function you need with [`fns.raw`](#fnsraw-and-returns) and an explicit `.returns(...)` call. -::: +On PostgreSQL you also get `ilike(expr, pattern)` for text columns, as in `fns.ilike(f.displayName, '%alice%')`. `cosineDistance` comes with pgvector, so it is there only if your contract uses pgvector. ### `fns.raw` and `.returns()` @@ -937,12 +942,13 @@ Write a raw SQL fragment as a tagged template. Interpolate columns, typed expres | Name | Type | Required | Description | |---|---|---|---| | SQL fragment | Tagged template | Yes | The raw SQL, with `${...}` interpolations for columns and values. | -| `.returns(codecId)` | `string` | Yes for a selected value, a compared value, or a fragment used on its own as a `where()` predicate | Declares the result type, for example `'pg/int4@1'`, `'pg/text@1'`. A fragment used as a `where()` predicate declares `'pg/bool@1'`. | +| `.returns(codecId)` | `string` | Yes, always | Declares the result type, for example `'pg/int4@1'`, `'pg/text@1'`. A fragment used as a `where()` predicate declares `'pg/bool@1'`. | #### Remarks -- **`.returns()` only tells TypeScript what type to expect.** It does not change the value you actually get back. If PostgreSQL computes the value as `numeric`, you get a string at run time even though you annotated `'pg/int4@1'`. See [Group by a computed value](#group-by-a-computed-value). -- Use `fns.raw` for any SQL feature without a dedicated helper: `COALESCE`, `CAST`, `LENGTH`, `UPPER`, `EXTRACT`, and so on. +- **`.returns()` is always required.** A `fns.raw` fragment only becomes an expression once you call it, so there is nowhere in the builder you can use a fragment without it. +- **`.returns()` only tells TypeScript what type to expect.** It does not change the value you actually get back. PostgreSQL computes `EXTRACT` as a `numeric`, and a `numeric` reaches you as a string even though you annotated `'pg/int4@1'`. Writing `CAST(... AS int4)` inside the fragment does not change that either. To get a number, either convert it in JavaScript with `Number(row.yearPlaced)`, or select a real column of that type, or write the whole statement with [`db.raw.sql` and `.returnsRow()`](/orm/reference/raw-queries), which does read the value back as its declared type. +- Use `fns.raw` for any SQL feature without a dedicated helper: `COALESCE`, `CAST`, `LENGTH`, `UPPER`, `EXTRACT`, and so on. There is no `fns.coalesce` and no `fns.cast`. #### Examples @@ -952,7 +958,6 @@ Write a raw SQL fragment as a tagged template. Interpolate columns, typed expres const plan = db.sql.public.user .select('emailLength', (f, fns) => fns.raw`LENGTH(${f.email})`.returns('pg/int4@1')) .build(); -const rows = await runtime.query(plan); ``` ## Compiling and executing @@ -963,7 +968,7 @@ Compile a query so you can run it. #### Remarks -- `build()` takes **zero arguments** on every query type: select, insert, update, delete, and grouped. You supply parameter values where you write them, inside `insert([...])`, a `where()` callback, or a [`param()`](#param) call. There is no `build({ params })` form. +- `build()` takes **zero arguments** on every query type: select, insert, update, delete, and grouped. You supply parameter values where you write them, inside `insert([...])`, a `where()` callback, or a [`param()`](#param) call. #### Return type @@ -975,11 +980,6 @@ Compile a query so you can run it. Run a built query with `runtime.query(...)`, where `runtime` is `db.runtime()`. The rule is: use `query` for anything that returns rows, which means a `SELECT` or a write with `returning()`. Use `execute` for a write with no `returning()`. `query` returns an `AsyncIterableResult`. `await` it for an array of rows (`Row[]`), or `for await` over it to take one row at a time. `execute` resolves to `{ affectedRows }`. -```ts -const plan = db.sql.public.user.select('id', 'email').build(); -const rows = await runtime.query(plan); // Row[] -``` - ### `ResultType` Recover a built query's row type at the type level. @@ -988,7 +988,7 @@ Recover a built query's row type at the type level. - Import `ResultType` from `@prisma/orm-postgres/components/runtime`. It works with the ORM client as well as the SQL builder. - `ResultType` is **one row**, 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 [Model and result types](/orm/reference/orm-client#model-and-result-types). +- It also works on ORM queries: `ResultType`. `db.orm` is keyed by model name, so the model is `User` there and the table is `user` under `db.sql`. See [Model and result types](/orm/reference/orm-client#model-and-result-types). #### Examples @@ -1007,6 +1007,6 @@ const rows = await runtime.query(plan); // Row[] `await runtime.query(plan)` collects every row into an array. This is the common case and what every example on this page uses. You can also loop the same result with `for await` to handle one row at a time. -`postgres(...)` turns cursors off in the driver it builds, so every query loads every row into memory first. `await` and `for await` read that same in-memory result, so `for await` does not lower the memory a large result uses. The connection goes back to the pool before you read the rows. +`for await` does not save memory on PostgreSQL: the client loads every row first, and both `await` and `for await` read that same in-memory result. The connection goes back to the pool before you read the rows. -Each result is read once, one way. Re-awaiting a result you already awaited is safe and gives you the same array. Switching between `await` and `for await`, or looping a second time with `for await`, throws an error whose `code` is `RUNTIME.ITERATOR_CONSUMED`. See [`AsyncIterableResult`](/orm/reference/orm-client#asynciterableresult). +You can `await` a result as many times as you like. You cannot mix `await` and `for await` on the same result, and you cannot `for await` it twice. Either one throws an error whose `code` is `RUNTIME.ITERATOR_CONSUMED`. See [`AsyncIterableResult`](/orm/reference/orm-client#asynciterableresult). diff --git a/apps/docs/content/docs/orm/reference/transactions-and-runtime.mdx b/apps/docs/content/docs/orm/reference/transactions-and-runtime.mdx index a77eb51705a..5454ffd3763 100644 --- a/apps/docs/content/docs/orm/reference/transactions-and-runtime.mdx +++ b/apps/docs/content/docs/orm/reference/transactions-and-runtime.mdx @@ -6,7 +6,11 @@ metaTitle: Prisma ORM transactions and runtime reference metaDescription: 'Reference for the Prisma ORM client lifecycle, transactions, prepared statements, and execution options.' --- -This page documents the client lifecycle (create, connect, close), transactions, prepared statements, and per-query execution options across PostgreSQL and MongoDB. Availability is stated per method: where the two databases differ, or where a method exists on only one of them, the Remarks say so. Transactions are fully supported on PostgreSQL. They are not available on MongoDB; see [Transactions (MongoDB)](#transactions-mongodb) for what to do instead. +Prisma ORM 8 replaces `schema.prisma` with `contract.prisma`. Run `npx prisma contract emit` to compile that file into `contract.json`, which your app reads at run time, and `contract.d.ts`, which TypeScript reads. A new project keeps all three files in `src/prisma/`. Every client you create needs `contract.json`. [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7) lists the packages to install, the commands to run, and what replaces `prisma.user.findMany()`. + +The runtime is the connected object you run queries on. `db.runtime()` returns it. + +This page documents the client lifecycle (create, connect, close), transactions, prepared statements, and per-query execution options across PostgreSQL and MongoDB. Availability is stated per method: where the two databases differ, or where a method exists on only one of them, the Remarks say so. Transactions are fully supported on PostgreSQL. They are not available on MongoDB. To run one there, share one `MongoClient` between Prisma ORM and your own `mongodb` driver code and use a driver session. See [Transactions (MongoDB)](#transactions-mongodb). For a task-oriented walkthrough, see the Fundamentals guide to [Transactions](/orm/fundamentals/transactions). For the ways to query inside a transaction, see the [ORM client reference](/orm/reference/orm-client), the [SQL query builder reference](/orm/reference/sql-query-builder), and the [raw queries reference](/orm/reference/raw-queries). @@ -20,12 +24,13 @@ Create a PostgreSQL client. #### Remarks -- Your contract is the `contract.prisma` file that replaced `schema.prisma`. Running `npx prisma contract emit` compiles it into two files: `contract.json`, read at run time, and `contract.d.ts`, read by TypeScript. A new project keeps all three in `src/prisma/`. [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7) lists the packages to install and the commands to run. - Import `contract.json` and pass it as `contractJson`, as every example below does. Pass `contract` instead only when you already hold a contract object in your own code, because Prisma ORM converts it to the same JSON. Supply exactly one. -- A binding tells the client which database to use. Pass exactly one of `url`, `pg`, and `binding`, or leave all three out and pass one to [`connect()`](#connect). The `binding` form is the same three choices in one object: `{ kind: 'url', url }`, `{ kind: 'pgPool', pool }`, or `{ kind: 'pgClient', client }`. -- When you pass a `pg` pool or client, you own it and you close it. When you pass a `url`, the client creates the pool and closes it. The pool it creates uses `connectionTimeoutMillis: 20000` and `idleTimeoutMillis: 30000`. Override them with `poolOptions`. `poolOptions` has no effect on a pool you supply yourself. -- Some types come from a PostgreSQL extension, such as a `pgvector.Vector(1536)` type in your `contract.prisma`. Pass that extension's package in `extensions`: `import pgvector from '@prisma/orm-extension-pgvector/runtime'`, then `extensions: [pgvector]`. -- Prisma ORM keeps a record in your database of which contract the database was set up for. With `verifyMarker: 'onFirstUse'`, the default, the first query checks that record, logs a warning if it is missing or does not match, and runs the query anyway. `verifyMarker: false` skips the check. See [How migrations work](/orm/migrations/how-migrations-work). +- The `` type argument is what types `db.orm` and `db.sql`. Pass it yourself whenever you pass `contractJson`, because TypeScript cannot read a type out of a JSON file. It is inferred only on the `contract` path. +- Tell the client which database to use in one of three ways: `url`, `pg`, or `binding`. Use `binding` when your code chooses between them at run time, because it is one object instead of three separate option names. Its three kinds are `{ kind: 'url', url }`, `{ kind: 'pgPool', pool }`, and `{ kind: 'pgClient', client }`. Pass exactly one of `url`, `pg`, and `binding`, or leave all three out and pass one to [`connect()`](#connect). +- When you pass a `pg` pool or client, you own it and you close it. When you pass a `url`, the client creates the pool and closes it. Set that pool's timeouts with `poolOptions`. `poolOptions` has no effect on a pool you supply yourself. +- Some types come from a PostgreSQL extension, such as a `pgvector.Vector(1536)` type in your `contract.prisma`. Run `npm install @prisma/orm-extension-pgvector` first. Then `import pgvector from '@prisma/orm-extension-pgvector/runtime'` and pass `extensions: [pgvector]`. +- A middleware is an object with a name and one or more hooks, such as `{ name: 'no-big-deletes', beforeQuery: (query) => { if (isForbidden(query)) throw new Error('blocked') } }`. Throw from a hook to block the query. See [How middleware works](/orm/middleware/how-middleware-works). +- Your migrations write a row into your database recording which contract that database matches. With `verifyMarker: 'onFirstUse'`, the default, the first query compares that row with your contract, logs a warning if the row is missing or does not match, and runs the query anyway. A warning usually means you have not run your migrations, so run them. `verifyMarker: false` skips the check. See [How migrations work](/orm/migrations/how-migrations-work). - `postgres(options)` does not open a connection. The client opens one on first use, so [`connect()`](#connect) is optional: call it to open the connection up front and fail early. #### Options @@ -50,9 +55,9 @@ Pass exactly one of `url`, `pg`, and `binding`. | `PostgresClient` | `postgres({ contractJson, url })` | A client that has not connected yet. Its members are listed below. | - `.orm` holds your models by model name. See the [ORM client reference](/orm/reference/orm-client). -- `.sql` is the SQL query builder, keyed by table name. See the [SQL query builder reference](/orm/reference/sql-query-builder). `.raw` writes raw SQL as a template string. See the [raw queries reference](/orm/reference/raw-queries). -- `.enums` holds the enum values from your contract, by PostgreSQL schema: `db.enums.public.Role`. `.nativeEnums` holds the enum types defined in PostgreSQL itself. -- `.contract` is the contract Prisma ORM read from `contractJson`, as an object. `.context` is internal: the query builders read it, and you do not call it. +- `.sql` is the SQL query builder, keyed by table name. The table name is the model's `@@map` value, or the model name with a lowercase first letter when the model sets no `@@map`. See the [SQL query builder reference](/orm/reference/sql-query-builder). `.raw` writes raw SQL as a template string. See the [raw queries reference](/orm/reference/raw-queries). +- `.enums` holds the enum values from your contract, by PostgreSQL schema: `db.enums.public.Role`. `.nativeEnums` holds the values of enum types that exist in PostgreSQL itself, created with `CREATE TYPE`, also by schema: `db.nativeEnums.public['AalLevel'].values` is `['aal1', 'aal2']`. In `.nativeEnums` each member's name is its own value, because a PostgreSQL enum type has values and no separate names. +- `.contract` is the contract as an object. `.context` is internal, not for you. - [`connect()`](#connect), [`runtime()`](#runtime), [`transaction()`](#dbtransactioncallback), [`prepare()`](#dbpreparedeclaration-callback), and [`close()`](#close) are documented on this page. #### Examples @@ -89,11 +94,11 @@ Open a connection and return a runtime. - Returns a `Promise`. The runtime is the connected object that runs queries. It has five calls: - `runtime.query(built)` runs a built query and returns its rows. A built query is the object you get from `.build()` at the end of a builder or raw chain. See [`runtime.query()`](#runtimequery). - `runtime.execute(built)` runs a built query that returns no rows, such as an insert, and returns `{ affectedRows }`. - - `runtime.connection()` takes one connection out of the pool so that several queries run on the same connection. See [Manual connection and transaction control](#manual-connection-and-transaction-control). + - `runtime.connection()` takes one connection out of the pool so that several queries run on the same connection. Call `connection.release()` when you are done, to put it back. See [Manual connection and transaction control](#manual-connection-and-transaction-control). - `runtime.prepare(...)` prepares a statement you run many times with different values. See [Prepared statements (PostgreSQL)](#prepared-statements-postgresql). - `runtime.telemetry()` reports how the most recent query went. See [`runtime.telemetry()`](#runtimetelemetry). -- `connect()` accepts a binding, so a client you created without one can be given its database here. -- Every error carries a `code`: to handle one, catch the error and compare `error.code` with the code's string. +- `connect()` takes the same database options as `postgres(...)`, so a client you created without one can be given its database here: `await db.connect({ url: process.env.DATABASE_URL })`, or `await db.connect({ binding: { kind: 'url', url } })`. +- Every error has a `code` property. Catch the error and compare `error.code` with the value shown here, for example `if (error.code === 'DRIVER.NOT_CONNECTED')`. - Calling `connect()` once the client has already connected rejects with an error whose `code` is `DRIVER.ALREADY_CONNECTED`. Calling `connect()` after the client has been closed rejects with an error whose `code` is `DRIVER.NOT_CONNECTED` (`Postgres client is closed`). #### Return type @@ -181,15 +186,16 @@ Create a MongoDB client. #### Remarks - Pass the contract with `contractJson` or `contract` (supply exactly one), the same way as on PostgreSQL. -- A binding tells the client which database to use. Pass exactly one of `url`, `uri` plus `dbName`, `mongoClient` plus `dbName`, and `binding`, or leave all four out and pass one to [`connect()`](#connect-1). The `binding` form is the same choices in one object: `{ kind: 'url', url, dbName }` or `{ kind: 'mongoClient', client, dbName }`. +- Tell the client which database to use in one of four ways: `url`, `uri`, `mongoClient`, or `binding`. Use `binding` when your code chooses between them at run time, because it is one object instead of separate option names. It has two kinds, `{ kind: 'url', url, dbName }` and `{ kind: 'mongoClient', client, dbName }`. The `uri` path uses the `url` kind. +- Pass exactly one of `url`, `uri`, `mongoClient`, and `binding`, or leave all four out and pass one to [`connect()`](#connect-1). `dbName` is a separate option and does not count as one of the four, so `{ url, dbName }` is allowed. - With `url`, the database name comes from the path of the connection string, as in `mongodb://host:27017/app`. Add `dbName` to override that name, or when the string has no name in its path. - With `uri`, Prisma ORM never reads the database name from the string, so `dbName` is required. That is the only difference between `uri` and `url`. -- **Client ownership**: with `url` or `uri`, Prisma ORM creates the underlying `MongoClient` and closes it on `close()`. With `mongoClient`, you supplied the client, so Prisma ORM does **not** close it: `close()` leaves your client open and you close it yourself. That is what lets you use one `MongoClient` both through Prisma ORM and in code you write against the `mongodb` package directly. +- **Client ownership**: with `url` or `uri`, Prisma ORM creates the underlying `MongoClient` and closes it on `close()`. With `mongoClient`, you supplied the client, so Prisma ORM does **not** close it. Because `close()` leaves your client open, you can use one `MongoClient` both through Prisma ORM and in code you write against the `mongodb` package directly. You close that client yourself. - `mongo(options)` does not open a connection. The runtime is built on first use, or explicitly via [`connect()`](#connect-1). #### Options -Pass exactly one of `url`, `uri` plus `dbName`, `mongoClient` plus `dbName`, and `binding`. +Pass exactly one of `url`, `uri`, `mongoClient`, and `binding`. `dbName` is separate, and `uri` and `mongoClient` both require it. | Name | Type | Required | Description | |---|---|---|---| @@ -199,7 +205,7 @@ Pass exactly one of `url`, `uri` plus `dbName`, `mongoClient` plus `dbName`, and | `mongoClient` + `dbName` | `MongoClient` + `string` | See note | An existing `MongoClient` you own, plus the database name. | | `binding` | `{ kind: 'url' \| 'mongoClient', ... }` | See note | The same choices in one object. | | `middleware` | Array of middleware | No | Code that runs around every query. See [How middleware works](/orm/middleware/how-middleware-works). | -| `mode` | `'strict'` or `'permissive'` | No | Passed through to your middleware, which decides what to do with it. Defaults to `'strict'`. | +| `mode` | `'strict'` or `'permissive'` | No | No built-in behaviour. Your own middleware can read it. Defaults to `'strict'`. | #### Return type @@ -207,7 +213,7 @@ Pass exactly one of `url`, `uri` plus `dbName`, `mongoClient` plus `dbName`, and |---|---|---| | `MongoClient` | `mongo({ contractJson, url, dbName })` | Prisma ORM's `MongoClient` type, not the driver's. A client that has not connected yet. | -- `.orm` holds your models by model name, with no schema segment: `db.orm.users`. See the [ORM client reference](/orm/reference/orm-client). +- `.orm` holds your models by collection name, with no schema segment: `db.orm.users`. The collection name is the model's `@@map` value, or the model name with a lowercase first letter when the model sets no `@@map`. The [example schema](/orm/reference/orm-client#example-schema) sets `@@map("users")` on `User`, which is why the key here is `users`. See the [ORM client reference](/orm/reference/orm-client). - `.query` is the pipeline builder, which builds MongoDB aggregation pipelines. See the [pipeline builder reference](/orm/reference/pipeline-builder). `.raw` sends raw MongoDB commands. See the [raw queries reference](/orm/reference/raw-queries). - `.enums` holds the enum values from your contract, `.contract` is the contract as an object, and `.context` is internal. [`connect()`](#connect-1), [`runtime()`](#runtime-1), and [`close()`](#close-1) are documented below. @@ -231,6 +237,7 @@ import { MongoClient } from 'mongodb'; import type { Contract } from './contract.d'; import contractJson from './contract.json' with { type: 'json' }; +// `MongoClient` here is the driver's class; the `db` below is Prisma ORM's own MongoClient type const client = new MongoClient(process.env.MONGODB_URL!); const db = mongo({ contractJson, mongoClient: client, dbName: 'app' }); // you own `client`; db.close() will not close it for you @@ -243,7 +250,7 @@ Open a connection and return a runtime. #### Remarks - Returns a `Promise`. Calling `connect()` when the client is already connected rejects with an error whose `code` is `DRIVER.ALREADY_CONNECTED`. Running any query connects the client too, so if you then call `connect()`, it fails for the same reason. PostgreSQL raises the same code from the same situation. -- `connect()` accepts a binding, so a client you created without one can be given its database here. +- `connect()` takes the same database options as `mongo(...)`, so a client you created without one can be given its database here: `await db.connect({ url: process.env.MONGODB_URL, dbName: 'app' })`. - After the client has been closed, `connect()` rejects with an error whose `code` is `DRIVER.NOT_CONNECTED` (`Mongo client is closed`). #### Return type @@ -252,23 +259,14 @@ Open a connection and return a runtime. |---|---|---| | `Promise` | `const runtime = await db.connect()` | The connected runtime (`query`, `execute`, and `close` only). | -#### Examples - -##### Connecting twice throws - -```typescript -await db.connect(); -await db.connect(); // rejects with DRIVER.ALREADY_CONNECTED -``` - ### `runtime()` Get the runtime. #### Remarks -- On MongoDB, `runtime()` returns a **`Promise`**: you must `await` it. The connection to MongoDB is opened on first use, which is why this call is asynchronous. PostgreSQL's `runtime()` is synchronous, so write `await db.runtime()` on MongoDB and `db.runtime()` on PostgreSQL. -- `MongoRuntime` has `query`, `execute`, and `close` only. It has no `connection()`, `prepare()`, or `telemetry()` (see [Transactions (MongoDB)](#transactions-mongodb) and [Execution options and results](#execution-options-and-results)). +- On MongoDB, `runtime()` returns a **`Promise`**: you must `await` it. PostgreSQL's `runtime()` is synchronous, so write `await db.runtime()` on MongoDB and `db.runtime()` on PostgreSQL. +- `MongoRuntime` has `query`, `execute`, and `close` only. It has no `connection()`, `prepare()`, or `telemetry()`. See [Transactions (MongoDB)](#transactions-mongodb) and [Execution options and results](#execution-options-and-results). #### Return type @@ -282,9 +280,9 @@ Run a built query through the runtime. #### Remarks -- `(await db.runtime()).query(built)` runs any built MongoDB query, including one built by the [pipeline builder](/orm/reference/pipeline-builder) (`db.query`). The client itself has no `execute()` method. +- `(await db.runtime()).query(built)` runs any built MongoDB query, including one built by the [pipeline builder](/orm/reference/pipeline-builder) (`db.query`). - Returns an [`AsyncIterableResult`](#asynciterableresult), which holds the rows the query returns: `await` it for an array, or `for await` to read the rows one at a time as they arrive. -- `runtime.execute(built)` is the sibling call for writes that return no rows. It returns `{ affectedRows }`. +- For a write that returns no rows, use `runtime.execute(built)`, which returns `{ affectedRows }`. There is no `execute()` on the client itself. #### Return type @@ -297,7 +295,7 @@ Run a built query through the runtime. ##### Run a query built by the pipeline builder [#run-a-pipeline-builder-plan] ```typescript -// 'posts' is the collection name, which is your model name with a lowercase first letter +// 'posts' is the collection name, the same key you use on `db.orm` const built = db.query.from('posts').build(); const posts = await (await db.runtime()).query(built); ``` @@ -308,6 +306,7 @@ Close the client. #### Remarks +- Call `db.close()`, not `runtime.close()`. `db.close()` marks the client closed and then closes the `MongoClient` Prisma ORM created. `runtime.close()` closes only the connection Prisma ORM opened and does not mark the client closed, so the client then looks usable and is not. - Returns a `Promise`. After `close()`, any further use rejects with an error whose `code` is `DRIVER.NOT_CONNECTED` (`Mongo client is closed`). That covers `db.runtime()` and ORM access such as `db.orm.users.first()`, since both go through the same runtime. When you supplied a `mongoClient`, `close()` does not close your client (see [`mongo(options)`](#mongooptions)). #### Return type @@ -346,7 +345,7 @@ The MongoDB client implements `Symbol.asyncDispose` too, so `await using` closes ``` ## Transactions (PostgreSQL) -A transaction groups several writes into one unit: they all commit together, or they all roll back. Use `db.transaction(...)` in application code. Use `withTransaction(...)` when you hold a runtime but no client. Open a connection and drive the transaction yourself when you need the same connection between statements. +A transaction groups several writes into one unit: they all commit together, or they all roll back. Use `db.transaction(...)` in application code. Use `withTransaction(...)` in a helper or a library that is handed a runtime rather than a client. Open a connection and drive the transaction yourself when you need the same connection between statements. ### `db.transaction(callback)` @@ -354,10 +353,10 @@ Run a callback inside a transaction. The transaction commits when the callback r #### Remarks -- Query through `tx`, not `db`. `tx.orm` holds your models. Build queries with `tx.sql`, then run them with `tx.query(...)` to get rows back, or `tx.execute(...)` when the write returns no rows. `tx.enums` holds the members of the enums declared in your contract, and `tx.nativeEnums` holds the members of the PostgreSQL enum types in your database. Every call on `tx` uses the same transaction connection, and queries on `db` run outside the transaction. -- `db.transaction(...)` takes the callback and nothing else. There are no `isolationLevel`, `timeout`, or `maxWait` options. Nothing retries a failed transaction for you, so call `db.transaction(...)` again yourself when you want a retry. -- To bound the queries inside a transaction, pass a `signal` to each `tx.query(...)` and `tx.execute(...)` call. See [`RuntimeExecuteOptions`](#runtimeexecuteoptions). -- `tx` has no `transaction()` method, so transactions do not nest. When a function you call opens its own transaction today, pass `tx` down to that function instead. +- Query through `tx`, not `db`. `tx.orm` holds your models. Build queries with `tx.sql`, then run them with `tx.query(...)` to get rows back, or `tx.execute(...)` when the write returns no rows. Every call on `tx` uses the same transaction connection, and queries on `db` run outside the transaction. +- `db.transaction(...)` takes the callback and nothing else. There are no `isolationLevel`, `timeout`, or `maxWait` options, and Prisma ORM offers no way to set the isolation level. Nothing retries a failed transaction for you, so when PostgreSQL reports a serialization failure, call `db.transaction(...)` again yourself. +- To bound the queries inside a transaction, pass a `signal` to each `tx.query(...)` and `tx.execute(...)` call, as the example below does. See [`RuntimeExecuteOptions`](#runtimeexecuteoptions). +- `tx` has no `transaction()` method, so transactions do not nest. If a helper you call starts its own transaction, change it to take `tx` as a parameter instead. Annotate that parameter with `TransactionContext`, imported from `@prisma/orm-postgres/family-runtime`, which covers `query` and `execute`. - `tx.sql` is a full SQL builder, keyed the same way as `db.sql`: use `tx.sql.public.
`, where `public` is the PostgreSQL schema. - Reads inside the transaction see the transaction's own uncommitted writes. - The callback's return value passes through as the result of `db.transaction(...)`. @@ -373,7 +372,8 @@ Run a callback inside a transaction. The transaction commits when the callback r | Return type | Example | Description | |---|---|---| -| `Promise` | `await db.transaction(async (tx) => ...)` | Whatever the callback returns. | +| `Promise` | `await db.transaction(async (tx) => ...)` | Whatever the callback returns, so `R` is your callback's own return type. | +| `tx.enums` / `tx.nativeEnums` | `tx.enums.public.Role.values` | The same enum accessors the client has. `.values` is the list of members. See [`postgres(options)`](#postgresoptions). | #### Examples @@ -415,8 +415,10 @@ const { createdId, found } = await db.transaction(async (tx) => { ```typescript await db.transaction(async (tx) => { + const signal = AbortSignal.timeout(5000); // give this statement five seconds await tx.execute( tx.sql.public.tag.insert([{ id: crypto.randomUUID(), label: 'tx-sql-insert' }]).build(), + { signal }, ); }); ``` @@ -442,7 +444,7 @@ For Prisma ORM 7 users, an array of queries becomes a callback: - prisma.post.create({ data: { title, userId } }), - ]); + const { user, post } = await db.transaction(async (tx) => { -+ const user = await tx.orm.public.User.create({ email, displayName, kind: 'user' }); ++ const user = await tx.orm.public.User.create({ email, displayName }); + const post = await tx.orm.public.Post.create({ title, userId: user.id }); + return { user, post }; + }); @@ -452,12 +454,11 @@ The callback form does something the array form never could: one query's result ### `withTransaction(runtime, callback)` -A transaction helper you import directly. Use it when you hold a runtime but no client. +A transaction helper you import directly. `db.transaction(...)` is built on it. Use it directly in a helper or a library that is handed a runtime rather than a client. #### Remarks -- Imported from `@prisma/orm-postgres/family-runtime`. -- The callback receives a transaction handle whose only methods are `query` and `execute`. Unlike `db.transaction(...)`'s `tx`, it has no `.orm` or `.sql`. Build queries with the client's `db.sql` and run them through `tx.query(...)` for rows or `tx.execute(...)` for writes that return no rows. +- Imported from `@prisma/orm-postgres/family-runtime`. The callback receives a transaction handle whose only methods are `query` and `execute`. Unlike `db.transaction(...)`'s `tx`, it has no `.orm` or `.sql`, so only running queries goes through `tx`. Building them still needs a SQL builder, either the client's `db.sql` or one passed in to your helper. - Commits on return, rolls back on throw, the same as `db.transaction(...)`. #### Options @@ -480,7 +481,7 @@ A transaction helper you import directly. Use it when you hold a runtime but no ```typescript import { withTransaction } from '@prisma/orm-postgres/family-runtime'; -const runtime = await db.connect(); // opens the connection and returns the runtime +const runtime = db.runtime(); await withTransaction(runtime, async (tx) => { await tx.execute(db.sql.public.tag.insert([{ id: crypto.randomUUID(), label: 'wt-commit-1' }]).build()); await tx.execute(db.sql.public.tag.insert([{ id: crypto.randomUUID(), label: 'wt-commit-2' }]).build()); @@ -492,6 +493,7 @@ await withTransaction(runtime, async (tx) => { ```typescript import { withTransaction } from '@prisma/orm-postgres/family-runtime'; +const runtime = db.runtime(); try { await withTransaction(runtime, async (tx) => { await tx.execute(db.sql.public.tag.insert([{ id: crypto.randomUUID(), label: 'wt-rollback' }]).build()); @@ -508,9 +510,8 @@ The lowest level: acquire a connection, open a transaction on it, commit or roll #### Remarks -- `runtime.connection()` returns a dedicated connection. `connection.transaction()` opens a transaction on it. Run built queries with `transaction.query(...)` for rows or `transaction.execute(...)` for writes that return no rows, then call `transaction.commit()` or `transaction.rollback()`. -- Always `release()` the connection when done, to return it to the pool. -- `connection.destroy(reason)` throws that connection away instead of returning it to the pool. It does not close the pool or the runtime. The runtime opens a replacement connection on the next query. Use `release()` for the normal path and `destroy()` only to discard a connection you no longer trust. `reason` is optional and the pool never reads its value, so plain `destroy()` is enough. +- `runtime.connection()` returns a dedicated connection. `connection.transaction()` opens a transaction on it. Run built queries with `transaction.query(...)` for rows or `transaction.execute(...)` for writes that return no rows, then call `transaction.commit()` or `transaction.rollback()`. Always `release()` the connection when done, to return it to the pool. +- `connection.destroy()` throws that connection away instead of returning it to the pool. It does not close the pool or the runtime. The runtime opens a replacement connection on the next query. Use `release()` for the normal path and `destroy()` only to discard a connection you no longer trust. #### Options @@ -520,7 +521,7 @@ The lowest level: acquire a connection, open a transaction on it, commit or roll | `connection.transaction()` | none | Returns a `Promise` of a transaction on that connection. | | `transaction.commit()` / `transaction.rollback()` | none | Commit or discard the transaction. | | `connection.release()` | none | Return the connection to the pool. | -| `connection.destroy(reason)` | `Error`, optional | Evict this connection from the pool. | +| `connection.destroy()` | none | Evict this connection from the pool. | #### Examples @@ -575,8 +576,8 @@ Prepare a statement off a runtime. #### Remarks -- The declaration maps each parameter name to a type id, for example `{ label: 'pg/text@1' }`. A type id is the `codecId` your contract gives that column, so read the ids out of your `contract.json`. See [The contract artifact](/orm/contract-authoring/the-contract-artifact). -- The callback takes one argument, the declared `params`, and returns the built query. Build it with a SQL builder you already hold, such as `db.sql`. +- The declaration maps each parameter name to a type id, for example `{ label: 'pg/text@1' }`. A type id is the PostgreSQL type plus `@1`, such as `pg/int4@1`, `pg/uuid@1`, `pg/bool@1`, or `pg/timestamptz-temporal@1`. It is called `codecId` in code. To take one straight from a column, read `db.sql.public.user.columns.id`. See [`fns.raw` and `.returns()`](/orm/reference/sql-query-builder#fnsraw-and-returns). +- The callback takes one argument, the declared `params`, and returns the built query, which is what `.build()` gives you and what the tables below call `SqlQueryPlan`. Build it with a SQL builder you already hold, such as `db.sql`. In the `where((f, fns) => ...)` calls below, `f` holds the table's columns and `fns` holds the comparison functions. - A declared parameter that the callback never uses is rejected at prepare time with an error whose `code` is `RUNTIME.PREPARE_UNUSED_PARAM`, with `details.unused` listing the offending names. The rejection happens at `prepare()`, before any execution. - The resulting `PreparedStatement` runs via `ps.query(target, params)`; see [`PreparedStatement.query`](#preparedstatementquerytarget-params). @@ -657,7 +658,7 @@ Run a prepared statement against a target, with the parameter values. #### Remarks -- A query that returns rows prepares into a `PreparedStatement`, which you run with `query(...)`. A raw SQL statement ending in `.affectedCount()` prepares into a `PreparedExecution` instead, which you run with `execute(target, params)`. That call resolves to `{ affectedRows }`, the number of rows the statement changed: +- A query that returns rows prepares into a `PreparedStatement`, which you run with `query(...)`. A statement written as raw SQL with `db.raw.sql` and ending in `.affectedCount()`, which asks for a row count instead of rows, prepares into a `PreparedExecution` instead. You run that with `execute(target, params)`, and it resolves to `{ affectedRows }`, the number of rows the statement changed. See the [raw queries reference](/orm/reference/raw-queries): ```typescript const archive = await runtime.prepare({}, () => @@ -714,16 +715,15 @@ Per-query options for cancellation and scope. #### Remarks -- `signal` is an `AbortSignal` for per-query cancellation. A signal that is **already aborted** when you call `query(...)` short-circuits before any query runs, rejecting with an error whose `code` is `RUNTIME.ABORTED` and whose `details.phase` is `'stream'`. The error's `cause` is the signal's reason, exactly as you passed it to `controller.abort(...)`. -- An abort that lands after rows have started arriving ends the stream with the same `RUNTIME.ABORTED` error. `details.phase` says where it landed: `'stream'` while rows are being read, `'encode'` while a parameter is being encoded, and `'decode'` while a returned value is being decoded. -- `scope` is `'runtime'`, `'connection'`, or `'transaction'`. It is the label [middleware](/orm/middleware/how-middleware-works) sees for the query, and the runtime sets it for you, so do not set it yourself. +- `signal` is an `AbortSignal` for per-query cancellation. A signal that is **already aborted** when you call `query(...)` rejects before any row is fetched, with an error whose `code` is `RUNTIME.ABORTED` and whose `details.phase` is `'stream'`. The error's `cause` is the signal's reason, exactly as you passed it to `controller.abort(...)`. +- An abort that lands after rows have started arriving ends the stream with the same `RUNTIME.ABORTED` error. `details.phase` says where the abort landed. +- There is also a `scope` option. The runtime sets it for you so that [middleware](/orm/middleware/how-middleware-works) can see whether the query ran on the runtime, a connection, or a transaction. Do not pass it yourself. #### Options | Name | Type | Required | Description | |---|---|---|---| | `signal` | `AbortSignal` | No | Per-query cancellation signal. | -| `scope` | `'runtime' \| 'connection' \| 'transaction'` | No | The label middleware sees for this query, which the runtime sets for you. | #### Examples @@ -745,9 +745,8 @@ Read telemetry about the most recent query. #### Remarks -- PostgreSQL only. `telemetry()` does not exist on the MongoDB runtime. -- `telemetry()` returns `null` on a freshly-connected runtime, before any query has run. -- After a query, it returns an object of the shape `{ lane, target: 'postgres', fingerprint, outcome, durationMs? }`. `lane` names which API built the query, such as `orm-client` for `db.orm` or `raw` for `db.raw.sql`. `fingerprint` is a hash of the query text with the literal values replaced, so every run of the same query shares one fingerprint. `outcome` is `'success'` or `'runtime-error'`. The object reflects only the **most recent** query, not a running history. +- PostgreSQL only. `telemetry()` does not exist on the MongoDB runtime. Read it after a query to log how long that query took and whether it succeeded. +- `telemetry()` returns `null` on a freshly-connected runtime, before any query has run. After a query, it returns an object of the shape `{ lane, target: 'postgres', fingerprint, outcome, durationMs? }`. `lane` names which API built the query, such as `orm-client` for `db.orm` or `raw` for `db.raw.sql`. Every run of the same query text shares one `fingerprint`, whatever values you pass, so you can group runs of one query together. `outcome` is `'success'` or `'runtime-error'`. `durationMs` is how long the query took, and the runtime records it for every query it times. The object reflects only the **most recent** query, not a running history. #### Return type @@ -770,8 +769,8 @@ const after = runtime.telemetry(); ### `AsyncIterableResult` -`all()`, `createAll()`, and the runtime's `query(...)` return an `AsyncIterableResult`. `all()` and `query(...)` read rows, and `createAll()` writes rows and returns the ones it wrote. `await` the result to collect an array, or `for await` it to take rows one at a time. +[`all()`](/orm/reference/orm-client#all), [`createAll()`](/orm/reference/orm-client#createall), and the runtime's `query(...)` return an `AsyncIterableResult`. `all()` and `query(...)` read rows, and `createAll()` writes rows and returns the ones it wrote. `await` the result to collect an array, or `for await` it to take rows one at a time. -On PostgreSQL, a result you hold or read slowly does not keep a pooled connection checked out, because a query run through the client loads every row into memory first and hands its connection back before you read them. A connection or transaction you opened yourself is unaffected: it stays yours until you release or commit it. +`for await` does not save memory on PostgreSQL: every row is loaded first. The upside is that a result you hold or read slowly does not keep a pooled connection checked out, because the client hands the connection back before you read the rows. A connection or transaction you opened yourself is unaffected: it stays yours until you release or commit it. -Each result is read once, one way. Re-`await`ing a result you already awaited is safe and returns the same array. Switching between `await` and `for await`, or looping a second time with `for await`, throws an error whose `code` is `RUNTIME.ITERATOR_CONSUMED`. For the full single-consumption rules, shared identically by PostgreSQL and MongoDB, see [`AsyncIterableResult`](/orm/reference/orm-client#asynciterableresult) in the ORM client reference. +Pick `await` or `for await` for a given result and do not mix the two. Re-`await`ing a result you already awaited is safe and returns the same array. Switching between `await` and `for await`, or looping a second time with `for await`, throws an error whose `code` is `RUNTIME.ITERATOR_CONSUMED`. For the full rules on reading a result, shared identically by PostgreSQL and MongoDB, see [`AsyncIterableResult`](/orm/reference/orm-client#asynciterableresult) in the ORM client reference. diff --git a/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part1.round2.md b/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part1.round2.md new file mode 100644 index 00000000000..2d168541b44 --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part1.round2.md @@ -0,0 +1,101 @@ +# Reader review: `pipeline-builder.part1.mdx` + +Reading as a Prisma ORM 7 user, first time on Prisma 8, no source access. + +## Sentences I could not restate after one reading + +**1. Line 86, the contract paragraph.** Too many new ideas in one place. + +> "Your contract is the `contract.prisma` file that replaced `schema.prisma`. Running `npx prisma contract emit` compiles it into two generated files: `contract.json`, read at run time, and `contract.d.ts`, read by TypeScript." + +What stopped me: "contract" is a noun I have never seen, and the page uses it as if I know it. I know `schema.prisma`. I do not know what "your contract" is, why the file was renamed, or whether "contract" means the file, the generated JSON, or a concept. Plainest wording I would have understood: "Prisma 8 renames `schema.prisma` to `contract.prisma`. Run `npx prisma contract emit` to generate `contract.json` (used at run time) and `contract.d.ts` (used for types)." Then never use the bare word "contract" to mean something else. + +**2. Line 137, the decoding bullet.** + +> "Only `match()`, `sort()`, `limit()`, `skip()`, `sample()`, `project()`, `addFields()`, and `vectorSearch()` keep that." + +What stopped me: "keep that." I had to scroll back up to work out that "that" means the decoding described in the bullet above. The bullet before it is a bolded paragraph, so the link is not obvious. Plainer: "Decoding only survives these stages: ... After any other stage, including `lookup()` and `group()`, documents come back raw." + +**3. Line 159, the match() comparison rule.** + +> "The value you pass is compared exactly as you wrote it, with no conversion into the stored type." + +What stopped me: "conversion into the stored type" made me stop and build a mental model of encode/decode that the page has not yet given me. Plainer: "Prisma sends your value to MongoDB unchanged. It does not turn a string into an `ObjectId`, a date, or anything else." + +**4. Line 643, the redact workaround.** + +> "Write `f.rawPath('$KEEP')` instead. The builder adds one more `$`, so the server sees `$$KEEP`." + +What stopped me: `rawPath` is introduced here and nowhere else. I do not know what a "path" is in this API, why a method named "rawPath" is the way to express a constant, or whether the one-extra-`$` rule applies anywhere else. This reads as a bug I am being asked to work around, and I cannot tell whether it will keep working. + +**5. Line 691, the raw-expression rule for option-object stages.** + +> "Build a raw expression with the `fn.*` helpers and pass its `.node` property, or name a field with `MongoAggFieldRef.of('price')`." + +What stopped me: `.node`. Nothing on the page says what `.node` is or when I need it. It appears again in the `redact()` example on line 669 — `fn.eq(f.kind, fn.literal('tutorial')).node` — where the first argument has `.node` and the two `rawPath` arguments do not. After one reading I could not state the rule for when to add `.node`. + +**6. Line 692.** + +> "The stages you pass to `facet` and `unionWith` are classes too: `MongoMatchStage`, `MongoCountStage`, `MongoSortStage`, `MongoLimitStage`, and the rest." + +What stopped me: "too" (too, as opposed to what?) and "and the rest." There is no list of the rest and no link to one. I cannot write a `facet` with a `$group` in it from this page. + +## Words and phrases I had to guess + +- **"contract"** — guessed: the schema file, renamed. +- **"root"** (`from(root)`, "Unknown root") — guessed: the collection name. The page says this on line 95, but uses "root" in the signature before explaining it. +- **"plan"** — every example writes `const plan = ....build()`, while line 127 calls the result "a built query." Guessed these are the same thing and the variable name is arbitrary. +- **"decoded"** (line 136) — guessed: Prisma converts driver values into the types my schema declares. +- **`fn`, `acc`, `expr`** — guessed: three import namespaces for expressions, accumulators, and "wrap this as an aggregation expression." `acc` is explained at line 478. `fn` is never introduced; it just appears in the line 194 example. +- **`fn.literal(2023)`** — guessed: I must wrap plain constants so the builder does not read them as field paths. The page never says why I cannot pass `2023`. +- **`.node`** — guessed: the underlying expression object inside a helper's wrapper. Low confidence. +- **`default_`** (line 737, trailing underscore) — guessed: `default` is a reserved word in JavaScript, so the key was renamed. The page does not say, and I would have typed `default` and been confused. +- **`@@type("mongo/string@1")`, `@@discriminator(kind)`, `@@base(Post, "article")`** — guessed from the prose on line 21. The `@1` version suffix I could not guess at all. +- **"aggregation-time writes with `$out` / `$merge`"** (line 11) — guessed: stages that write results back to a collection. +- **`mongo(...)`** (line 86) — guessed: the Prisma 8 replacement for `new PrismaClient()`. + +## Places I asked "so what do I actually type?" + +1. **Line 98, the client-free builder.** "passing the `contract.json` you import." I do not know how to import a JSON file in my project, or what path it is at. There is no code block here, and every other entry point has one. + +2. **Line 117.** The first example uses `db` and `runtime` with no import lines and no `mongo(...)` call, just a link. The rest of the page has import lines. This is the one example where I most need them. + +3. **Lines 199–203, the `_id` warning.** This tells me a thing does not work and names two escape hatches, neither with code. `rawCommand()` links forward to a section I do not have. `db.orm.posts.where({ _id })` is a fragment, not runnable. "Fetch one post by id" is the single most common thing I do, and after reading this section I still cannot type it. + +4. **Lines 683–687.** Six stages given as bare parameter lists and nothing else: + > "`geoNear({ near, distanceField, spherical?, maxDistance?, minDistance?, query?, key?, distanceMultiplier?, includeLocs? })`." + + No types, no descriptions, no example. `near` also appears in the line 691 list as needing a raw expression, so I cannot even guess its shape. Same for `graphLookup`, `setWindowFields`, `densify`, `fill`, `bucketAuto`. + +5. **Line 458, the `unwind()` example.** "This example assumes a `tags String[]` field on `Post`, which the example schema above does not have." So the one example I would copy cannot be run against the schema the page gave me. + +6. **Line 734, `bucket()`.** `groupBy: MongoAggFieldRef.of('duration')` — but `duration` lives on `Tutorial`, not `Post`, and I am aggregating `'posts'`. Whether that is legal, and whether `MongoAggFieldRef.of` accepts any string or only fields of the root model, is not stated. + +7. **Line 669, `redact()`.** The example passes `.node` on one argument and not the others. I would not know which way to write my own. + +8. **Line 527, the ORM 7 migration diff.** The `+` line calls `acc.count()` with no import in the diff. Small, but this is the block a v7 user copies first. + +## Places that explain the machinery when I only wanted the instruction + +- **Line 131:** "`build()` and `aggregate()` are two names for the same method. This page uses `build()`." I only need to be told which one to type. +- **Line 137:** the whole decoding bullet is an explanation of Prisma's internal encode/decode behavior. What I need is the instruction: "after `lookup()` or `group()`, compare ids with `String(a._id) === String(b._id)`." That instruction is there, but buried under the mechanism. +- **Line 643:** "The builder adds one more `$`, so the server sees `$$KEEP`." I do not need to know what the builder adds. I need "write `f.rawPath('$KEEP')`." +- **Line 97:** "If a method is missing from autocomplete, you have already chained past it." This explains why my editor behaves a certain way rather than telling me the rule, which is: call `insertOne`/`insertMany` directly on `from()`, before any stage. +- **Line 13:** "Where a method has not been verified against a running MongoDB, the Remarks say so." This tells me about the state of the documentation team's testing. As a reader I read it as "some of this page may be wrong," which makes me trust the parts with no such note less, not more. + +## Could I do what the page is for, after one reading? + +Partly. + +**Yes:** filter, sort, page, sample, project, add computed fields, join with `lookup()`, group and count, promote a sub-document. The stage sections from `match()` through `sortByCount()` each have a runnable example with imports, and I could copy them and change field names. + +**No, I would still not know:** + +- **How to get `db` and `runtime` in the first place.** The page assumes them from line 117 onward and hands the setup to two links. +- **How to fetch one document by its id.** The page tells me it is impossible through this builder and does not show me the working alternative in code. +- **What `fn` is, what it contains, or when a value needs `fn.literal(...)` or `.node`.** `fn` is used in six examples and defined in none of them. The page says the definition is in "Expression helpers," which is on the other half. +- **How to use any of the option-object stages except `unionWith`, `facet`, and `bucket`.** The other six are a list of parameter names. +- **Which `Mongo*Stage` classes exist and what their constructors take.** "and the rest" is not enough to write a `facet`. +- **Whether the Atlas stages work.** Line 753: "None of the three has been verified against a running Atlas deployment." + +One structural note: the page's opening says it "documents every stage, accumulator, expression helper, and write method." Reading only this half, I hit forward links to `#expression-helpers` and `#rawcommand` four times before the page ends, and two of those (the `_id` workaround and the `fn` namespace) are things I need to finish a basic task, not advanced extras. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part2.round2.md b/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part2.round2.md new file mode 100644 index 00000000000..85087cce09b --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part2.round2.md @@ -0,0 +1,119 @@ +Read once, top to bottom, as an ORM 7 user. + +## 1. Sentences I could not restate after one reading + +**Line 132** — the `.node` rule. This is the worst sentence block on the page. +> "Every `fn.*` call returns a wrapper object. Its `.node` property is the raw expression inside that wrapper. `match()` takes the wrapper itself, through `expr(...)`. `fn.cond()` and the stages that take an options object take `.node`." + +What stopped me: three ideas about three different call sites, plus an unresolvable reference. "the stages that take an options object" — which stages are those? I cannot name one. Is `project()` one? The example at line 151 passes a bare `fn.add(...)` into `project()`, with no `.node`, so apparently not — but I had to infer that from an example instead of from the rule. There are three cases (bare wrapper, `.node`, `expr(...)`) and the page states two. + +Plainest wording I'd have understood: a table. "Pass the `fn.*` value as-is inside `project()`, `addFields()` and accumulators. Pass `.node` as the first argument of `fn.cond()` and inside `f.stage.set()`. Wrap it in `expr(...)` only inside `match()`." + +**Line 137 vs the example at line 178** — these contradict each other. +> "**Inside `fn.cond()`, pass `.node`.** Write `fn.cond(fn.eq(a, b).node, thenExpr, elseExpr)`." + +The example writes `fn.cond(fn.eq(...).node, fn.literal('is-tutorial'), fn.literal('not-tutorial'))`. Only the first argument gets `.node`; the other two do not. So "inside `fn.cond()`, pass `.node`" is false as written. I read it twice and still had to work the rule out from the code. Say: "The condition argument of `fn.cond()` takes `.node`. The then and else arguments do not." + +**Line 117** — +> "Five have a different name because the plain one was taken" + +Taken by what? `fn.first` does not appear anywhere. `acc.first` exists, but that is a different import on a different object. I cannot resolve "taken", so I cannot check my own guesses about names. + +**Line 117, second half** — +> "Each helper is the camelCase name of the MongoDB aggregation operator without its `$` prefix, so `$toUpper` is `fn.toUpper`" + +This only helps someone who already knows MongoDB's aggregation operators by name. I have used Prisma 7 for two years and have never written `$dateTrunc`. The table at 121–130 is names only, with no argument list, so for anything except the four or five obvious ones I still do not know what to type. + +**Line 136** — +> "and the rest of the date, regex, replace, and object helpers." + +I cannot restate this because it does not tell me anything. Which helpers, and what named arguments does each take? `fn.dateDiff` takes an object — with what keys? The page never says. + +**Line 33** — +> "Wherever a signature takes `n`, wrap the number: write `n: fn.literal(2)`, not `n: 2`." + +`fn` has not been introduced yet. It is defined 80 lines later, at line 111. Same problem at line 59, where `fn.year()` appears in an accumulator example before `fn` exists. Move the `fn` section above accumulators, or say "`fn.literal` comes from the expression helpers below". + +**Line 235** — +> "Pipeline form calls `f.stage.*` and can read other fields of the same document." + +`f.stage` is not explained until line 382. On first reading I did not know whether `stage` was a literal property name or a placeholder for something. It is literal, which I only learned later. + +**Line 381** — +> "There are fourteen, listed under the bullets." + +I looked for a bulleted list. There is a table. "Listed in the table below" is what I needed. + +**Line 246** — +> "The builder does not check it against your contract, and each value is stored exactly as you pass it, with no conversion to the type your contract gives that field." + +I understand the words. I do not understand the consequence, which is the only part I care about. My contract says a field is a date. What do I pass? A JS `Date`? An ISO string? If I pass the string, does the read side then break because "decoded" expects a real date? The page states the rule and stops exactly where my question starts. Same for line 246's tacked-on "`match()` filter values follow the same rule." + +**Line 303** — +> "its `_id` is a raw `ObjectId` even though the TypeScript type says otherwise." + +Says otherwise how? I guessed the type claims `string`. The page never says what the type claims, so I cannot predict what will break. + +**Line 191** — +> "Dates and ids are raw driver values." + +"Raw driver value" is not defined. I guessed: a `Date` object and an `ObjectId` object from the `mongodb` package. If that guess is right, say it in those words. + +**Line 33, last sentence** — +> "`acc.first()` and `acc.last()` only mean something when a `sort()` comes before the `group()`." + +What happens if I forget? An error? A silently arbitrary value? "Mean something" is doing a lot of work. I would have understood: "Without a `sort()` before the `group()`, the document MongoDB calls first is arbitrary and can change between runs." + +## 2. Words and phrases I had to guess + +- **`f.kind`** (lines 86, 178, 203) — guessed it is the field that says whether a `Post` is an `Article` or a `Tutorial`. Part 1 may have said so; the field just appears here. +- **`_id: null`** in every `group()` example — guessed it means "one group containing every document". Never stated on this page. +- **"the plain one was taken"** — guessed there is an internal name clash I cannot see. +- **"streams the pipeline output"** (line 436) — guessed "streams" means nothing operationally different from "writes", and is just describing `$merge`. +- **"result objects, not documents"** (line 245) — guessed that `runtime.query()` on a write returns a one-element array holding that object. I only worked this out because every example destructures with `const [result] =`. The page never says why a single write result comes back inside an array. +- **`acc.bottom`, "the reverse sort order"** (line 27) — guessed it means the reverse of the `sortBy` I supply. Could also have meant a fixed descending default. +- **`MongoFieldFilter.eq('kind', 'tutorial')`** (line 203) — guessed it builds a filter for a raw stage. Nothing on the page explains this class. +- **"Top `n` values by value"** for `maxN` (line 24) — "by value" reads like a typo or a tautology. Guessed it means the n largest. +- **"terminals"** in the anchors `#read-terminals` and `#pipeline-write-terminals-out-and-merge` — internal vocabulary that leaks into URLs. Readers will see these in a link. + +## 3. Places I asked "so what do I actually type?" and got no answer + +1. **`expr()` is never imported.** It is named at lines 111, 132, 137, 138 as the thing that makes `match()` work with computed values, and no example on the page uses it, imports it, or shows its shape. This is one of the page's three main rules and it has zero runnable code. +2. **Argument shapes for most `fn.*` helpers.** The table gives 70-odd names. Five get an argument shape at line 136. For `fn.dateDiff`, `fn.dateTrunc`, `fn.slice`, `fn.zip`, `fn.range`, `fn.getField`, `fn.convert`'s `to` values, and the rest, I have a name and nothing else. +3. **`insertOne` and required fields.** Line 255 passes `bio: null` and `address: null` explicitly. Must I list every field, including nullable ones? Must I pass `_id`? The page shows one shape and states no rule. +4. **Dates and ObjectIds on insert.** Following from line 246 — what literally goes in a date field or a relation id field? This is the first thing anyone hits. +5. **Building raw stages for `pipe()`.** Line 190 says "such as `new MongoMatchStage(...)`". If I want a raw `$lookup` or `$redact` stage, what class do I construct and with what arguments? The one example is the one stage I would never need `pipe()` for, since `match()` is typed. +6. **`pipe(stage)`** — what do I put in `NewShape`? An interface? No example. +7. **`fn.toObjectId()` on a real field.** Line 139 says to "give `fn.toObjectId()` an expression taken from the `f` argument instead". No example shows it. The only id example (line 488) sidesteps into `rawCommand()`. +8. **`whenMatched` as "an array of update stages"** (line 437) — what does one of those look like? Is it `f.stage.*`, or raw `$set` objects? Not shown. +9. **`acc.top` return shape.** The table says "Single document". The example comment says "output is what you get back" and names the variable `newestTitle`, so it returns a title string, not a document. Which is it? These two lines in the same page disagree. + +## 4. Places the page explains internals when I only wanted to know what to do + +- **Line 132, "Every `fn.*` call returns a wrapper object. Its `.node` property is the raw expression inside that wrapper."** I do not want to know the builder has wrapper objects with a raw expression inside. I want a rule for where to type `.node`. The internals are offered in place of the rule. +- **Line 139, "because a literal carries no field type to convert from."** This explains why the library cannot do it. It does not help me, and the sentence before it already told me it throws. +- **Line 437, "The builder passes both to MongoDB unchecked."** What I need is the consequence: a misspelled `whenMatched` value fails at the database, not in TypeScript. +- **Line 361, "the unknown key is ignored and the default `'after'` applies"** — this one earns its keep, because it tells me the failure is silent. Contrast with the two above. +- **Line 218, "`aggregate()` is an alias: it returns the identical query and runs identically."** Fine, but I am left asking why both exist and which one I should write. The page does not recommend one. + +## 5. Structure problem + +`### pipe()` (line 184) sits at heading level 3 under `## Expression helpers` (line 109). `pipe()` is a pipeline stage, not an expression helper. In a rendered sidebar it will appear nested under expression helpers. It belongs beside the other stages, which are in part 1. + +Also, `## Accumulators` jumps straight to `#### Examples` at line 35, skipping level 3. + +## After one reading, could I do what the page is for? + +Partly. I could write a `group()` with `count`, `sum`, `avg`, `min`, `max`, `first`, `last`, `push`, `addToSet`. I could write a basic `updateMany`, `deleteMany`, `insertOne`, `out()`, and `merge()`, and copy the `rawCommand()` `_id` filter. Those have complete examples and I would type them correctly. + +What I still would not know: + +- Where `expr()` comes from, or how to write a single `match()` filter that uses a computed value. This is presented as core behaviour and has no code at all. +- Whether to write a bare `fn.*` value, `.node`, or `expr(...)` at any call site not shown in an example. The stated rule and the shown examples do not agree. +- What arguments any date, regex, array, set, or conversion helper takes. I have names only. +- What to pass for a date or an id when inserting, which is the first thing I would do. +- How to build any raw stage for `pipe()` other than the match stage shown. +- Whether `acc.top` gives me back a document or a single value. +- How to convert a string id from my own code without dropping to `rawCommand()`. The page says an expression from `f` works and never shows it. + +The write-methods section (lines 227–457) is the strongest part: the array rule at 231–236 is stated once, firmly, with the error codes, and every method has a runnable example. The expression-helpers section is the weakest: it is a name list plus a rule I cannot apply. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/raw-queries-and-index.round2.md b/docs/orm-docs-audit/c21-orm-reference/raw-queries-and-index.round2.md new file mode 100644 index 00000000000..00ad4b1e189 --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-reference/raw-queries-and-index.round2.md @@ -0,0 +1,169 @@ +# Reader review: `index.mdx` and `raw-queries.mdx` + +I read both once, top to bottom, as a Prisma 7 user with no knowledge of this version. + +--- + +## Page 1: `/orm/reference/index.mdx` + +### Sentences I could not restate after one reading + +**1.** "The **pipeline builder** is at `db.query`. It builds MongoDB aggregation pipelines." + +What stopped me: the name. `db.query` sounds like "run a query." Two lines later the page writes `runtime.query(plan)`, which is a *different* `query`. I now have `db.query` (build a pipeline) and `runtime.query` (run a plan) and the page never says they are unrelated. I guessed they were the same thing. + +Plainer: "The pipeline builder is at `db.query`. It builds MongoDB aggregation pipelines. It is not the same as `runtime.query()`, which runs a built query." + +**2.** "In Prisma ORM 7 you passed `findMany({ where, include })` arguments; those arguments are chained methods now, and [Coming from Prisma ORM 7] maps each one." + +What stopped me: "those arguments are chained methods now" compresses the single biggest change in the product into half a sentence, buried at the end of a six-sentence paragraph. This is the sentence I most needed and it is the one I nearly skipped. + +Plainer: "In Prisma ORM 7 you wrote `findMany({ where, include })`. Now each of those arguments is a method you chain: `.where(...).include(...)`. See [Coming from Prisma ORM 7]." + +**3.** "The raw queries page uses prose instead." + +Could not restate: "prose instead" of what, exactly — instead of all four parts, or instead of the headings? It also turns out to be wrong: the raw queries page *does* have a `Remarks` heading under `findOneAndUpdate()`. One page contradicting the other on my second click is bad. + +### Words I had to guess + +- **"a projection across two tables"** — I guessed "picking some columns from a join." The page uses "projection" as if I already write SQL that way. +- **"today it works on PostgreSQL only"** — I guessed "MongoDB support is planned." "Today" is doing work a plain "currently, PostgreSQL only" would do without the wink. +- **`db.orm.public.User`** vs **`db.sql.public.user`** — capital `User`, lowercase `user`, in two adjacent lines of the same code block. I guessed one is the model and one is the table. The page never flags the casing difference, and this is exactly the kind of thing that costs a reader twenty minutes. + +### "So what do I actually type?" + +**The code block does not run.** + +```ts +const rows = await runtime.query(plan); +``` + +`runtime` is never created. Nothing above it produces a `runtime`. I know `db.runtime()` exists, but the page does not use it, so I do not know whether to write `const runtime = db.runtime()`, `await db.runtime()`, or something else. This is the first code on the first page of the section and I cannot copy it. + +**Why does one line need `.build()` and the other does not?** + +```ts +const admins = await db.orm.public.User.where(...).all(); +const plan = db.sql.public.user.select(...).build(); +const rows = await runtime.query(plan); +``` + +The ORM client is awaited directly. The SQL builder needs `.build()` and then a separate run step. That is a large, load-bearing difference between the two APIs and the page shows it without naming it. I was left guessing whether `.build()` is optional. + +### Internals where I wanted instructions + +Nothing serious. This page mostly stays on "what to do." + +### Could I do what the page is for? + +Partly. I can pick which of the three APIs to use — that part works, and it is the page's main job. I could not copy the example and run it, and I would not know how to get a `runtime`. + +--- + +## Page 2: `/orm/reference/raw-queries.mdx` + +### Sentences I could not restate after one reading + +**1. The warning block (line 14).** This is one paragraph of twelve sentences covering: what a type id is, which API converts values, what `.returnsRow` does, what `.returns` does not do, why `numeric` is a string, why dates are not, and then the entire MongoDB BSON story. I read it before I knew what `fns.raw`, `db.raw.sql`, `.returns()`, or `.returnsRow()` were, because the page has not introduced any of them yet. + +I could restate no part of it after one reading. It also forward-references: "which is why the `upperEmail` example below is a plain string" — I have not seen `upperEmail`. + +This should be two short warnings, each placed next to the thing it warns about: one in the PostgreSQL section, one in the MongoDB section. + +**2.** "A type id such as `pg/text@1` tells Prisma ORM how to turn the database's bytes into a JavaScript value." + +Then later: `.returns(codecId)`, `param(value, { codecId })`, `'pg/text@1'`, and "codec" in the error message. **The page calls the same thing three names — "type id", "codecId", "codec" — and never says they are the same thing.** I guessed they were, but only because the values looked identical. Pick one word. + +**3.** "On a built raw query, `.returns` is a property: a record of the columns that query declared. That is a different thing from the `.returns(codecId)` method you call on a fragment." + +This contradicts its own example. In the code, `authorsWithPosts` ends at `.returnsRow({...})` — there is **no `.build()`** — and then `authorsWithPosts.returns.postCount` is read off it. So `.returns` is a property on something that is *not* built. Either the sentence is wrong or the example is. I could not tell which. + +**4.** "`where()`'s callback returns a boolean expression, the same thing the comparison helpers such as `fns.eq(...)` return. A raw fragment declared `.returns('pg/bool@1')` is one, so it satisfies `where()` on its own, with no `fns.eq(...)` wrapper." + +"is one" — one *what*? I had to go back two clauses to find "a boolean expression." + +Plainer: "A raw fragment that declares `.returns('pg/bool@1')` is itself a boolean expression, so you can pass it straight to `where()` without wrapping it in `fns.eq(...)`." + +**5.** "`select()` has three forms, and the two used on this page are the aliased form below and the object form further down." + +This tells me a count and two names, defines neither, and points at code I have not read. It is navigation instructions, not information. I would cut it and just link `select()`. + +**6.** "TypeScript rejects an options object that names `sort` or `returnDocument`, and an untyped value that names either is dropped with no error at run time." + +Two failure modes in one sentence, and "an untyped value" is not a thing I recognise. I guessed it means "an options object typed as `any`." + +Plainer: "TypeScript rejects `sort` or `returnDocument` in the options object. If you get past the type check — say, by casting to `any` — those keys are silently ignored at run time." + +**7.** "Two of those are `_id` equality filters and `$redact` with `$$KEEP` / `$$PRUNE`." + +"Two of those" — of what? The antecedent is "anything the typed pipeline builder can't express," two sentences back and phrased as a mass noun. Plainer: "Two examples: filtering by `_id` equality, and `$redact` with `$$KEEP` / `$$PRUNE`." + +### Words and phrases I had to guess + +- **"escape hatch"** — guessed: "the way out when the normal API can't do it." Used three times and never defined. It is jargon, but survivable. +- **"the short list below"** (line 52) — guessed it meant the list of inferred types in the `param()` section, which is *four sections later* and is about something else (what Prisma picks automatically), not a list of type ids I may write. +- **"relational-core"** in `@prisma/orm-postgres/relational-core/expression` — guessed this is an internal package name I just have to type. Same for **"query-ast"** in `@prisma/orm-mongo/query-ast/execution`. Both read like directories someone forgot to hide. +- **"a raw collection with nine methods"** — guessed "an object." I counted the nine myself to check. +- **"a write yields a single result object as its one row"** — guessed: "the result comes back wrapped in an array of length 1." Only the `const [result] = ...` in the example made this click. + +### "So what do I actually type?" + +**1. Where is the list of type ids?** The page shows me `pg/text@1`, `pg/bool@1`, `pg/int8@1`, and `pg/timestamptz-temporal@1`. That last one appears at line 125 with no introduction. There is no link to a complete list. When I need `numeric`, or `jsonb`, or `uuid`, I have nowhere to look. This is the single biggest gap on the page. + +**2. `.returns()` with a column instead of a string.** "For a type id that is not in the short list below, take the type from the column instead: `db.sql.public.user.columns.id`." Do I write `.returns(db.sql.public.user.columns.id)`? The page never shows it. `.returnsRow()` takes columns, so I assume `.returns()` does too — but I am guessing at a compile error. + +**3. How do I create `db` for MongoDB?** PostgreSQL gets a full setup block: import, `postgres({...})`, `await db.connect()`. The MongoDB half of the page gets nothing. I do not know the import path, the constructor call, or the options. Half the page is unrunnable. + +**4. `db.connect()` vs `db.runtime()`.** "On PostgreSQL you get the runtime from `await db.connect()`. On MongoDB you get it from `await db.runtime()` instead." So does `db.runtime()` work on PostgreSQL or not? Does `db.connect()` exist on MongoDB? The word "instead" implies they are alternatives for the same job, which means I now have two names for one thing and no rule for choosing. + +**5. Does `execute()` return a number or an object?** Line 158: "`runtime.execute(...)`, which answers with the number of rows the statement changed." Line 170: `// stats.affectedRows`. Those are two different return types. I cannot tell whether to write `if (stats > 0)` or `if (stats.affectedRows > 0)`. + +**6. Do I interpolate the built query or the unbuilt one?** "A raw query that returns rows can go inside another one." The example interpolates `authorsWithPosts`, which stops at `.returnsRow()`. Everywhere else on the page I am told to call `.build()` before using something. So: `${authorsWithPosts}` or `${authorsWithPosts.build()}`? The example says the former; the surrounding prose implies the latter. + +**7. What do I do when I need a dynamic column name?** "Do not build a column or table name from user input with a template; `fns.raw` cannot parameterize an identifier." Correct and important — and then the page stops. I have a real query with a user-chosen sort column. The page tells me what is forbidden and offers no alternative. + +**8. Are `RawFindOneAndUpdateCommand`'s trailing arguments optional?** `new RawFindOneAndUpdateCommand('users', filter, update, true, { count: -1 }, 'after')` is six positional arguments with a bare `true` in the middle. If I only want `returnDocument: 'after'` and no sort, do I pass `undefined` for sort? `null`? The page shows one call and no signature. + +**9. `tx` is undefined.** "You can also run a built raw query inside a transaction: `tx.query(...)` and `tx.execute(...)`." Where does `tx` come from? No example, no link on that sentence. + +**10. The upsert counter contradicts the MongoDB rule.** Line 216 says "Raw filters hold native BSON values, so the examples below build ids with the driver's `ObjectId` class." Then the counter example writes `const filter = { _id: 'pageViews' }` — a plain string `_id`, no `ObjectId`. Is that allowed, or a mistake? I could not tell. (The same example also names a page-view counter `db.raw.collection('users')`, which made me reread it twice looking for the point.) + +**11. Why does `_id` filtering need `rawCommand()`?** "Filtering by `_id` works here because a real `ObjectId` in a raw pipeline document matches." But `aggregate()` also takes raw pipeline documents. So why can't I filter `_id` there? The page implies a difference and does not name it. + +### Places the page explains internals when I wanted instructions + +**1. The whole type-inference paragraph (line 86).** + +> "A whole `number` within the 32-bit signed range, `-2147483648` to `2147483647`, becomes `pg/int4@1`. A larger whole number becomes `pg/int8number@1`, up to JavaScript's safe integer limit of `9007199254740991`. Every other number, including any fractional one, becomes `pg/float8@1`. A `bigint` becomes `pg/int8@1`, a `string` becomes `pg/text@1`, a `boolean` becomes `pg/bool@1`, and a `Uint8Array` becomes `pg/bytea@1`." + +Seven mappings and two magic constants in one prose block. This is a table, not a paragraph. And what I actually came for is one sentence: *"Interpolate plain numbers, strings, booleans, bigints, and `Uint8Array` directly. For anything else — a `Date`, a decimal you need as `numeric` — wrap it in `param(value, { codecId })`."* Give me that first, then the table for when I need it. + +**2.** "Writing `FROM "user"` with no schema works because PostgreSQL resolves the name through its search path; `public."user"` is the explicit form." + +This explains PostgreSQL to me, in the middle of an example about `.returnsRow()`. I did not ask. + +**3.** "Every example is transcribed from an executable test suite that runs against a live database." + +This is about how the docs are produced. It tells me nothing I can act on. Cut it. + +**4.** "Because `returnDocument` is never sent, the driver applies its own default, which is the document as it was before the update." + +I am being told the mechanism of a limitation. What I need is the rule: "`findOneAndUpdate()` always returns the document as it was *before* the update. To get the version after, use `db.query.rawCommand(...)`." The page does say this, just after two sentences of plumbing. + +**5.** "In plain JavaScript there is nothing to reject it, so the tagged template throws as soon as you call it." + +Borderline. The error code and message are useful. The explanation of *why* TypeScript can't help is not. + +### Could I do what the page is for? + +**PostgreSQL: mostly yes.** After one reading I could write a `fns.raw` fragment in a `select()`, use one as a `where()` predicate, and run a whole `db.raw.sql` statement with `.returnsRow()`. The examples are concrete and the shapes are clear. + +What I would still not know: +- Which type id to write for any column type the page did not happen to show. +- Whether `runtime.execute()` gives me a number or an object. +- Whether to `.build()` a subquery before interpolating it. +- What to do about a dynamic column name. + +**MongoDB: no.** I cannot create the client — the page never shows how. Every MongoDB example starts from a `db` that does not exist on this page. I could read the nine method signatures and understand what each does, but I could not run one. + +**The one thing that would help most:** the page assumes I read it in order, then repeatedly forward-references ("the short list below", "the counter below shows what that costs you", "the object form further down"). Reference pages get read by jumping to a heading. Every section should stand on its own. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part1.round2.md b/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part1.round2.md new file mode 100644 index 00000000000..1246a2ea9e6 --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part1.round2.md @@ -0,0 +1,97 @@ +Read once, top to bottom, as a Prisma 7 user. + +## Sentences I could not restate after one reading + +**1. Line 131 (the whole "Entry points" paragraph).** It is one block of ~10 sentences that switches topic four times: how to build a query, what a contract is, what `prisma contract emit` produces, how table names are derived, and what `public` means. Quoting the worst of it: + +> "The client reads your contract, the `contract.prisma` file that replaced `schema.prisma`. Running `npx prisma contract emit` compiles the contract into the two files every example here imports: `contract.json`, read when your app runs, and `contract.d.ts`, read by TypeScript." + +What stopped me: "contract" is a brand-new word introduced mid-paragraph with no heading of its own, and the sentence explains what each file is *for internally* when all I need is "run this command before you build queries." Plainer: "Before you can build queries, run `npx prisma contract emit`. It writes `contract.json` and `contract.d.ts`, which the examples below import." + +**2. Line 131, last sentence.** + +> "`public` is the PostgreSQL schema, the namespace your tables live in unless you set another one." + +I can restate it, but "unless you set another one" is a dead end — the page never says where you set it or what you write instead. Plainer: name the frontmatter/schema setting, or drop the clause. + +**3. Line 135.** + +> "`db.runtime()` is the connection that runs a built query. On PostgreSQL it is synchronous, so do not await it." + +Two ideas, and the second is about the tool's internals. Then three lines later the examples `await runtime.query(plan)`. After one reading I was unsure whether "do not await" applied to `query()` too. Plainer: "`db.runtime()` returns the connection. Do not `await` it — only `runtime.query()` is awaited." + +**4. Lines 149–155.** + +> "The `sql(...)` function from `@prisma/orm-postgres/builder/runtime` builds the same query builder around your own database client instead, from that client's `db.context`" + +Three unexplained things in one sentence: what "your own database client" means when the example still passes `db.context` (so it *is* the Prisma client?), what `db.context` is, and `rawCodecInferer: { inferCodec: () => 'pg/text@1' }` — which appears in code and is never mentioned in prose at all. I could not restate any of this. I would cut the whole block or give it a section that says who needs it and why. + +**5. Line 187–189.** + +> "The first is `f`, which has one property per column the query can name at that point." + +"at that point" is doing a lot of work and the thing it refers to ("scope") is defined in the *next* paragraph. Plainer: put line 189 first, then define `f` in terms of it. + +**6. Line 207.** + +> "A computed expression's result type comes from `.returns(...)` on `fns.raw`, or from the operation's own declared return type. A type id such as `pg/int4@1` is the PostgreSQL type plus a version, always `@1` today. [Raw queries] shows the ones a bare JavaScript value gets, and you can take one from a column: `db.sql.public.user.columns.id`." + +Three sentences, four ideas, and the third one I genuinely could not parse. "shows the ones a bare JavaScript value gets" — the ones *what*? Type ids, presumably, but "a bare JavaScript value gets" a type id is backwards from how I think about it. And "you can take one from a column" — take a *type id* from a column? Does `db.sql.public.user.columns.id` evaluate to the string `'pg/uuid@1'`, or to something else I pass to `.returns()`? Unresolved. + +**7. Line 346.** + +> "After the join, a column name that both tables have must be written under its table, such as `f.post.id` or `f.post.createdAt`. A bare `select('id')` or `orderBy((f) => f.createdAt, ...)` throws, because a name both tables have is not in the query's scope on its own." + +The second sentence restates the first and then adds a reason that contradicts line 286, which told me that after *any* join I address columns by table name. So: is table-prefixing always required after a join, or only for names both tables share? The page says both. + +**8. Line 343.** + +> "Not available on SQLite, where the call does not compile. If you reach it anyway, it throws an error whose `code` is `ORM.CAPABILITY_MISSING`" + +"If you reach it anyway" — how? If it does not compile, what is the path where it runs? I guessed "if you ignore the TypeScript error or use JavaScript," but the page does not say. Same wording at line 448. + +## Words and phrases I had to guess + +- **"table-level, SQL-shaped methods"** (line 9) — guessed: "methods that look like SQL clauses and start from a table rather than a model." +- **"facet"** — in the anchor `[#the-dbsql-facet]` (line 133). The word appears nowhere in the prose. Guessed it is leftover internal vocabulary; it will show up in the URL people share. +- **"plan"** (line 145) — the variable name. `build()` is described as returning "the built query," but the variable is called `plan` everywhere. I guessed they are the same thing. `build()` itself is only defined in part 2, and I am told to call it ~20 times before that. +- **"Project a row down to"** (line 193) — guessed "reduce each row to." +- **"scope"** (line 189) — I guessed "the set of column names TypeScript will let you write," but I did not know whether violating it is a compile error or a runtime error until line 201 told me it is neither, it is a throw at call time. +- **`outerLeftJoin`** — guessed it is SQL's `LEFT OUTER JOIN`. The name is reversed from SQL, and the page never says so. +- **`rawCodecInferer` / `inferCodec`** (line 154) — no guess. I do not know what a codec is here. +- **`db.context`** (line 154) — guessed "some internal handle on the connection." +- **`SelectQuery`** — guessed it is a type name I never import, only see in tables. +- **`lateral`** (line 344) — "The callback receives a `lateral` builder." I guessed it is a table-less starting point that you point at a table with `.from()`. + +## Places I asked "so what do I actually type?" + +1. **Installing and importing.** Line 131 sends me to "Coming from Prisma ORM 7" for "the packages to install and the commands to run." On a reference page that opens with `import postgres from '@prisma/orm-postgres/runtime'`, I wanted one line naming the package. +2. **`import type { Contract } from './contract.d'`** — where do `contract.json` and `contract.d.ts` land relative to my source? The import path `'./contract.d'` (with the `.d`, no extension) is unusual enough that I would have copied it wrong. +3. **Setting a schema other than `public`.** Told it is possible, not told how. +4. **Getting a type id for `.returns()`.** I need `pg/int4@1` for every `fns.raw`. There is no list on this page and the pointer to it (line 207) is a sentence I could not parse. +5. **Ordering nulls.** Line 395 tells me `nulls` is accepted by TypeScript and silently does nothing. It does not tell me what to do instead — presumably `fns.raw` in `orderBy`, but the page does not say. +6. **`select()` replacing columns.** "Calling `select()` a second time never replaces the first call's columns." So how do I drop a column I already selected? Not answered. +7. **`db.sql.public.tag.insert([{ id, label: 'typescript' }])`** (line 165) — `id` is an undeclared variable. The `Uuid @id @default(uuid())` in the schema made me expect I could omit it. Do I have to generate ids myself with `db.sql`? +8. **Inside `lateralJoin`, where does `f.user.id` come from?** The example (line 371) uses `f.post.userId` and `f.user.id` in the same callback. The Remarks say "The subquery can filter on the outer row's columns," but never say that the outer table's columns appear on the same `f` under its own table name. I inferred it from the example, not the prose. +9. **Is a built query reusable?** Can I hold `plan` and run it twice, or with different parameters? Not addressed. + +## Places explaining internals when I wanted instructions + +- **Line 345:** "a chain with `.as(...)` on the end throws `subquery.getRowFields is not a function`." That is a JavaScript stack trace from inside the library. I only needed "do not call `.as()` here" — which the sentence already said twice before this. +- **Line 207:** "A type id such as `pg/int4@1` is the PostgreSQL type plus a version, always `@1` today." Tells me how the identifier is constructed; does not tell me where to get the right one. +- **Line 131:** the `contract.json` / `contract.d.ts` split — "read when your app runs" vs "read by TypeScript" is the library's internal division of labour. I need to run one command. +- **Line 448:** "`DISTINCT ON` is a PostgreSQL extension to SQL." True, and irrelevant to what I do. +- **Line 19:** the schema block carries `types { Embedding1536 = pgvector.Vector(1536) }`, `@@type("pg/text@1")`, `@@discriminator(type)`, and `@@base(Task, "bug")` — four features I have never seen, in the first code block, none of which any example uses. The page tells me to ignore `Address`, `Task`, `Bug`, and `Feature`, but not the syntax around them. + +## Could I do what the page is for, after one reading? + +Partly. I could write a `select` with `where`, `orderBy`, `limit`, `offset`, an `innerJoin`, an `outerLeftJoin`, and a `.as()` subquery, by copying the examples and changing table and column names. Those sections are clear and each has a runnable example. + +What I would still not know: + +- **How to get to a working `db` at all.** Everything on the page hangs off `db.sql.public`, and the one paragraph that creates `db` sends the install and setup steps to another page while spending its own words on what the two generated files are for. I would have stopped at line 131 and gone looking elsewhere. +- **What `build()` actually returns and whether I can reuse it.** The page's central verb is defined in a half I do not have. +- **How to write any `fns.raw` expression**, because I cannot get a type id for `.returns()` and every computed-column example needs one. +- **Whether table-prefixing after a join is always required or only for ambiguous names.** Lines 286 and 346 disagree. +- **What the second entry point (`sql(...)`, `rawCodecInferer`) is for,** or whether I am supposed to care. +- **The SQLite story.** The note promises SQLite is supported, one sentence says drop `public`, and then every example and two "PostgreSQL only" remarks are Postgres. I would not know which of the SELECT methods I can use on SQLite without testing each one. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part2.round2.md b/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part2.round2.md new file mode 100644 index 00000000000..af0430a136c --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part2.round2.md @@ -0,0 +1,86 @@ +Read once, top to bottom, as a Prisma 7 user seeing this version for the first time. + +## Sentences I could not restate after one reading + +**Line 38 (the aggregate note).** "`count()`, `sum()` over an integer column, and `avg()` over an integer column all return a JavaScript `number`. If a `count()` or integer `sum()` result is larger than 2^53 - 1, the query fails with an error whose `code` is `RUNTIME.DECODE_FAILED`. Use `countBigInt()` or `sumBigInt(field)` instead when totals can get that large. `avg()` does not fail this way. It is computed as a floating-point `number` and loses precision instead, so use `avgDecimal(field)` when you need an exact figure. `min()` and `max()` return the input column's own type." +Six rules in one paragraph before I have seen a single aggregate call. I stopped at "the exact form of" in the heading, "with exact variants beside them" — I did not know what a "variant" was until I hit the function table 300 lines later. Plainer: put this note *after* the first `count()` example, and split it into "these return `number`", "these overflow, use the BigInt version", "`avg` loses precision, use `avgDecimal`". + +**Line 62.** "Every example from here on calls `runtime`, and names the built query `plan`." +Two things stopped me. You do not "call `runtime`" — you call `runtime.query()`. And why is the built query named `plan`? Nothing on the page calls it a plan; the page calls it "the built query" everywhere else, and then the anchors give the game away: `[#executing-a-plan]`, `[#recover-the-row-type-from-a-plan]`. I guessed "plan" is an older name for the same thing and that I am seeing a half-finished rename. Plainer: "Every example below assumes `const runtime = db.runtime()`." + +**Line 86–87.** "`// rows === [{ yearPlaced: '2024', orderCount: 10 }]` / `// yearPlaced is the string '2024', not the number 2024. See `.returns()` below.`" +I annotated the expression `'pg/int4@1'` and got a string. I could restate the words but not the rule. Then line 403 confirms `.returns()` "does not change the value you actually get back" and points me *back* to this example. The two places point at each other and neither tells me how to get a number. + +**Line 399.** "Required: Yes for a selected value, a compared value, or a fragment used on its own as a `where()` predicate". +That is a list of every use of `fns.raw` I can think of. So when is `.returns()` *not* required? The page never says. Plainer: say "always required, except X". + +**Line 469.** "`postgres(...)` turns cursors off in the driver it builds, so every query loads every row into memory first." +`postgres(...)` never appears anywhere on this page. "turns cursors off in the driver it builds" is a sentence about the inside of the tool. I could not restate what I am supposed to *do* with it. + +**Line 471.** "Each result is read once, one way. Re-awaiting a result you already awaited is safe and gives you the same array." +These contradict each other on first read. "Read once" then "re-awaiting is safe". Plainer: "You can `await` a result as many times as you like. You cannot mix `await` and `for await` on the same result, and you cannot `for await` it twice." + +**Line 191.** "The column it goes into decides how it is stored and read, so you never annotate an insert value." +"Annotate" with what? I had not met `param()` or `.returns()` yet at this point in the page. + +**Line 3.** "Grouping starts with `groupBy()`, which turns a query into a `GroupedQuery`." +But in every example `groupBy()` comes *after* two `.select()` calls. Does the order in the chain matter? Not said. + +## Words and phrases I had to guess + +- **`plan`** — guessed: the object `.build()` returns, same thing the page elsewhere calls "the built query". Two names for one thing. +- **"These are fixed:"** (line 361) — guessed: these functions exist no matter which database you use, unlike the aggregates below. The word "fixed" does not say that. +- **"in scope"** (line 90, "neither a column in scope nor an alias you selected") — guessed: a column of the table I started from. +- **`codecId`** (line 331) vs "type id" (part 1) vs `.returns(codecId)` — guessed these are all the same thing. The page uses two names and never joins them up. +- **"typed to resolve to the returned rows"** (line 236) — guessed: TypeScript now knows the query gives back rows, so I should use `query` not `execute`. +- **`relational-core`** and **`components/runtime`** in the two import paths — guessed these are just package subpaths with no meaning I need. +- **`db.orm.public.User`** (line 450, capital U) against `db.sql.public.user` (lowercase) everywhere else — guessed the ORM side keys by model name and the SQL side by table name. The page does not say so, and the capital letter looked like a typo. +- **"exact form of"** (`countBigInt`, `sumBigInt`, `avgDecimal`) — guessed: the version that does not lose precision. + +## Places I asked "so what do I actually type?" and got no answer + +1. **`exists` and `notExists`.** Listed in the table at line 368. No signature, no example, nothing. Do they take a subquery? Another builder? I cannot write one. +2. **`in` and `notIn`.** Same. Listed, never shown. Do I pass an array? +3. **`ilike`** (line 384). Named, never shown. Is it `fns.ilike(f.email, '%@example.com')`? +4. **Getting a number out of a raw expression.** I now know `.returns('pg/int4@1')` lies to TypeScript and I still get `'2024'`. The page never shows the fix. Do I write `CAST(... AS int4)` in the raw SQL? `Number(row.yearPlaced)`? Say it. +5. **Choosing a `codecId`.** `param()` requires one and its only example is `'pg/text@1'`. What do I pass for a date, a uuid, a boolean, an array? The list is linked from `fns.raw` at line 392 but not from the `param()` section, which is the section that forces me to pick one. +6. **`having()` with an alias.** `orderBy()` takes the alias `'totalAmount'` (line 164). `having()` re-writes the whole `fns.sum(f.amount)` (line 121). Can `having()` take the alias too? Not said, so I would have retyped the aggregate and hoped. +7. **`update()` mixing forms.** The table gives a values object *or* an expression callback. Can I set one column to a literal and another to an expression in the same callback? Not said. +8. **`distinct()` and `distinctOn()` on a grouped query** (line 153). I do not know what distinct means once rows are already one-per-group, and there is no example. +9. **`execute` appears at line 205** in the first insert example, with no explanation. It is defined at line 435, 230 lines later. On first read I did not know why insert used `execute` and the `returning()` example used `query`. +10. **`1n`** (line 98). "Do not pass a `bigint` literal such as `1n`." What happens if I do — a type error, a wrong result, a crash? I would want to know whether this is a compile-time safety net or a trap. +11. **`sum(field)` over a non-integer column** (line 377): "the type PostgreSQL's own `sum` produces for it". So for a `numeric` column I get… a string? A `Decimal`? I would have to go read PostgreSQL docs to use my own schema. +12. **`build()` and defaults** (line 182): "The value is generated when you call `build()`." So if I build once and run twice, do I insert the same uuid twice? That is a real question the sentence raises and does not answer. + +## Places explaining the inside when I only wanted to know what to do + +- **Line 469**, the whole "turns cursors off in the driver it builds" paragraph. I wanted to know whether `for await` saves memory. The answer is no. Say that in one sentence and drop the driver. +- **Line 182**, "The value is generated when you call `build()`." I wanted to know that leaving a column out applies its default. +- **Line 224**, "On a database without it, TypeScript rejects the call. If you get past the type check, for example by casting, the call throws an error whose `code` is `ORM.CAPABILITY_MISSING`." I wanted "works on PostgreSQL and SQLite". The part about defeating your own type check is a curiosity, not instruction. It also never names the database that lacks `RETURNING`. +- **Line 90**, "Prisma ORM does not check that every other selected column is grouped or aggregated. PostgreSQL rejects the query when it runs." Useful in effect, but framed as a division of labour between two systems. Plainer: "If you select a column you did not group or aggregate, the query fails when it runs, not when you build it." +- **Line 450**, "Since `8.0.0-rc.10` it also works on ORM queries". A release note in the middle of a reference entry. +- **Line 425**, "There is no `build({ params })` form." Answers a question I did not ask, unless it is aimed at people migrating from something. + +## Two structural problems + +**The schema switches back without telling me.** Line 5 says "The examples below use a separate `customer` / `order` schema". Every mutation example from line 204 on is back to `db.sql.public.tag` and `db.sql.public.user` from part 1. Nothing marks the switch back, and I spent a moment looking for a `tag` model in the `customer`/`order` schema. + +**Line 147's comment describes code that is not there.** "`// avgAmount is 300, and avgDecimal(f.amount) would give the exact '300.0000000000000000'`" — the example uses `avg`, and the comment tells me about a function the example does not call. + +## Could I do what the page is for, after one reading? + +Partly. + +I could write a grouped query with a count or a sum, filter it with `having()`, sort by an aggregate alias, and take the top row. I could insert rows, update with a values object, delete with a `where()`, and add `returning()`. I could pick `query` versus `execute`. That is the bulk of the page and it works. + +What I still would not know how to do: + +- Write an `exists`, `notExists`, `in`, `notIn`, or `ilike` call. The page names five functions it never shows. +- Get a usable number back from a computed expression. I know `.returns()` will not do it and the page stops there. +- Pick a `codecId` for anything that is not text or `int4`. +- Decide whether `having()` accepts an alias, or whether I must retype the aggregate. +- Mix fixed values and expressions in one `update()`. +- Say what `distinct()` means on a grouped query. +- Tell whether rebuilding versus reusing a built `insert()` gives me a new uuid. + +And I would still be carrying the unexplained word "plan" through the whole page, including into two heading anchors. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part1.round2.md b/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part1.round2.md new file mode 100644 index 00000000000..186af5217b1 --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part1.round2.md @@ -0,0 +1,89 @@ +I read the page once, top to bottom, as a Prisma 7 user. + +## Sentences I could not restate after one reading + +**Line 23** — "Your contract is the `contract.prisma` file that replaced `schema.prisma`. Running `npx prisma contract emit` compiles it into two files: `contract.json`, read at run time, and `contract.d.ts`, read by TypeScript." + +This is the single most important fact on the page and it is buried in a bullet under `postgres(options)`. I stopped at "Your contract is" — the page says "the contract" seven times before this and never says what a contract is. Plainer: put this before the first heading, as its own short section: "Prisma 8 replaces `schema.prisma` with `contract.prisma`. Run `npx prisma contract emit` to generate `contract.json` and `contract.d.ts` in `src/prisma/`. Every client you create needs `contract.json`." + +**Line 25** — "A binding tells the client which database to use. Pass exactly one of `url`, `pg`, and `binding`, or leave all three out and pass one to [`connect()`](#connect). The `binding` form is the same three choices in one object: `{ kind: 'url', url }`, `{ kind: 'pgPool', pool }`, or `{ kind: 'pgClient', client }`." + +Three ideas in one bullet: what a binding is, when you may omit it, and a fourth option that repeats the first three. I could not work out why `binding` exists at all if `url` and `pg` already do the job. Plainer: "Tell the client which database to use in one of three ways: `url`, `pg`, or `binding`. Use `binding` when your code chooses between them at run time, because it is one object instead of three separate option names." + +**Line 28** — "Prisma ORM keeps a record in your database of which contract the database was set up for. With `verifyMarker: 'onFirstUse'`, the default, the first query checks that record, logs a warning if it is missing or does not match, and runs the query anyway." + +I do not know what "a record" is, where it lives, or what "the database was set up for" means. "Marker" appears only in the option name, never in the prose, so I cannot connect `verifyMarker` to "a record". The behaviour is also self-cancelling: it checks, warns, and runs anyway. So what do I do when I see the warning? Plainer: "Migrations write a marker row into your database recording which contract it matches. By default the first query compares that marker with your contract and logs a warning on a mismatch, then runs anyway. A warning usually means you have not run your migrations." + +**Line 96** — "Every error carries a `code`: to handle one, catch the error and compare `error.code` with the code's string." + +"the code's string" stopped me. Which string? Compare with what? Plainer: "Every error has a `code` property. Catch the error and compare `error.code` with the value shown here, for example `if (error.code === 'DRIVER.NOT_CONNECTED')`." Show the line of code. + +**Line 270** — "On MongoDB, `runtime()` returns a **`Promise`**: you must `await` it. The connection to MongoDB is opened on first use, which is why this call is asynchronous." + +The reason does not hold. Line 29 says PostgreSQL also opens its connection on first use, and line 121 says PostgreSQL's `runtime()` is synchronous anyway. So the stated reason explains nothing, and I now distrust both sentences. Either give the real reason or drop it and just state the difference. + +**Line 285** — "`(await db.runtime()).query(built)` runs any built MongoDB query, including one built by the [pipeline builder](/orm/reference/pipeline-builder) (`db.query`). The client itself has no `execute()` method." + +Two unrelated facts. The second one names something that does not exist, without saying what to use instead in the same sentence. I had to read line 287 to learn `runtime.execute` is the answer. + +**Line 187** — "That is what lets you use one `MongoClient` both through Prisma ORM and in code you write against the `mongodb` package directly." + +Follows three sentences about who closes what. By the time I reached "That is what lets you", I had lost which fact "that" points back to. + +## Words and phrases I had to guess + +- **"runtime"** — I guessed "the connected object you actually run queries on". The page confirms this at line 89, which is after the word has been used eleven times including in the page title. Define it in the intro. +- **"binding"** — I guessed "the database connection details". Never stated plainly. +- **"built query"** / **"`.build()`"** (line 90) — I guessed you chain builder calls and finish with `.build()` to get a plain object. No example of a chain appears before it is used at line 112. +- **"`postgres`"** — I guessed `Contract` is the exported type from `contract.d` and the generic is what makes `db.sql.public.tag` typed. The page never says the generic is required or what happens if you leave it out. +- **"`.nativeEnums` holds the enum types defined in PostgreSQL itself"** (line 54) — I could not guess how this differs from `.enums`, or when I would use it. No example. +- **"`.context` is internal: the query builders read it, and you do not call it"** — I guessed it is an implementation detail I should ignore. Then why is it in a reference list I am reading? +- **"`mode`: `'strict'` or `'permissive'`. Passed through to your middleware, which decides what to do with it"** (line 202) — I guessed nothing. This option does literally nothing on its own and the description says so. It tells me neither what to pass nor why. +- **"`middleware`: Array of middleware"** (lines 43, 201) — the type restates the name. I still do not know the shape of one item. +- **"marker"** — see above. +- **"`.sql` is the SQL query builder, keyed by table name"** vs **"`.orm` holds your models by model name"** — I guessed `Tag` the model becomes `tag` the table. Only a code comment at line 111 tells me. On MongoDB the same rule is in another code comment at line 300. A naming rule this important should not live in comments. + +## Where I asked "so what do I actually type?" + +1. **`connect()` with a binding.** Lines 95 and 246 both say "`connect()` accepts a binding, so a client you created without one can be given its database here." There is no example anywhere on the page. Is it `db.connect({ url })` or `db.connect({ kind: 'url', url })`? This is the only way to use the documented "leave all three out" path, and it is never shown. +2. **Middleware.** Both `middleware` rows link away. One two-line example of a middleware function would cost less than the link. +3. **`mode`.** No example, no meaning, no guidance. +4. **`binding` on MongoDB.** Line 184 says the choices are `{ kind: 'url', url, dbName }` or `{ kind: 'mongoClient', client, dbName }`, but the text right above offers `uri` as a separate option. If I am on the `uri` path, what do I put in `binding`? Unanswerable from the page. +5. **`url` plus `dbName` on MongoDB.** Line 192 says "Pass exactly one of `url`, `uri` plus `dbName`, `mongoClient` plus `dbName`, and `binding`." The very next example (line 223) passes `url` **and** `dbName`. I read the rule as forbidding that. Line 185 explains the override, but the rule and the example contradict each other on the page as written. +6. **`extensions`.** Line 27 shows the import and the array. It does not say to run `npm install @prisma/orm-extension-pgvector` first. I would have hit a missing-module error. +7. **Closing the Mongo runtime.** Line 253 says `MongoRuntime` is "(`query`, `execute`, and `close` only)". The PostgreSQL runtime has five calls and none is `close`. So on MongoDB I have `db.close()` and `runtime.close()` and the page never says which one I should call, or what the other does. +8. **`runtime.connection()`.** Line 92 says it "takes one connection out of the pool". It does not say how I give it back, or whether it does that for me. The link points to a section I do not have, but the one-line summary invites the question and leaves it open. +9. **What replaces `prisma.user.findMany()`.** As a v7 user this is my first question on any v8 page. The link at line 23 is the right answer, but it sits at the end of a bullet about file names. Put it in the intro. + +## Where the page explains the machinery when I only wanted to know what to do + +- **Line 28, `verifyMarker`.** I do not need to know Prisma writes a record in my database. I need to know: leave the default on, and if you see the warning, run your migrations. +- **Line 55, `.context` is internal: the query builders read it, and you do not call it.** Telling me how the builders talk to each other. Cut it, or cut the whole entry. +- **Line 270, "which is why this call is asynchronous."** An internal reason that contradicts line 29 and line 121. +- **Line 208, "Prisma ORM's `MongoClient` type, not the driver's."** This is a real trap, so it belongs somewhere — but the example at lines 228 to 235 imports the driver's `MongoClient` and assigns Prisma's to `db`, with both names live in one ten-line file, and never comments on it. Either name the type differently or warn me in the example. +- **Line 26, pool defaults.** "The pool it creates uses `connectionTimeoutMillis: 20000` and `idleTimeoutMillis: 30000`." The numbers are useful in the options table, where they already are. In the prose they are just internals. + +## Other things I noticed + +- **Line 297.** The heading is "Run a query built by the pipeline builder" but the anchor is `#run-a-pipeline-builder-plan`. The word "plan" does not appear anywhere on the page. Anyone who lands on that link expects a different section. +- **Line 210 vs line 300.** Line 210 says `.orm` "holds your models by model name" and shows `db.orm.users`. Line 300 says the collection name is "your model name with a lowercase first letter". If my model is `User`, is the ORM key `user` or `users`? The example says `users`, the rule says `user`. One of these is wrong, and I cannot tell which. +- **Line 9.** "Transactions are fully supported on PostgreSQL. They are not available on MongoDB." As a v7 user who uses `$transaction` on MongoDB today, this is alarming and the page defers the whole answer to a section I do not have. One sentence naming the replacement would stop me panicking. + +## Could I do what the page is for, after one reading? + +Partly. + +**I could:** create a PostgreSQL client from a connection string, run one `select`, and close it. The four examples at lines 62, 109, 147 and 166 are clear, complete, and copy-pasteable. Same for the basic MongoDB client. This is the page's main job and it does it. + +**I could not:** + +- Create a client without a binding and supply the database later at `connect()`. No example exists. +- Write a middleware, or decide anything about `mode`. +- Work out whether `db.orm` keys are singular or plural, because the page says both. +- Decide whether to call `runtime.close()` or `db.close()` on MongoDB. +- Work out what `binding` is for on MongoDB when I am using `uri`. +- Know whether `postgres` is required or optional. +- Know what to do when the `verifyMarker` warning fires. +- Know how `.nativeEnums` differs from `.enums` in practice. + +The largest single fix is moving the contract explanation from line 23 into the introduction. Everything on the page depends on `contractJson`, and a reader who does not already know what a contract is spends the whole first section not understanding the first argument of the first function. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part2.round2.md b/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part2.round2.md new file mode 100644 index 00000000000..4ab9be491da --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part2.round2.md @@ -0,0 +1,88 @@ +Read once, top to bottom, as a Prisma 7 user. + +## Sentences I could not restate after one reading + +**1. Line 3 + line 109 + line 114 — a direct contradiction.** +> "Use `withTransaction(...)` when you hold a runtime but no client." + +then + +> "Build queries with the client's `db.sql` and run them through `tx.query(...)`" + +Both examples use `db.sql`. So the case the section tells me it is for — no client — is the one case the examples show is impossible. I do not know when I would ever be holding a runtime and no `db`. Plainest version I would have understood: say the actual situation (a helper that receives a runtime as an argument?) and show it without `db`, or drop the claim. + +**2. Line 11 — too many ideas in one bullet.** +> "`tx.enums` holds the members of the enums declared in your contract, and `tx.nativeEnums` holds the members of the PostgreSQL enum types in your database." + +Two new APIs, never shown, never used again. I do not know what "holds the members" gives me — an array? an object of string constants? Why two, and which one do I want? No example anywhere on the page. + +**3. Line 14.** +> "When a function you call opens its own transaction today, pass `tx` down to that function instead." + +"today" made me stop — today as in "in your current Prisma 7 code"? Plainest: "If a helper you call starts its own transaction, change it to take `tx` as a parameter instead." And then the obvious follow-on is missing: what TypeScript type do I annotate that parameter with? Not stated. + +**4. Line 232.** +> "A type id is the `codecId` your contract gives that column, so read the ids out of your `contract.json`." + +Three stops. "codecId" is a word I have never seen. Part 1 told me the contract is `contract.prisma`; this says `contract.json` — I do not know what that file is, where it comes from, or whether I wrote it. And "read the ids out of" is not an instruction I can follow without seeing one line of that file. + +**5. Line 371.** +> "A signal that is **already aborted** ... rejecting with an error whose `code` is `RUNTIME.ABORTED` and whose `details.phase` is `'stream'`." + +If it "short-circuits before any query runs", why is the phase `'stream'`? That reads like a bug or an internal quirk, and I could not restate why. + +**6. Line 429.** +> "a query run through the client loads every row into memory first and hands its connection back before you read them" + +I can parse it, but it contradicts what I thought `for await` was for. If every row is already in memory, streaming buys me nothing on PostgreSQL. The page never says that plainly, and never warns me about large result sets. + +**7. Line 431.** +> "Each result is read once, one way. Re-`await`ing a result you already awaited is safe and returns the same array." + +First sentence says once; second says twice is fine. I had to read it three times to work out the rule is really "pick `await` or `for await` and never mix". + +## Words and phrases I had to guess + +- **`family-runtime`** (line 113, import path) — guessed it is just a subpath name with no meaning I need. Nothing tells me. +- **`SqlQueryPlan`** (lines 242, 288) — guessed this is the type of what `.build()` returns, i.e. "the built query". The page uses two names for one thing and never links them. +- **`R`** in `(tx) => Promise` (line 24) — guessed "whatever type your callback returns". +- **`kind: 'user'`** (line 99) — appears only in the migration diff, with no counterpart in the Prisma 7 code above it. Guessed it is a required column in some example model. It made me think the new API required a field I did not have. +- **`(f, fns)`** in `where((f, fns) => fns.eq(f.label, params.label))` — guessed fields and functions. Never said on this page. +- **`db.raw.sql`** and **`.affectedCount()`** (line 318) — guessed raw SQL, and that `.affectedCount()` marks it as a write. Both appear for the first time inside a code sample. +- **`all()` / `createAll()`** (line 427) — guessed model methods from the ORM page. They arrive with no introduction. +- **`lane`** (line 404) — guessed it means "which of the several query APIs you used". + +## Where I asked "so what do I actually type?" + +1. **The type id.** `'pg/text@1'` is the only one on the page. I have an `int`, a `timestamptz`, a `boolean`, a `uuid`. I cannot guess their ids and the page points me at a file it never shows. +2. **`db.connect()` vs `db.runtime()`.** Line 137 does `const runtime = await db.connect()`; line 184 does `const runtime = db.runtime()`. Line 281 says calling `connect()` after `prepare()` fails. Nowhere does the page say which one I should write in my own code, or why. +3. **Bounding a transaction with a timeout.** Line 13 tells me to pass a `signal` to every `tx.query` and `tx.execute`. No example on the page does it. I wanted one line showing `AbortSignal.timeout(...)` inside a `db.transaction`. +4. **Isolation level.** Line 12 says the options are gone. It does not say whether I can get `SERIALIZABLE` at all — e.g. by running `SET TRANSACTION ISOLATION LEVEL` as the first statement. I have production code that needs it and the page leaves me stuck. +5. **Passing `tx` to a helper.** Told to do it (line 14), never shown, and no type given. +6. **MongoDB.** Line 220: "share one `MongoClient` between Prisma ORM and your driver code and use a driver session". No code, and no hint how a `MongoClient` gets shared with the Prisma client. +7. **`tx.enums` / `tx.nativeEnums`.** Mentioned, never typed out. +8. **Line 146 example** uses `runtime` that is not declared in that snippet. I would have copied it and got an undefined variable. +9. **Retrying.** "call `db.transaction(...)` again yourself" — on which error codes? A serialization failure has a code; the page does not say which one, so I cannot write the retry it tells me to write. + +## Where it explains the insides when I only wanted to know what to do + +- **Line 167:** "`reason` is optional and the pool never reads its value, so plain `destroy()` is enough." Then the parameter should not be in the table (line 177) at all. Telling me an argument is ignored is internals. +- **Line 373 / 380:** `scope` is documented as an option, with a type, in the options table — and then "the runtime sets it for you, so do not set it yourself." If I must not pass it, it is not an option. +- **Line 404:** `fingerprint` is "a hash of the query text with the literal values replaced". That is how it is built, not what I do with it. Nothing on the page tells me why I would read telemetry at all, or when `durationMs` is missing. +- **Line 429:** the pooling/buffering explanation, above. +- **Line 372:** `details.phase` of `'encode'` / `'decode'` tells me where inside the driver the abort landed. I cannot act differently on any of them. + +## Could I do what the page is for, after one reading? + +Partly. I could write `db.transaction(async (tx) => ...)` and port a `$transaction([...])` array — the diff at line 93 is the clearest thing on the page, and line 105 explains the payoff well. + +I could not: + +- Write a prepared statement, because I cannot find a type id for any column I actually have. +- Decide whether to use `db.connect()` or `db.runtime()`. +- Port any transaction that relied on `isolationLevel` or `timeout`, which is most of mine. +- Write the retry loop the page tells me to write, because it never names the error code to retry on. +- Use `withTransaction` with any confidence, because its stated purpose contradicts both its examples. +- Use `tx.enums`, `tx.nativeEnums`, or `telemetry()` for anything, having been told they exist and nothing else. + +The single biggest hole is the type id. It is the one input I cannot invent, and the page answers it by naming a file it never shows. \ No newline at end of file From d5fa39ece6f6602abaf3b8e333413a8a3c180c25 Mon Sep 17 00:00:00 2001 From: willbot Date: Sun, 13 Sep 2026 09:17:43 +0200 Subject: [PATCH 07/14] docs(orm8): orm/reference pages, reader-review round three applied Flat line budget, cutting preferred. Corrections that change what the pages say, each looked up in rc.10: in() and notIn() take an unbuilt subquery, the same as exists(); the type-id rule is pg/ plus the type name plus @1; every table is in public today; one non-listed pipeline stage anywhere makes the whole result raw; the abort phases are six values; match() takes MongoOrExpr.of([...]) for OR; the manual connection path is for statements outside a transaction; a prepared statement has no close(); param() binds a Temporal.Instant for a DateTime; releasing a manual connection needs a commit or rollback first. Pages total 3,423 lines. Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- .../docs/content/docs/orm/reference/index.mdx | 12 +- .../docs/orm/reference/pipeline-builder.mdx | 244 +++++++++--------- .../docs/orm/reference/raw-queries.mdx | 58 ++--- .../docs/orm/reference/sql-query-builder.mdx | 159 ++++++------ .../reference/transactions-and-runtime.mdx | 194 +++++++------- .../pipeline-builder.part1.round3.md | 100 +++++++ .../pipeline-builder.part2.round3.md | 59 +++++ .../raw-queries-and-index.round3.md | 119 +++++++++ .../sql-query-builder.part1.round3.md | 96 +++++++ .../sql-query-builder.part2.round3.md | 85 ++++++ .../transactions-and-runtime.part1.round3.md | 93 +++++++ .../transactions-and-runtime.part2.round3.md | 98 +++++++ 12 files changed, 978 insertions(+), 339 deletions(-) create mode 100644 docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part1.round3.md create mode 100644 docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part2.round3.md create mode 100644 docs/orm-docs-audit/c21-orm-reference/raw-queries-and-index.round3.md create mode 100644 docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part1.round3.md create mode 100644 docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part2.round3.md create mode 100644 docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part1.round3.md create mode 100644 docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part2.round3.md diff --git a/apps/docs/content/docs/orm/reference/index.mdx b/apps/docs/content/docs/orm/reference/index.mdx index 6352925c38c..d167eb0498d 100644 --- a/apps/docs/content/docs/orm/reference/index.mdx +++ b/apps/docs/content/docs/orm/reference/index.mdx @@ -6,9 +6,13 @@ metaTitle: Prisma ORM API reference metaDescription: 'Reference index for the Prisma ORM client, SQL query builder, pipeline builder, raw queries, and runtime APIs.' --- -Prisma ORM gives you three ways to write a query. In Prisma ORM 7 you wrote `findMany({ where, include })`. Now each of those arguments is a method you chain: `.where(...).include(...)`. See [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7), which maps each argument to its method. The **ORM client** is at `db.orm`. It has model methods such as `where()`, `create()`, and `include()`, and it works on both PostgreSQL and MongoDB. The **SQL query builder** is at `db.sql`. It has table methods that match SQL, such as `select()`, `innerJoin()`, and `groupBy()`, and it works on PostgreSQL only. The **pipeline builder** is at `db.query`. It builds MongoDB aggregation pipelines. It is not the same as `runtime.query()`, which runs a built query. +Prisma ORM gives you three typed ways to write a query. In Prisma ORM 7 you wrote `findMany({ where, include })`. Now each of those arguments is a method you chain: `.where(...).include(...)`. See [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7), which maps each argument to its method. -Use the ORM client for everyday reads and writes across your models, including loading related rows with `include()`. Use the SQL query builder when the result you want is not a model row, such as a few columns drawn from two joined tables, or an aggregate over a join. Use the pipeline builder for MongoDB aggregation. When none of them can express the query, drop to a [raw query](/orm/reference/raw-queries). For connecting and disconnecting the client, transactions, and prepared statements, see [Transactions and runtime](/orm/reference/transactions-and-runtime). In the example below, the ORM client runs when you `await` it. The SQL query builder and the pipeline builder do not run on their own: each produces a built query with `.build()`, which you then run with `runtime.query(...)`. Watch the casing: `User` is the model name and `user` is the table name. +- The **ORM client** is at `db.orm`. It has model methods such as `where()`, `create()`, and `include()`, it works on both PostgreSQL and MongoDB, and it covers everyday reads and writes across your models. +- The **SQL query builder** is at `db.sql`. It has table methods that match SQL, such as `select()`, `innerJoin()`, and `groupBy()`, it works on PostgreSQL only, and it returns results that are not model rows, such as an aggregate over a join. +- The **pipeline builder** is at `db.query`. It builds MongoDB aggregation pipelines. + +Plus raw SQL and raw commands when none of them fits. See [Raw queries](/orm/reference/raw-queries). For connecting and disconnecting the client, transactions, and prepared statements, see [Transactions and runtime](/orm/reference/transactions-and-runtime). In the example below, `db.runtime()` returns the connection that runs a built query. Create it once and reuse it. `.all()` runs the query and returns every row. It is what replaced `findMany`. The ORM client runs when you `await` it. The SQL query builder and the pipeline builder do not run on their own. Each produces a built query with `.build()`, which you then run with `runtime.query(...)`. Watch the casing: `User` is the model name and `user` is the table name. ```ts const runtime = db.runtime(); @@ -17,9 +21,11 @@ const plan = db.sql.public.user.select('id', 'email').where((f, fns) => fns.eq(f const rows = await runtime.query(plan); ``` +`db.query` builds MongoDB pipelines. The separate `runtime.query()` method is what runs any built query. + ## How database differences are documented -Every method on a reference page has the same four parts: Remarks, Options, Return type, and Examples. The raw queries page is shorter and has fewer tables. If a method works differently on PostgreSQL and MongoDB, or exists on only one of them, its Remarks say so. For a walkthrough of reading and writing data rather than a method list, see [Reading data](/orm/fundamentals/reading-data), [Writing data](/orm/fundamentals/writing-data), [Relations and joins](/orm/fundamentals/relations-and-joins), [Transactions](/orm/fundamentals/transactions), and [Advanced queries](/orm/fundamentals/advanced-queries). +Every method on a reference page has the same four parts: Remarks, Options, Return type, and Examples. If a method works differently on PostgreSQL and MongoDB, or exists on only one of them, its Remarks say so. For a walkthrough of reading and writing data rather than a method list, see [Reading data](/orm/fundamentals/reading-data), [Writing data](/orm/fundamentals/writing-data), [Relations and joins](/orm/fundamentals/relations-and-joins), [Transactions](/orm/fundamentals/transactions), and [Advanced queries](/orm/fundamentals/advanced-queries). }> diff --git a/apps/docs/content/docs/orm/reference/pipeline-builder.mdx b/apps/docs/content/docs/orm/reference/pipeline-builder.mdx index 692ad030825..c33750b4d20 100644 --- a/apps/docs/content/docs/orm/reference/pipeline-builder.mdx +++ b/apps/docs/content/docs/orm/reference/pipeline-builder.mdx @@ -8,7 +8,7 @@ metaDescription: 'Reference for the Prisma ORM MongoDB pipeline builder''s stage The pipeline builder gives you a typed way to build MongoDB aggregation pipelines. You reach it through `db.query`, chain aggregation stages onto a starting collection, and finish the chain with the call that builds the query. It is the MongoDB counterpart to the [SQL query builder](/orm/reference/sql-query-builder): a lower-level API for the queries the [ORM client](/orm/reference/orm-client) doesn't express. -Everything the builder produces compiles to a MongoDB aggregation pipeline. There is no `find` and no `distinct`. To fetch a single document, filter and then `limit(1)`. Use the ORM client (`db.orm`) for everyday reads and writes across models and relations, and the pipeline builder when you need a stage the ORM client doesn't expose: grouping, `$lookup` joins with post-processing, computed projections, multi-stage transformations, or stages that write the results into a collection (`$out` and `$merge`). This page documents every stage, accumulator, expression helper, and write method. +Everything the builder produces becomes a MongoDB aggregation pipeline. There is no `find()` or `distinct()` here. Use `db.orm` or [`rawCommand()`](#rawcommand) for those. To fetch a single document, filter and then `limit(1)`. Use the ORM client (`db.orm`) for everyday reads and writes across models and relations, and the pipeline builder when you need a stage the ORM client doesn't expose: grouping, `$lookup` joins with post-processing, computed projections, multi-stage transformations, or stages that write the results into a collection (`$out` and `$merge`). This page documents every stage, accumulator, expression helper, and write method. :::note[This is the MongoDB pipeline builder] The pipeline builder targets MongoDB only. PostgreSQL has no pipeline builder: use the [SQL query builder](/orm/reference/sql-query-builder) for PostgreSQL joins and aggregates, and the [ORM client](/orm/reference/orm-client) for everyday queries on either database. @@ -16,7 +16,7 @@ The pipeline builder targets MongoDB only. PostgreSQL has no pipeline builder: u ## Example schema -All examples on this page run against the schema below. It is the same MongoDB schema as the [ORM client](/orm/reference/orm-client) page, so examples carry over between the two pages. The `posts` collection holds two kinds of document, `Article` and `Tutorial`, told apart by the value of the `kind` field. No example on this page uses them. A type id such as `mongo/string@1` is the stored type plus a version, always `@1` today. +All examples on this page run against the schema below. It is the same MongoDB schema as the [ORM client](/orm/reference/orm-client) page, so examples carry over between the two pages. `Article` and `Tutorial` are variants of `Post`. They are stored in the same `posts` collection and told apart by the value of the `kind` field. See [`variant()`](/orm/reference/orm-client#variant) on the ORM client page for how to query one variant. You do not need `variant()` on this page, because `kind` is an ordinary field you can filter and group on.
@@ -81,7 +81,9 @@ model Tutorial { Every pipeline starts with `db.query.from('posts')`, naming the collection to aggregate over. It ends with a call that turns the chain into a query you can run. Between them you chain stages. -`db` is the client you create with `mongo(...)`. See [Transactions and runtime](/orm/reference/transactions-and-runtime#mongooptions) for how to create it. Prisma ORM 8 renames `schema.prisma` to `contract.prisma`. Run `npx prisma contract emit` to generate `contract.json`, used at run time, and `contract.d.ts`, used for types. [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7) has the setup steps. +`db` is the client you create with `mongo(...)`, whose `dbName` option is the name of the MongoDB database to use. See [Transactions and runtime](/orm/reference/transactions-and-runtime#mongooptions) for every option and for how to create the client. + +Prisma ORM 8 renames `schema.prisma` to `contract.prisma`. Run `npx prisma contract emit` before any code on this page compiles: it writes `contract.json`, used at run time, and `contract.d.ts`, used for types. The example below imports `contract.json` with `with { type: 'json' }`, which works with the TypeScript settings `prisma orm init` writes for you. [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7) has the setup steps. ### `from()` @@ -98,7 +100,7 @@ Enter the pipeline builder on a collection. | Name | Type | Required | Description | |---|---|---|---| -| `root` | Collection name (`string` literal) | Yes | The collection to aggregate over. | +| `collection` | Collection name (`string` literal) | Yes | The collection to aggregate over. | #### Return type @@ -128,12 +130,11 @@ Nothing runs until you execute the pipeline. `build()` turns the chain into a bu #### Remarks -- Call `build()`. `aggregate()` is the same method under another name. - On MongoDB, `db.runtime()` returns a promise, so you must await it: `const runtime = await db.runtime()`. Hold it in a variable and reuse it. -- `await` that call for an array of documents, or `for await` over it to take one document at a time. +- `await` that call for an array of documents, or write `for await (const doc of runtime.query(plan)) { ... }` to take one document at a time. - There is no `db.execute` on the MongoDB client. -- **After `lookup()` or `group()`, compare ids with `String(a._id) === String(b._id)`.** Both a looked-up sub-document's `_id` and the top-level `_id` are the driver's own `ObjectId` values there, not hex strings. -- **Read results are decoded.** Prisma ORM converts each field from what the driver returns into the type your contract gives it: `_id` becomes a hex string, and a date becomes the date type in your contract. Decoding only survives these stages: `match()`, `sort()`, `limit()`, `skip()`, `sample()`, `project()`, `addFields()`, and `vectorSearch()`. After any other stage, including `lookup()` and `group()`, documents come back raw. +- **After `lookup()` or `group()`, compare ids with `String(a._id) === String(b._id)`**, where `a` is a looked-up sub-document and `b` is the parent document it came from. Both `_id` values are the driver's own `ObjectId` objects there, and two `ObjectId` objects are never `===` even when they hold the same id. +- **Read results are decoded.** If every stage in your pipeline is one of `match()`, `sort()`, `limit()`, `skip()`, `sample()`, `project()`, `addFields()`, and `vectorSearch()`, results are decoded: `_id` is a hex string and dates are your contract's date type. If any stage is not in that list, including `lookup()` and `group()`, every document comes back raw: `_id` is the driver's `ObjectId` and dates are the driver's `Date`. #### Examples @@ -155,8 +156,8 @@ Filter documents by a predicate. #### Remarks - `match()` takes a callback whose `f` argument has one property per field, and returns a filter expression (`(f) => f.kind.eq('tutorial')`). -- Prisma ORM sends your value to MongoDB unchanged. It does not turn a string into an `ObjectId`, a date, or anything else. That is what you want for most scalar fields. It is why an `_id` filter never matches: your hex string is compared as a string against a stored `ObjectId`. See the warning below. -- Leaf fields expose these filter operators: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`, `exists`, and `type`. They work like the MongoDB query operators of the same name (`$ne`, `$gt`, `$in`, `$exists`, `$type`, and so on). +- Neither a hex string nor a driver `ObjectId` matches in `match()`. The builder sends the value as a plain object, so an `ObjectId` arrives as its bytes. See the warning below. +- Scalar fields expose these filter operators: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`, `exists`, and `type`. They work like the MongoDB query operators of the same name (`$ne`, `$gt`, `$in`, `$exists`, `$type`, and so on). - To compare computed values rather than a field against a constant, wrap the comparison in `expr(...)`: `match((f) => expr(fn.gt(fn.year(f.createdAt), fn.literal(2023))))`. `fn` holds the helpers that build expressions, and `expr()` wraps one so `match()` accepts it. The `fn.*` helpers take expressions, not plain values, so wrap every constant in `fn.literal()`. See [Expression helpers](#expression-helpers). #### Options @@ -196,7 +197,7 @@ const recent = await runtime.query(plan); ``` :::warning[`_id` equality filters do not work through the pipeline builder] -Filtering by `_id` equality inside `match()` **never matches any document**. Neither a hex string (`f._id.eq('507f...')`) nor the driver's own `ObjectId` matches. The hex-string form passes TypeScript, so it compiles cleanly and then returns zero documents. There is no supported way to filter by `_id` equality through the typed pipeline builder. To fetch one document by its id, use the [ORM client](/orm/reference/orm-client): +Filtering by `_id` equality inside `match()` **never matches any document**. Neither a hex string (`f._id.eq('507f...')`) nor the driver's own `ObjectId` matches. There is no supported way to filter by `_id` equality through the typed pipeline builder. To fetch one document by its id, use the [ORM client](/orm/reference/orm-client), which converts the hex string for you. The pipeline builder does not convert it: ```typescript const postId = '6650f1c2a1b2c3d4e5f60003'; @@ -354,7 +355,7 @@ Join documents from another collection (`$lookup`). #### Remarks -- `lookup()` takes a callback that builds the join with `from(root).on((local, foreign) => ({ local, foreign })).as(name)`: the other collection, the two fields to match on, and the name of the output array field. +- `lookup()` takes a callback that builds the join with `from(collection).on((local, foreign) => ({ local, foreign })).as(name)`: the other collection, the two fields to match on, and the name of the output array field. - The object returned by `on()` has two fixed keys, `local` and `foreign`. Write `({ local: local.authorId, foreign: foreign._id })`. - The joined documents arrive in an array under the `as` name. Compare their ids with `String(a._id) === String(b._id)`. @@ -362,7 +363,7 @@ Join documents from another collection (`$lookup`). | Name | Type | Required | Description | |---|---|---|---| -| `builder` | Callback `(from) => from(root).on(...).as(name)` | Yes | The join specification. | +| `builder` | Callback `(from) => from(collection).on(...).as(name)` | Yes | The join specification. | #### Return type @@ -385,7 +386,6 @@ const plan = db.query ) .build(); const withAuthor = await runtime.query(plan); -// withAuthor[0].author is an array; compare ids with String(a._id) === String(b._id) ``` ### `project()` @@ -395,7 +395,7 @@ Reshape each document. #### Remarks - `project()` has two forms. A **key-list** form narrows to the named fields (`project('title', 'kind')`); `_id` is retained implicitly even when not listed. -- A **callback** form computes a projection spec (`project((f) => ({ title: 1, shout: fn.toUpper(f.title) }))`), keeping, dropping, or computing each field. +- A **callback** form computes a projection spec (`project((f) => ({ title: 1, shout: fn.toUpper(f.title) }))`). Write `1` to keep a field, `0` to drop it, or an expression to compute it. MongoDB rejects a mix of `1` and `0` in one projection, except for `_id: 0`. #### Options @@ -426,7 +426,7 @@ import { fn } from '@prisma/orm-mongo/query-builder'; const plan = db.query .from('posts') - .project((f) => ({ title: 1, shout: fn.toUpper(f.title) })) + .project((f) => ({ _id: 0, title: 1, shout: fn.toUpper(f.title) })) .build(); const projected = await runtime.query(plan); ``` @@ -438,7 +438,6 @@ Unroll an array field into one document per element. #### Remarks - `unwind(field, { preserveNullAndEmptyArrays? })` maps to MongoDB's `$unwind`. The named field must hold an array. -- One input document becomes one output document per element of that array. #### Options @@ -451,7 +450,7 @@ Unroll an array field into one document per element. | Return type | Example | Description | |---|---|---| -| Pipeline builder | `db.query.from(root).unwind('items')` | A builder emitting one document per array element. | +| Pipeline builder | `db.query.from('posts').unwind('tags')` | A builder emitting one document per array element. | #### Examples @@ -467,7 +466,6 @@ const plan = db.query .sort({ tags: 1 }) .build(); const perTag = await runtime.query(plan); -// one document per tag, each with the post's other fields beside it ``` ### `group()` @@ -477,7 +475,7 @@ Group documents by a key and compute per-group aggregates. #### Remarks - `group()` takes a callback that receives **only** the `f` argument, one property per field (`(f) => ...`), and returns a spec object. The spec's `_id` sets the grouping key; every other key must be an **accumulator**. -- `acc` holds the accumulators `group()` accepts, such as a count or a sum. Import it and use it inside the callback, **not** as a second callback argument: `import { acc } from '@prisma/orm-mongo/query-builder'`, then `acc.count()`, `acc.push(f.title)`. The callback takes one argument; there is no `(f, acc) => ...` form. +- `acc` holds the accumulators `group()` accepts, such as a count or a sum, documented under [Accumulators](#accumulators). Import it and use it inside the callback, **not** as a second callback argument: `import { acc } from '@prisma/orm-mongo/query-builder'`, then `acc.count()`, `acc.push(f.title)`. The callback takes one argument; there is no `(f, acc) => ...` form. - A `_id: null` key groups the whole collection into a single bucket. - A non-accumulator value for a non-`_id` key throws as soon as you call `group()`, with an error whose `code` is `ORM.ARGUMENT_INVALID`: `group() field "" must use an accumulator (e.g. acc.sum(), acc.count()). Got "" expression.` A `null` value for a key other than `_id` throws the same way. @@ -521,7 +519,6 @@ const plan = db.query .group((f) => ({ _id: null, total: acc.count(), latest: acc.max(f.createdAt) })) .build(); const [summary] = await runtime.query(plan); -// { _id: null, total: 2, latest: } ``` For Prisma ORM 7 users, `groupBy` becomes a `group()` stage on the pipeline builder: @@ -597,7 +594,6 @@ Reduce the pipeline to a single document holding the count. ```typescript const plan = db.query.from('posts').count('total').build(); const [{ total }] = await runtime.query(plan); -// { total: 2 } ``` ### `sortByCount()` @@ -639,11 +635,16 @@ Keep or prune documents (and sub-documents) based on an expression. #### Remarks -- `redact()` maps to MongoDB's `$redact`, whose expression must evaluate to one of the system variables `$$KEEP`, `$$DESCEND`, or `$$PRUNE`. -- `f.rawPath(path)` names a field or a MongoDB system variable by its raw string, with no checking against your model. For `$$KEEP` write `f.rawPath('$KEEP')`. There is no expression helper for a system variable, and `fn.literal(...)` cannot stand in. The server rejects both `fn.literal('KEEP')` and `fn.literal('$$KEEP')`. -- Every `fn.*` call returns a wrapper. `.node` is the raw expression inside it. `match()` takes the wrapper through `expr(...)`. `fn.cond()`, `redact()`, and the option-object stages take `.node`. +- `redact()` maps to MongoDB's `$redact`, whose expression must evaluate to one of MongoDB's `$$` variables: `$$KEEP`, `$$DESCEND`, or `$$PRUNE`. +- `f.rawPath(path)` names a field or a MongoDB `$$` variable by its raw string, with no checking against your model. It prefixes `$` to the string you give it, so `f.rawPath('$KEEP')` reaches MongoDB as `$$KEEP`. There is no expression helper for a `$$` variable, and `fn.literal(...)` cannot stand in. The server rejects both `fn.literal('KEEP')` and `fn.literal('$$KEEP')`. - For anything more involved, use [`rawCommand()`](#rawcommand), where you write the `$redact` stage with `$$KEEP` and `$$PRUNE` directly. +| Call site | What you pass | +|---|---| +| `project()`, `addFields()`, and the arguments of an accumulator | The `fn.*` value itself. | +| The condition argument of `fn.cond()`, `redact()`, and the option-object stages | The `.node` property of the `fn.*` value. | +| `match()` | The `fn.*` value wrapped in `expr(...)`. | + #### Options | Name | Type | Required | Description | @@ -670,7 +671,6 @@ const plan = db.query ) .build(); const tutorials = await runtime.query(plan); -// fn.cond() takes its condition as a raw expression, so that argument ends in .node. The other two take wrappers, which f.rawPath() returns. ``` ### Option-object stages @@ -680,17 +680,16 @@ Several stages take a single MongoDB stage options object rather than a typed `( #### Remarks - `bucket({ groupBy, boundaries, default_?, output? })` sorts documents into the ranges you name. `default` is a reserved word in JavaScript, so the key is `default_`. `groupBy` is a raw expression. -- `bucketAuto({ groupBy, buckets, output?, granularity? })` sorts them into a given number of ranges, chosen so each holds about the same number of documents. `groupBy` is a raw expression. +- `bucketAuto({ groupBy, buckets, output?, granularity? })` sorts them into a given number of ranges, chosen so each holds about the same number of documents. `groupBy` is a raw expression. `granularity` picks the bucket size from one of MongoDB's preferred-number series. - `geoNear({ near, distanceField, spherical?, maxDistance?, minDistance?, query?, key?, distanceMultiplier?, includeLocs? })` sorts documents by their distance from a point and writes that distance into `distanceField`. `near` is a raw expression. - `graphLookup({ from, startWith, connectFromField, connectToField, as, maxDepth?, depthField?, restrictSearchWithMatch? })` follows links from document to document through one collection and collects what it reaches into `as`. `startWith` is a raw expression. -- `setWindowFields({ partitionBy?, sortBy?, output })` computes a value for each document from the documents around it, such as a running total. `partitionBy` is a raw expression. -- `densify({ field, partitionByFields?, range })` adds documents to fill the gaps in a sequence of numbers or dates. -- `fill({ partitionBy?, partitionByFields?, sortBy?, output })` replaces missing or null values with a value you choose. `partitionBy` is a raw expression. +- `setWindowFields({ partitionBy?, sortBy?, output })` computes a value for each document from the documents around it, such as a running total. `partitionBy` is a raw expression. `output` is an object mapping each new field name to `{ operator, window? }`, where `operator` is the accumulator or expression to compute. +- `densify({ field, partitionByFields?, range })` adds documents to fill the gaps in a sequence of numbers or dates. `range` is `{ step, unit?, bounds }`, where `bounds` is `'full'`, `'partition'`, or a pair of start and end values. +- `fill({ partitionBy?, partitionByFields?, sortBy?, output })` replaces missing or null values with a value you choose. `partitionBy` is a raw expression. `output` is an object mapping each field name to `{ method }` or `{ value }`, where `value` is an expression. - `facet(facets)` takes an object mapping each output field name to an array of stages. - `unionWith(collection, pipeline?)` takes a collection name and an optional array of stages. -- `geoNear`, `densify`, and `fill` have not been verified against a running MongoDB. -- Build a raw expression with the `fn.*` helpers and pass its `.node` property, or name a field with `MongoAggFieldRef.of('price')`. -- The stages you pass to `facet` and `unionWith` are classes: `MongoMatchStage`, `MongoProjectStage`, `MongoSortStage`, `MongoLimitStage`, `MongoSkipStage`, `MongoLookupStage`, `MongoUnwindStage`, `MongoGroupStage`, `MongoAddFieldsStage`, `MongoReplaceRootStage`, `MongoCountStage`, `MongoSortByCountStage`, `MongoSampleStage`, `MongoRedactStage`, `MongoOutStage`, `MongoUnionWithStage`, `MongoBucketStage`, `MongoBucketAutoStage`, `MongoGeoNearStage`, `MongoFacetStage`, `MongoGraphLookupStage`, `MongoMergeStage`, `MongoSetWindowFieldsStage`, `MongoDensifyStage`, `MongoFillStage`, `MongoSearchStage`, `MongoSearchMetaStage`, and `MongoVectorSearchStage`. Import them, and `MongoAggFieldRef`, from `@prisma/orm-mongo/query-ast/execution`. +- Build a raw expression with the `fn.*` helpers and pass its `.node` property, or name a field with `MongoAggFieldRef.of('duration')`. +- The stages you pass to `facet` and `unionWith` are classes: `MongoMatchStage`, `MongoProjectStage`, `MongoSortStage`, `MongoLimitStage`, `MongoSkipStage`, `MongoLookupStage`, `MongoUnwindStage`, `MongoGroupStage`, `MongoAddFieldsStage`, `MongoReplaceRootStage`, `MongoCountStage`, `MongoSortByCountStage`, `MongoSampleStage`, `MongoRedactStage`, `MongoOutStage`, `MongoUnionWithStage`, `MongoBucketStage`, `MongoBucketAutoStage`, `MongoGeoNearStage`, `MongoFacetStage`, `MongoGraphLookupStage`, `MongoMergeStage`, `MongoSetWindowFieldsStage`, `MongoDensifyStage`, `MongoFillStage`, `MongoSearchStage`, `MongoSearchMetaStage`, and `MongoVectorSearchStage`. Import them, and `MongoAggFieldRef`, from `@prisma/orm-mongo/query-ast/execution`. Each stage class takes the same arguments as the MongoDB stage it builds. The three used below are `MongoCountStage(field)`, `MongoSortStage(spec)`, and `MongoLimitStage(n)`. #### Examples @@ -703,6 +702,8 @@ const combined = await runtime.query(plan); ##### Run several sub-pipelines with `facet()` +`MongoFieldFilter` is the filter class from `@prisma/orm-mongo/query-ast/execution`. See [MongoFieldFilter](/orm/reference/orm-client#mongofieldfilter) on the ORM client page for its methods, such as `gt` and `in`. + ```typescript import { MongoCountStage, MongoFieldFilter, MongoLimitStage, MongoMatchStage, MongoSortStage } from '@prisma/orm-mongo/query-ast/execution'; @@ -747,10 +748,9 @@ const buckets = await runtime.query(plan); #### Remarks - These stages need MongoDB Atlas. The query builds, and MongoDB rejects it when it runs. -- `search(config, index?)` takes an Atlas Search operator object, for example `{ text: { query: 'hello', path: 'title' } }`, and an optional index name. +- `search(config, index?)` takes an Atlas Search operator object, for example `{ text: { query: 'hello', path: 'title' } }`, and an optional `index`, which names the Atlas Search index to use. - `searchMeta(config, index?)` takes the same two arguments and returns counts and facets instead of documents. - `vectorSearch({ index, path, queryVector, numCandidates, limit, filter? })` takes the index name, the field holding the vectors, the vector to search for as an array of numbers, how many candidates to consider, how many documents to return, and an optional filter object. -- Test all three against your own Atlas deployment before relying on them. #### Return type @@ -790,7 +790,12 @@ The builder exposes nineteen accumulators: | `acc.stdDevPop(expr)` | `stdDevPop(expression)` | Population standard deviation. | | `acc.stdDevSamp(expr)` | `stdDevSamp(expression)` | Sample standard deviation. | -`firstN` is shown below. `lastN`, `maxN`, and `minN` take the same arguments. `top`, `bottom`, `topN`, and `bottomN` take `output` and `sortBy` instead. `output` is the expression whose value you want back from each chosen document. `sortBy` is a plain object such as `{ createdAt: 1 }`, where `1` sorts ascending and `-1` sorts descending. Wherever a signature takes `n`, wrap the number: write `n: fn.literal(2)`, not `n: 2`. `fn.literal` comes from the [expression helpers](#expression-helpers) below. Put a `sort()` before the `group()` when you use `acc.first()` or `acc.last()`. Without one, the document MongoDB calls first is arbitrary and can change between runs. +- `firstN`, `lastN`, `maxN`, and `minN` take `{ input, n }`. `input` is the expression whose values you collect. +- `top` and `bottom` take `{ output, sortBy }`. `topN` and `bottomN` take the same two plus `n`. +- `output` is the expression whose value you want back from each chosen document. `sortBy` is a plain object such as `{ createdAt: 1 }`, where `1` sorts ascending and `-1` sorts descending. +- Wherever a signature takes `n`, wrap the number: write `n: fn.literal(2)`, not `n: 2`. `fn.literal` comes from the [expression helpers](#expression-helpers) below. + +**Put a `sort()` before the `group()` when you use `acc.first()` or `acc.last()`.** Without one, the document MongoDB calls first is arbitrary and can change between runs. #### Examples @@ -801,10 +806,8 @@ The builder exposes nineteen accumulators: ```typescript import { acc, fn } from '@prisma/orm-mongo/query-builder'; -const plan = db.query - .from('posts') - .group((f) => ({ _id: null, total: acc.count(), counted: acc.sum(fn.literal(1)) })) - .build(); +const plan = db.query.from('posts') + .group((f) => ({ _id: null, total: acc.count(), counted: acc.sum(fn.literal(1)) })).build(); const [{ total, counted }] = await runtime.query(plan); // { total: 2, counted: 2 } ``` @@ -812,8 +815,7 @@ const [{ total, counted }] = await runtime.query(plan); ##### `avg()`, `min()`, and `max()` ```typescript -const plan = db.query - .from('posts') +const plan = db.query.from('posts') .group((f) => ({ _id: null, earliest: acc.min(f.createdAt), avgYear: acc.avg(fn.year(f.createdAt)) })) .build(); const [stats] = await runtime.query(plan); @@ -823,9 +825,7 @@ const [stats] = await runtime.query(plan); ##### `first()` and `last()` ```typescript -const plan = db.query - .from('posts') - .sort({ createdAt: 1 }) +const plan = db.query.from('posts').sort({ createdAt: 1 }) .group((f) => ({ _id: null, firstTitle: acc.first(f.title), lastTitle: acc.last(f.title) })) .build(); const [{ firstTitle, lastTitle }] = await runtime.query(plan); @@ -834,24 +834,16 @@ const [{ firstTitle, lastTitle }] = await runtime.query(plan); ##### `push()` and `addToSet()` -```typescript -const plan = db.query - .from('posts') - .group((f) => ({ _id: null, allTitles: acc.push(f.title), distinctKinds: acc.addToSet(f.kind) })) - .build(); -const [{ allTitles, distinctKinds }] = await runtime.query(plan); -``` +Both take one expression: `group((f) => ({ _id: null, allTitles: acc.push(f.title), distinctKinds: acc.addToSet(f.kind) }))`. `allTitles` holds one entry per document. `distinctKinds` holds each value once. ##### `firstN()` (an `N`-variant) ```typescript -const plan = db.query - .from('posts') - .sort({ createdAt: 1 }) +const plan = db.query.from('posts').sort({ createdAt: 1 }) .group((f) => ({ _id: null, firstTwo: acc.firstN({ input: f.title, n: fn.literal(2) }), - newestTitle: acc.top({ output: f.title, sortBy: { createdAt: -1 } }), // newestTitle is the title string, not a document + newestTitle: acc.top({ output: f.title, sortBy: { createdAt: -1 } }), // the title string, not a document })) .build(); const [{ firstTwo, newestTitle }] = await runtime.query(plan); @@ -860,13 +852,22 @@ const [{ firstTwo, newestTitle }] = await runtime.query(plan); ## Expression helpers -Expression helpers (`fn.*`) build the computed values used inside stages like `addFields()`, `project()`, `group()` accumulators, and `match()` (via `expr()`). Import them from `@prisma/orm-mongo/query-builder`: +Expression helpers (`fn.*`) build the computed values used inside stages like `addFields()`, `project()`, `group()` accumulators, and `match()` (via `expr()`). An expression is an `f.field` value or an `fn.*` value, never a plain JavaScript value. Stage callbacks and these helpers take expressions. The update operators on the [write methods](#write-methods) and the `MongoFieldFilter` methods take plain JavaScript values instead. Import the helpers from `@prisma/orm-mongo/query-builder`: ```typescript import { fn } from '@prisma/orm-mongo/query-builder'; ``` -Each helper is the camelCase name of the MongoDB aggregation operator without its `$` prefix, so `$toUpper` is `fn.toUpper` and `$dateToString` is `fn.dateToString`. Five have a different name: `in` and `typeof` are reserved words in JavaScript, `toString` is already a method on every JavaScript object, and `$first` and `$last` get the suffix `Elem`. So write `fn.firstElem` for `$first`, `fn.lastElem` for `$last`, `fn.isIn` for `$in`, `fn.typeOf` for `$type`, and `fn.toString_` for `$toString`. Here is every helper. Anything missing from this list has no helper, including `$and`, `$or`, `$not`, `$switch`, `$ifNull`, `$map`, `$reduce`, and `$filter`. +Each helper is the camelCase name of the MongoDB aggregation operator without its `$` prefix, so `$toUpper` is `fn.toUpper` and `$dateToString` is `fn.dateToString`. Five helpers are named differently: + +| MongoDB operator | Helper | Why | +|---|---|---| +| `$in` | `fn.isIn` | `in` is a reserved word in JavaScript. | +| `$type` | `fn.typeOf` | `typeof` is a reserved word in JavaScript. | +| `$toString` | `fn.toString_` | `toString` is already a method on every JavaScript object. | +| `$first` and `$last` | `fn.firstElem` and `fn.lastElem` | none | + +Here is every helper. Anything missing from this list has no helper, including `$and`, `$or`, `$not`, `$switch`, `$ifNull`, `$map`, `$reduce`, and `$filter`. | Group | Helpers | |---|---| @@ -881,12 +882,13 @@ Each helper is the camelCase name of the MongoDB aggregation operator without it | Objects | `objectToArray`, `arrayToObject`, `getField`, `setField` | | Constants and branching | `literal`, `cond` | -`fn.literal(value)` wraps a constant so MongoDB reads it as a value and not as the name of a field. What you write at each call site differs: +`fn.literal(value)` wraps a constant so MongoDB reads it as a value and not as the name of a field. Every `fn.*` helper returns a typed wrapper, and `.node` is the raw MongoDB expression inside that wrapper. Arguments typed as an expression take the wrapper. Arguments typed as a raw MongoDB expression take `.node`, and those are the condition of `fn.cond()`, `redact()`, and the option-object stages: `bucket()`, `bucketAuto()`, `graphLookup()`, `setWindowFields()`, and `fill()`. Passing an `expr(...)` value where `.node` is expected throws a `TypeError`. This is what you pass at each call site: | Call site | What you pass | |---|---| | `project()`, `addFields()`, and the arguments of an accumulator | The `fn.*` value itself. | -| The condition argument of `fn.cond()`, `f.stage.set()`, `f.stage.replaceRoot()`, `f.stage.replaceWith()`, and the `groupBy`, `startWith`, and `partitionBy` options of `bucket()`, `bucketAuto()`, `graphLookup()`, `setWindowFields()`, and `fill()` | The `.node` property of the `fn.*` value. | +| The condition argument of `fn.cond()`, and `f.stage.set()`, `f.stage.replaceRoot()`, `f.stage.replaceWith()` | The `.node` property of the `fn.*` value. | +| The `groupBy`, `startWith`, and `partitionBy` options of the option-object stages | The `.node` property of the `fn.*` value. | | `match()` | The `fn.*` value wrapped in `expr(...)`. | `expr()` is what lets a `match()` filter compare computed values. Import it beside `fn`: @@ -895,14 +897,29 @@ Each helper is the camelCase name of the MongoDB aggregation operator without it import { expr, fn } from '@prisma/orm-mongo/query-builder'; const plan = db.query.from('posts').match((f) => expr(fn.gt(fn.year(f.createdAt), fn.literal(2024)))).build(); +``` + +For OR, AND, or NOT, pass `match()` a filter you build yourself instead of a callback: + +```typescript +import { MongoFieldFilter, MongoOrExpr } from '@prisma/orm-mongo/query-ast/execution'; + +const kindFilter = MongoOrExpr.of([ + MongoFieldFilter.eq('kind', 'tutorial'), + MongoFieldFilter.eq('kind', 'guide'), +]); +const plan = db.query.from('posts').match(kindFilter).build(); const rows = await runtime.query(plan); +// rows are the documents whose kind is either tutorial or guide ``` #### Remarks -- The condition argument of `fn.cond()` takes `.node`. The then and else arguments do not. Passing an `expr(...)` value as the condition throws a `TypeError`. -- Some helpers take one object of named arguments rather than positional ones: `fn.dateDiff({ startDate, endDate, unit, timezone?, startOfWeek? })`, `fn.dateTrunc({ date, unit, binSize?, timezone?, startOfWeek? })`, `fn.dateAdd({ startDate, unit, amount, timezone? })`, `fn.dateSubtract` with the same keys as `fn.dateAdd`, `fn.dateToString({ date, format })`, `fn.trim({ input })`, `fn.regexMatch({ input, regex })`, `fn.zip({ inputs, useLongestLength?, defaults? })`, `fn.getField({ field, input? })`, `fn.setField({ field, input, value })`, and `fn.convert({ input, to, onError?, onNull? })`. In `fn.convert`, `to` is the MongoDB type name you want, written as an expression, such as `fn.literal('int')` or `fn.literal('objectId')`. -- `fn.slice(array, ...rest)` and `fn.range(start, end, step)` take positional arguments. So does every other helper not named above, in the order MongoDB documents for that operator. Each argument is an expression from the `f` argument or a `fn.literal(...)` value. +- There is no `fn.*` helper for OR, AND, or NOT. In a `match()` filter, build the filter yourself: `MongoOrExpr.of([...])` is OR, `MongoAndExpr.of([...])` is AND, and `.not()` on any filter is NOT. The example above shows OR. +- Each entry in those lists is a `MongoFieldFilter`, which has `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`, `isNull`, and `isNotNull`. These are the same combinators the ORM client uses, described under [Combinators](/orm/reference/orm-client#combinators). +- Inside a computed value in `project()` or `addFields()` there is no way to write OR, AND, or NOT. Write that stage yourself with [`pipe()`](#pipe), or write the whole command with [`rawCommand()`](#rawcommand). +- Some helpers take one object of named arguments rather than positional ones: `fn.dateDiff({ startDate, endDate, unit, timezone?, startOfWeek? })`, `fn.dateTrunc({ date, unit, binSize?, timezone?, startOfWeek? })`, `fn.dateAdd({ startDate, unit, amount, timezone? })`, `fn.dateSubtract` with the same keys as `fn.dateAdd`, `fn.dateToString({ date, format })`, `fn.trim({ input })`, `fn.regexMatch({ input, regex })`, `fn.zip({ inputs, useLongestLength?, defaults? })`, `fn.getField({ field, input? })`, `fn.setField({ field, input, value })`, and `fn.convert({ input, to, onError?, onNull? })`. In `fn.convert`, `to` takes any MongoDB type name, written as an expression, such as `fn.literal('int')` or `fn.literal('objectId')`. +- `fn.slice(array, ...rest)` and `fn.range(start, end, step)` take positional arguments. So does every other helper not named above, in the order given for that operator in [MongoDB's aggregation operator reference](https://www.mongodb.com/docs/manual/reference/operator/aggregation/). Each argument is an expression from the `f` argument or a `fn.literal(...)` value. - **`fn.toObjectId()` cannot take a `fn.literal()` value.** `fn.toObjectId(fn.literal(id))` throws. To turn a string id from your own code into an `ObjectId`, build the `ObjectId` yourself and put it in a raw pipeline with [`rawCommand()`](#rawcommand). The last example on this page does exactly that. #### Examples @@ -910,8 +927,7 @@ const rows = await runtime.query(plan); ##### Arithmetic in a projection ```typescript -const plan = db.query - .from('posts') +const plan = db.query.from('posts') .project((f) => ({ title: 1, computed: fn.add(fn.literal(1), fn.multiply(fn.literal(2), fn.literal(3))) })) .build(); const rows = await runtime.query(plan); @@ -921,18 +937,14 @@ const rows = await runtime.query(plan); ##### String composition ```typescript -const plan = db.query - .from('posts') - .project((f) => ({ shout: fn.concat(fn.toUpper(f.title), fn.literal('!')) })) - .build(); -const rows = await runtime.query(plan); +const plan = db.query.from('posts') + .project((f) => ({ shout: fn.concat(fn.toUpper(f.title), fn.literal('!')) })).build(); ``` ##### Branch with `cond()` ```typescript -const plan = db.query - .from('posts') +const plan = db.query.from('posts') .project((f) => ({ title: 1, label: fn.cond(fn.eq(f.kind, fn.literal('tutorial')).node, fn.literal('is-tutorial'), fn.literal('not-tutorial')), @@ -947,7 +959,7 @@ Append an arbitrary raw pipeline stage the typed methods don't cover. #### Remarks -- `pipe(stage)` appends a raw stage that you construct yourself, and keeps the row type you already had. `@prisma/orm-mongo/query-ast/execution` exports one class per MongoDB stage: `MongoMatchStage`, `MongoProjectStage`, `MongoSortStage`, `MongoLimitStage`, `MongoSkipStage`, `MongoLookupStage`, `MongoUnwindStage`, `MongoGroupStage`, `MongoAddFieldsStage`, `MongoReplaceRootStage`, `MongoCountStage`, `MongoSortByCountStage`, `MongoSampleStage`, `MongoRedactStage`, `MongoOutStage`, `MongoUnionWithStage`, `MongoBucketStage`, `MongoBucketAutoStage`, `MongoGeoNearStage`, `MongoFacetStage`, `MongoGraphLookupStage`, `MongoMergeStage`, `MongoSetWindowFieldsStage`, `MongoDensifyStage`, `MongoFillStage`, `MongoSearchStage`, `MongoSearchMetaStage`, and `MongoVectorSearchStage`. The same module exports `MongoFieldFilter`, which builds the filter a `MongoMatchStage` takes. +- `pipe(stage)` appends a raw stage that you construct yourself, and keeps the row type you already had. The stages are the `Mongo*Stage` classes, one per MongoDB stage, from `@prisma/orm-mongo/query-ast/execution`. The example below uses `MongoMatchStage` and `MongoCountStage`. The same module exports `MongoFieldFilter`, which builds the filter a `MongoMatchStage` takes. - `pipe(stage)` declares the row type the rows have after that stage. `NewShape` is a type you write yourself, as at the end of the example below. - After a `pipe()` stage the rows come back exactly as MongoDB returns them, whatever the TypeScript type claims. A date is a `Date` and an id is an `ObjectId` from the `mongodb` package. - Prefer a typed stage where one exists. `pipe()` adds one raw stage to a typed chain, and [`rawCommand()`](#rawcommand) replaces the whole command. @@ -959,10 +971,8 @@ Append an arbitrary raw pipeline stage the typed methods don't cover. ```typescript import { MongoCountStage, MongoFieldFilter, MongoMatchStage } from '@prisma/orm-mongo/query-ast/execution'; -const plan = db.query - .from('posts') - .pipe(new MongoMatchStage(MongoFieldFilter.eq('kind', 'tutorial'))) - .build(); +const plan = db.query.from('posts') + .pipe(new MongoMatchStage(MongoFieldFilter.eq('kind', 'tutorial'))).build(); const rows = await runtime.query(plan); type Counted = { total: number }; @@ -979,42 +989,39 @@ Compile the pipeline into a query you can run. #### Remarks -- `build()` produces the built query. `aggregate()` is an alias that returns the identical query. Write `build()`. Neither runs the query: pass the result to `runtime.query(...)` to fetch documents. - -#### Return type - -| Return type | Example | Description | -|---|---|---| -| Built query | `db.query.from('posts').sort({ createdAt: 1 }).build()` | A query you run with `runtime.query(...)`. | +- Write `build()`. `aggregate()` is another name for the same method. Neither runs the query: pass the result to `runtime.query(...)` to fetch documents. ## Write methods -The pipeline builder can write, not just read. Write methods are available at three points: on the root collection, after a `match()` filter, and as the last call in a pipeline. +The pipeline builder can write, not just read. Write methods are available at three points: on the root collection, after a `match()` filter, and as the last call in a pipeline. A write method returns the built query on its own, so there is no `build()` call after one. The examples below name that built query `plan`. Updater callbacks (the `(f) => [...]` argument to the update methods) must return an **array** of operations. This is a firm rule: - Always return an array. Returning a bare operation, `f.bio.set('x')` rather than `[f.bio.set('x')]`, throws as soon as you call the write method. - An empty array throws an error whose `code` is `ORM.MUTATION_DATA_MISSING`: `Updater returned no operations. Return at least one update from the callback ...`. -- An updater is either all operator form or all pipeline form. Operator form calls an operator on a field, as in `f.bio.set(value)`. Pipeline form calls `f.stage.*`, where `stage` is a literal property name, and can read other fields of the same document. You cannot mix the two in one updater. TypeScript rejects the mixed array, and at run time it throws an error whose `code` is `ORM.ARGUMENT_INVALID`: `Cannot mix ...`. +- An updater is either all operator form or all pipeline form. Operator form calls an operator on a field, as in `f.bio.set(value)`. Pipeline form calls `f.stage.*`, and can read other fields of the same document. Type `f.stage` exactly: `stage` is not a placeholder for a stage name. You cannot mix the two in one updater. TypeScript rejects the mixed array, and at run time it throws an error whose `code` is `ORM.ARGUMENT_INVALID`: `Cannot mix ...`. - Applying the same operator to the same field twice in one updater throws an error whose `code` is `ORM.ARGUMENT_INVALID`: `Update spec collision: ...`. ### Root-level writes -`insertOne`, `insertMany`, `updateAll`, `deleteAll`, and `upsertOne` are available directly on `from(root)`, before any stage. +`insertOne`, `insertMany`, `updateAll`, `deleteAll`, and `upsertOne` are available on the collection you name in `from(...)`, before you add any stage. #### Remarks - These return a built query. Run it with `runtime.query(...)`. - Write results are result objects, not documents: `insertOne` gives `{ insertedId }`, `insertMany` gives `{ insertedIds, insertedCount }`, the update methods and `upsertOne` give `{ matchedCount, modifiedCount, upsertedCount, upsertedId }`, and the delete methods give `{ deletedCount }`. A write returns one result object, as the single row of the result, which is why every example here reads it with `const [result] = ...`. -- The document you pass to `insertOne` / `insertMany` is a plain record. TypeScript requires no particular field, and you can leave `_id` out and let MongoDB assign one. The builder does not check the record against your contract, and each value is stored exactly as you pass it, with no conversion to the type your contract gives that field. So pass a JavaScript `Date` for a date field and an `ObjectId` from the `mongodb` package for an id field, because nothing turns a string into either. [`match()`](#match) filter values follow the same rule. `insertMany([])` throws an error whose `code` is `ORM.MUTATION_DATA_MISSING`. +- The document you pass to `insertOne` / `insertMany` is a plain record. TypeScript requires no particular field, and the builder does not check the record against your contract. +- You can leave `_id` out and let MongoDB assign one. +- **On the way in, values are stored exactly as you pass them, so pass a `Date` for a date field and an `ObjectId` from the `mongodb` package for an id field.** Nothing turns a string into either. This is the write direction only. Reads are decoded as the first half of this page describes. +- [`match()`](#match) filter values follow the same rule as the values you insert. The one value you cannot filter on this way is `_id`, which needs [`rawCommand()`](#rawcommand). The last example on this page shows it. +- `insertMany([])` throws an error whose `code` is `ORM.MUTATION_DATA_MISSING`. #### Examples ##### `insertOne()` and `insertMany()` ```typescript -const onePlan = db.query - .from('users') +const onePlan = db.query.from('users') .insertOne({ name: 'Carol', email: 'carol@example.com', bio: null, role: 'author', address: null }); const [insertOneResult] = await runtime.query(onePlan); // { insertedId: } @@ -1044,9 +1051,10 @@ const [result] = await runtime.query(plan); ##### `upsertOne()` on the root (filter, then updater) +`upsertOne(filterCallback, updaterCallback)`. The first callback returns the filter. The second returns the array of update operations. + ```typescript -const plan = db.query - .from('users') +const plan = db.query.from('users') .upsertOne((f) => f.email.eq('erin@example.com'), (f) => [f.name.set('Erin'), f.email.set('erin@example.com')]); const [result] = await runtime.query(plan); // inserts when no document matches: result.upsertedId is defined @@ -1061,7 +1069,7 @@ After a `match()` filter, `updateMany`, `updateOne`, `deleteMany`, `deleteOne`, - The `match()` filter supplies the write's filter, so you do not repeat it. - After `match()`, `upsertOne()` takes the updater callback only (the filter comes from `match()`). - `updateOne` / `deleteOne` affect at most one matching document. `updateMany` / `deleteMany` affect all matches. Which document `updateOne` / `deleteOne` picks is up to the driver. Use `findOneAndUpdate()` after a `sort()` when the choice matters. -- `findOneAndUpdate()` and `findOneAndDelete()` return the document exactly as the driver gives it, so its `_id` is an `ObjectId` from the `mongodb` package even though the TypeScript type says `string`. +- `findOneAndUpdate()` and `findOneAndDelete()` return the document exactly as the driver gives it, so its `_id` is an `ObjectId` from the `mongodb` package even though the TypeScript type says `string`. To get the id as a string, write `String(result._id)`. - You cannot call `findOneAndUpdate()` or `findOneAndDelete()` after `skip()`, and TypeScript does not offer them there. If a cast gets one past TypeScript, it throws an error whose `code` is `ORM.OPERATION_UNSUPPORTED`. #### Examples @@ -1069,15 +1077,11 @@ After a `match()` filter, `updateMany`, `updateOne`, `deleteMany`, `deleteOne`, ##### `updateMany()` and `updateOne()` ```typescript -const manyPlan = db.query - .from('users') - .match((f) => f.role.eq('author')) +const manyPlan = db.query.from('users').match((f) => f.role.eq('author')) .updateMany((f) => [f.bio.set('matched-many')]); await runtime.query(manyPlan); -const onePlan = db.query - .from('users') - .match((f) => f.email.eq('alice@example.com')) +const onePlan = db.query.from('users').match((f) => f.email.eq('alice@example.com')) .updateOne((f) => [f.bio.set('single-update')]); const [result] = await runtime.query(onePlan); // { matchedCount: 1, modifiedCount: 1 } @@ -1086,10 +1090,7 @@ const [result] = await runtime.query(onePlan); ##### `deleteMany()` and `deleteOne()` ```typescript -const plan = db.query - .from('users') - .match((f) => f.role.eq('author')) - .deleteMany(); +const plan = db.query.from('users').match((f) => f.role.eq('author')).deleteMany(); const [result] = await runtime.query(plan); // { deletedCount: 2 } ``` @@ -1097,9 +1098,7 @@ const [result] = await runtime.query(plan); ##### `upsertOne()` after `match()` (updater only) ```typescript -const plan = db.query - .from('users') - .match((f) => f.email.eq('alice@example.com')) +const plan = db.query.from('users').match((f) => f.email.eq('alice@example.com')) .upsertOne((f) => [f.bio.set('upserted via match')]); const [result] = await runtime.query(plan); // updates on a hit: result.modifiedCount is 1 @@ -1110,24 +1109,20 @@ const [result] = await runtime.query(plan); `findOneAndUpdate()` returns the matched document. Its second argument is optional. That argument takes `returnDocument`, which controls which version you get back: `'before'` returns the document before the update, `'after'` returns it after. The default is `'after'`. The same argument takes `upsert`, which defaults to `false` and inserts a document when nothing matches. ```typescript -const plan = db.query - .from('users') - .match((f) => f.email.eq('alice@example.com')) +const plan = db.query.from('users').match((f) => f.email.eq('alice@example.com')) .findOneAndUpdate((f) => [f.bio.set('changed')], { returnDocument: 'before' }); const [beforeDoc] = await runtime.query(plan); // beforeDoc.bio is the value from before the update ``` :::warning[The option is `returnDocument`, not `returnNewDocument`] -`returnNewDocument` is not a valid option name. TypeScript rejects it and suggests `returnDocument`. If a cast gets it past TypeScript, the unknown key is ignored and the default `'after'` applies, so a typo produces a wrong but plausible result rather than a loud error. Use `returnDocument: 'before' | 'after'`. +Use `returnDocument: 'before' | 'after'`. `returnNewDocument` is not a valid option name. TypeScript rejects it and suggests `returnDocument`. If a cast gets it past TypeScript, the unknown key is ignored and you silently get the default `'after'`. ::: ##### `findOneAndDelete()` ```typescript -const plan = db.query - .from('users') - .match((f) => f.email.eq('alice@example.com')) +const plan = db.query.from('users').match((f) => f.email.eq('alice@example.com')) .findOneAndDelete(); const [deleted] = await runtime.query(plan); // deleted is the removed document @@ -1156,7 +1151,7 @@ Operator form takes these arguments: | `max(value)` | A value, written only if it is higher than the stored one. | | `push(value)` | One value to append to an array field. | | `addToSet(value)` | One value, appended only if the array does not already hold it. | -| `pop(direction)` | `1` removes the last element, `-1` the first. Optional, and defaults to `1`. | +| `pop(direction)` | `1` removes the last element, `-1` the first. The argument is optional and defaults to `1`. | | `pull(value)` | The value to remove from an array field. | | `pullAll(values)` | An array of values to remove. | | `currentDate()` | Nothing. Writes the current date. | @@ -1167,21 +1162,17 @@ Operator form takes these arguments: ##### Operator form ```typescript -const plan = db.query - .from('users') - .match((f) => f.role.eq('author')) +const plan = db.query.from('users').match((f) => f.role.eq('author')) .updateMany((f) => [f.bio.set('operator form')]); await runtime.query(plan); +// on a collection with a numeric views field and an array tags field: +// updateMany((f) => [f.views.inc(1), f.tags.push('mongodb')]) ``` -The example schema has no numeric field and no array field, so `inc` and `push` have no example here. On a collection with a numeric `views` field and an array `tags` field, they read `updateMany((f) => [f.views.inc(1), f.tags.push('mongodb')])`. - ##### Pipeline form (`f.stage.*`) ```typescript -const plan = db.query - .from('users') - .match((f) => f.role.eq('author')) +const plan = db.query.from('users').match((f) => f.role.eq('author')) .updateMany((f) => [f.stage.set({ bio: f.name.node })]); await runtime.query(plan); // each author's bio is set to that author's own name @@ -1193,10 +1184,10 @@ await runtime.query(plan); #### Remarks -- `out(collection)` writes the pipeline output into a destination collection, replacing its contents. A second argument, a plain string, names a different database: `out('users_snapshot', 'archive')`. -- `merge({ into })` writes the pipeline output into a target collection, merging with existing documents. `into` is a collection name or `{ db, coll }`. The options object also takes `on`, the field or fields to match on, plus `whenMatched` and `whenNotMatched`. +- `out(collection)` writes the pipeline output into a destination collection, replacing its contents. Pass a second argument, the database name as a string, to write into a different database: `out('users_snapshot', 'archive')`. +- `merge({ into })` writes the pipeline output into a target collection, merging with existing documents. `into` is a collection name or `{ db, coll }`. The options object also takes `on`, which is one field name as a string or several as an array of strings, plus `whenMatched` and `whenNotMatched`. - `whenMatched` takes `'replace'`, `'keepExisting'`, `'merge'`, or `'fail'`, or an array of update stages, which are `MongoAddFieldsStage`, `MongoProjectStage`, and `MongoReplaceRootStage` objects from `@prisma/orm-mongo/query-ast/execution`. `whenNotMatched` takes `'insert'`, `'discard'`, or `'fail'`. A misspelled value fails at the database rather than in TypeScript. -- Both end the chain: they return a built query you run with `runtime.query(...)`, and produce no rows of their own. +- Both end the chain. They return a built query you run with `runtime.query(...)`, and that call returns an empty array, because the output goes to the destination collection and not to your program. #### Examples @@ -1211,8 +1202,7 @@ await runtime.query(plan); ##### Merge with `merge()` ```typescript -const plan = db.query - .from('users') +const plan = db.query.from('users') .merge({ into: 'users_archive', on: 'email', whenMatched: 'merge', whenNotMatched: 'insert' }); await runtime.query(plan); ``` diff --git a/apps/docs/content/docs/orm/reference/raw-queries.mdx b/apps/docs/content/docs/orm/reference/raw-queries.mdx index 77552a05f4d..602a2fba30b 100644 --- a/apps/docs/content/docs/orm/reference/raw-queries.mdx +++ b/apps/docs/content/docs/orm/reference/raw-queries.mdx @@ -8,7 +8,7 @@ metaDescription: 'Reference for Prisma ORM raw queries: PostgreSQL raw SQL and M Raw queries are the way out when the typed APIs can't express the query you need. Reach for a typed API first: the [ORM client](/orm/reference/orm-client) for everyday reads and writes, the [SQL query builder](/orm/reference/sql-query-builder) for PostgreSQL joins and aggregates, and the [pipeline builder](/orm/reference/pipeline-builder) for MongoDB aggregation. When none of them reaches the SQL clause or MongoDB command you need, drop down to raw. -This page covers two of these ways out: **PostgreSQL raw SQL** (the `fns.raw` tagged template for fragments and the `db.raw.sql` tag for whole statements) and **MongoDB raw commands** (`db.raw.collection(...)` and `db.query.rawCommand(...)`). +There are two kinds of raw query: **PostgreSQL raw SQL** (the `fns.raw` tagged template for fragments and the `db.raw.sql` tag for whole statements) and **MongoDB raw commands** (`db.raw.collection(...)` and `db.query.rawCommand(...)`). ## PostgreSQL raw SQL @@ -19,10 +19,10 @@ A **raw fragment** is a piece of SQL you put inside a builder query. Write it as A **whole raw statement** is a complete SQL statement written with `db.raw.sql`. If it returns rows, end it with `.returnsRow(spec).build()` and pass the result to `runtime.query(...)`. If it does not, end it with `.affectedCount().build()` and pass the result to `runtime.execute(...)`. :::warning[Only `.returnsRow()` converts values for you] -A type id (`codecId` in the code) such as `pg/text@1` says how to turn the database's bytes into a JavaScript value. `.returnsRow(spec)` names a type id for every column of a whole `db.raw.sql` statement, so you get real JavaScript values back. A fragment's `.returns(...)` does not convert anything. It only tells TypeScript what type to expect, so the value is whatever the driver returns. For a `text` or an `integer` column the driver already gives you a JavaScript string or number. A `numeric` column arrives as a string, and a date arrives as the driver's own value. +A type id such as `pg/text@1` tells Prisma ORM what JavaScript type a column should become. In code, the property is called `codecId`. `.returnsRow(spec)` names a type id for every column of a whole `db.raw.sql` statement, so you get real JavaScript values back. A fragment's `.returns(...)` does not convert anything. It only tells TypeScript what type to expect, so the value is whatever the `pg` package, the PostgreSQL driver underneath, gives you. For a `text` or an `integer` column that is already a JavaScript string or number. A `numeric` column arrives as a string. A date column arrives as whatever the `pg` package produces for it, with no conversion by Prisma ORM. ::: -The examples in this section run against the `user` / `post` schema from the SQL query builder page. See its [example schema](/orm/reference/sql-query-builder#example-schema) for the full model definitions. As on that page, create the client with `postgres(...)` and reach tables through `db.sql.public`, which is keyed by table name (the model name with a lowercase first letter unless the model sets `@@map`). `db.runtime()` gives you the connection that runs a built query. On PostgreSQL it gives you that connection directly. On MongoDB it returns a promise, so there you write `const runtime = await db.runtime();`. +The examples in this section run against the `user` / `post` schema from the SQL query builder page. See its [example schema](/orm/reference/sql-query-builder#example-schema) for the full model definitions. As on that page, create the client with `postgres(...)` and reach tables through `db.sql.public`, which is keyed by table name (the model name with a lowercase first letter unless the model sets the `@@map` attribute, which renames a table). `db.runtime()` gives you the connection that runs a built query. On PostgreSQL it gives you that connection directly. On MongoDB it returns a promise, so there you write `const runtime = await db.runtime();`. ```ts import postgres from '@prisma/orm-postgres/runtime'; @@ -47,7 +47,7 @@ const rows = await runtime.query(plan); // rows === [{ id: aliceId, upperEmail: 'ALICE@EXAMPLE.COM' }] ``` -`.returns(...)` only tells TypeScript what to expect. It does not cast the value in SQL and it does not change how the driver reads it. It takes a type id string, or an object of the form `{ codecId, nullable }`. It does not take a column, so `.returns(db.sql.public.user.columns.id)` is a type error. Only `.returnsRow(spec)` takes columns. See the [type ids](#binding-a-bare-value-with-param) for the ones you are most likely to write, and the SQL query builder's [`fns.raw` and `.returns()`](/orm/reference/sql-query-builder#fnsraw-and-returns) for the full treatment. +`.returns(...)` only tells TypeScript what to expect. It does not cast the value in SQL and it does not change how the `pg` package reads it. It takes a type id string, or an object of the form `{ codecId, nullable }`. It does not take a column, so `.returns(db.sql.public.user.columns.id)` is a type error. Only `.returnsRow(spec)` takes columns. See [Binding a bare value with `param()`](#binding-a-bare-value-with-param) for the type ids you are most likely to write, and the SQL query builder's [`fns.raw` and `.returns()`](/orm/reference/sql-query-builder#fnsraw-and-returns) for the full treatment. ### `fns.raw` as a `where()` predicate @@ -64,7 +64,7 @@ const rows = await runtime.query(plan); ### Interpolating a typed expression -You can interpolate another expression, such as a comparison, a column reference, or another raw fragment. Interpolated values are always sent as query parameters, never pasted into the SQL. There is no way to send a column name or a table name as a parameter, so never build one from user input with a template. When the column is chosen at run time, pick it in your own code from a fixed set of columns you control, and choose the matching `fns.raw` fragment or `select()` call for each one. +You can interpolate another expression, such as a comparison, a column reference, or another raw fragment. Interpolated values are always sent as query parameters, never pasted into the SQL. Never put user input into the SQL text itself, because a column or table name cannot be a parameter. When the column is chosen at run time, pick it in your own code from a fixed set of columns you control, and choose the matching `fns.raw` fragment or `select()` call for each one. ```ts const plan = db.sql.public.user @@ -77,32 +77,32 @@ const rows = await runtime.query(plan); ### Binding a bare value with `param()` -Interpolate a plain `number`, `string`, `boolean`, `bigint`, or `Uint8Array` directly. For anything else, such as a `Date` or a decimal you want stored as `numeric`, wrap the value in `param(value, { codecId })` and name the type id yourself. Import `param` from `@prisma/orm-postgres/relational-core/expression`. +Interpolate a plain `number`, `string`, `boolean`, `bigint`, or `Uint8Array` directly. For anything else, such as a point in time or a decimal you want stored as `numeric`, wrap the value in `param(value, { codecId })` and name the type id yourself. Import `param` from `@prisma/orm-postgres/relational-core/expression`. A bare value that you do interpolate directly gets the type id Prisma ORM picks from its JavaScript type: | The value you interpolate | The type id it gets | | --- | --- | | a whole `number` from `-2147483648` to `2147483647` | `pg/int4@1` | -| a larger whole `number`, up to `9007199254740991` | `pg/int8number@1` | +| a larger whole `number`, up to `9007199254740991` | `pg/int8number@1`, which reads back as a `number` | | any other `number`, including a fractional one | `pg/float8@1` | -| a `bigint` | `pg/int8@1` | +| a `bigint` | `pg/int8@1`, which reads back as a `bigint` | | a `string` | `pg/text@1` | | a `boolean` | `pg/bool@1` | | a `Uint8Array` | `pg/bytea@1` | -A type id is the PostgreSQL type name plus a version, always `@1` today. Four more you are likely to write are `pg/uuid@1`, `pg/numeric@1`, `pg/jsonb@1`, and `pg/timestamptz-temporal@1`. Those are the common ones, not the whole set. +A type id is the PostgreSQL type name plus a version, always `@1` today. Four more you are likely to write are `pg/uuid@1`, `pg/numeric@1`, `pg/jsonb@1`, and `pg/timestamptz-temporal@1`. Those are the common ones, not the whole set. The ids your contract uses are in `contract.json`. For any column, `db.sql.public.
.columns..codecId` is its id. A point in time is the case `param()` exists for, and `pg/timestamptz-temporal@1` takes a `Temporal.Instant`, the standard JavaScript object for a point in time, not a `Date`. ```ts import { param } from '@prisma/orm-postgres/relational-core/expression'; -const targetLength = param(15, { codecId: 'pg/int4@1' }); +const cutoff = param(Temporal.Instant.from('2024-01-01T00:00:00Z'), { codecId: 'pg/timestamptz-temporal@1' }); const plan = db.sql.public.user .select('id', 'email') - .where((f, fns) => fns.raw`LENGTH(${f.email}) = ${targetLength}`.returns('pg/bool@1')) + .where((f, fns) => fns.raw`${f.createdAt} > ${cutoff}`.returns('pg/bool@1')) .build(); const rows = await runtime.query(plan); -// users whose email is exactly 15 characters long +// users created after the start of 2024 ``` ### Declaring a nullable result @@ -122,7 +122,7 @@ const rows = await runtime.query(plan); `db.raw` is the client's raw API, beside `db.sql` and `db.orm`. On PostgreSQL it holds `sql`, the same tagged template as `fns.raw`. On MongoDB it holds `collection(...)` instead, which the [MongoDB raw commands](#mongodb-raw-commands) section covers. Unlike `fns.raw`, `db.raw.sql` works outside a builder callback, and it has the extra methods that turn a whole statement into a query you can run. -Use `.returns(...)` to build an expression ahead of time and use it inside a builder query: +Use `.returns(...)` to build an expression ahead of time and pass it to the object form of [`select()`](/orm/reference/sql-query-builder#select), which names each output column: ```ts const serverNow = db.raw.sql`now()`.returns('pg/timestamptz-temporal@1'); @@ -133,7 +133,7 @@ const plan = db.sql.public.user const rows = await runtime.query(plan); ``` -Use `.returnsRow(spec)` to run a whole statement and get converted rows back. The spec names the columns of every returned row. Each entry is either a column taken from your contract, such as `db.sql.public.user.columns.id`, which brings its own conversion, nullability, and TypeScript type, or an explicit type id for a column your contract has no counterpart for. +Use `.returnsRow(spec)` to run a whole statement and get converted rows back. The spec names the columns of every returned row. Each entry is either a column from your contract or a type id. A column such as `db.sql.public.user.columns.id` brings its own conversion, nullability, and TypeScript type. Write a type id for a column your contract has no match for. ```ts const user = db.sql.public.user; @@ -154,7 +154,7 @@ const rows = await runtime.query(plan); // row.email is a string; row.postCount is a bigint ``` -Use `.affectedCount()` for a statement that returns no rows. It takes no spec. Pass the built query to `runtime.execute(...)`, which resolves to an object, `{ affectedRows }`, whose `affectedRows` is the number of rows the statement changed. You can also run a built raw query inside a transaction: [`db.transaction()`](/orm/reference/transactions-and-runtime#dbtransactioncallback) gives you a `tx`, and `tx.query(...)` and `tx.execute(...)` take the same built queries as `runtime.query(...)` and `runtime.execute(...)` do. +Use `.affectedCount()` for a statement that returns no rows. It takes no spec. Pass the built query to `runtime.execute(...)`, which resolves to `{ affectedRows }`, the number of rows the statement changed. You can also run a built raw query inside a transaction: [`db.transaction()`](/orm/reference/transactions-and-runtime#dbtransactioncallback) gives you a `tx`, and `tx.query(...)` and `tx.execute(...)` take the same built queries as `runtime.query(...)` and `runtime.execute(...)` do. ```ts const plan = db.raw.sql` @@ -167,7 +167,7 @@ const stats = await runtime.execute(plan); // stats.affectedRows ``` -A raw query that returns rows can go inside another one. That is how you write a subquery or a CTE. Interpolate the object that `.returnsRow(spec)` gives you, before you call `.build()` on it, and its SQL and its values are put into the outer statement in order. Only the outer statement is built. The outer statement declares its own columns. For a column the inner query already declared, read it back from the inner object's `.returns` property, which is a record of the columns that query declared, rather than writing the type id out again. That property is a different thing from the `.returns(typeId)` method you call on a fragment. +You can nest a `.returnsRow()` query inside another. That is how you write a subquery or a CTE. Interpolate it before `.build()`, and call `.build()` on the outer one only. Its `.returns` property holds its declared columns, so `outer.returnsRow({ postCount: inner.returns.postCount })` reuses a declaration. Three names look alike. `.returns(typeId)` is a method on a fragment, `.returnsRow(spec)` is a method on a statement, and `.returns` is a property on the object `.returnsRow()` gives you. You cannot nest a statement that ends with `.affectedCount()`, because it produces no rows. You can nest an `UPDATE ... RETURNING`, because it does. ```ts const authorsWithPosts = db.raw.sql` @@ -186,8 +186,6 @@ const plan = db.raw.sql` `.returnsRow({ email: user.columns.email, postCount: authorsWithPosts.returns.postCount }).build(); ``` -You cannot interpolate a statement that ends with `.affectedCount()`, because it produces no rows. You can interpolate an `UPDATE ... RETURNING`, because it does. - ### Unsupported interpolation Interpolation accepts columns, expressions, `param(...)` values, raw queries that return rows, and the bare types `number`, `bigint`, `string`, `boolean`, and `Uint8Array`. Interpolating anything else, such as a `Date`, is rejected by TypeScript when you compile. In plain JavaScript the tagged template throws as soon as you call it, with an error whose `code` is `RUNTIME.RAW_SQL_UNSUPPORTED_INTERPOLATION` and this message: @@ -196,16 +194,16 @@ Interpolation accepts columns, expressions, `param(...)` values, raw queries tha unsupported JS value type for raw-SQL interpolation: wrap this value in `param(...)` with an explicit codec ``` -To bind a `Date`, or any value whose type is not in the list, wrap it in [`param(value, { codecId })`](#binding-a-bare-value-with-param) with the right type id. +To bind any value whose type is not in the list, wrap it in [`param(value, { codecId })`](#binding-a-bare-value-with-param) with the right type id. For a `Date`, convert it to a `Temporal.Instant` first and bind that. ## MongoDB raw commands -`db.raw.collection(collectionName)` returns an object with nine methods for running MongoDB commands directly against a collection. The collection name is the one your contract declares (`'users'`, `'posts'`), the same name the ORM client uses (`db.orm.users`). MongoDB has no schema segment, which is why there is no `public` here and none on `db.orm`. +`db.raw.collection(collectionName)` returns an object with nine methods for running MongoDB commands directly against a collection. The collection name is the one your contract declares (`'users'`, `'posts'`), the same name the ORM client uses (`db.orm.users`). MongoDB has no schemas, so there is no `public` segment here and none on `db.orm`. -Each method builds a command. Call `.build()` on it, then run it with `runtime.query(built)`. Every one of these methods goes through `query()`, including the writes. A write comes back as an array holding one result object, which is why the examples read it with `const [result] = ...`. +Each of these nine methods builds a command. Call `.build()` on it, then run it with `runtime.query(built)`, the inserts, updates, and deletes included. A write returns a one-element array, so destructure it with `const [result] = ...`. :::warning[MongoDB raw results are native BSON values] -Nothing on the MongoDB side of this page is converted for you. An `_id` comes back as the driver's own `ObjectId` instance, not a hex string, so compare one with `String(...)`. Raw filters take native BSON values too, so build ids with the driver's `ObjectId` class. The write methods give you the driver's own result objects (`{ insertedId }`, `{ matchedCount, modifiedCount }`, `{ deletedCount }`), and you read those keys directly. +Nothing on the MongoDB side of this page is converted for you. An `_id` comes back as an `ObjectId` instance from the `mongodb` package, the MongoDB driver underneath, not a hex string. Compare one with `String(row._id) === aliceId`. Raw filters take native BSON values too, so build ids with that package's `ObjectId` class. The write methods give you its own result objects, `{ insertedId }`, `{ matchedCount, modifiedCount, upsertedCount, upsertedId }`, and `{ deletedCount }`, and you read those keys directly. ::: The examples in this section run against the `users` / `posts` schema from the pipeline builder page. See its [example schema](/orm/reference/pipeline-builder#example-schema) for the full model definitions. Create the client with `mongo(...)`, get the connection that runs a built query from `await db.runtime()`, and declare the two example ids once: @@ -227,13 +225,13 @@ Run a raw aggregation pipeline against a collection. The type parameter says wha ```ts const plan = db.raw.collection('posts').aggregate<{ _id: unknown; title: string }>([{ $match: { title: 'Hello world' } }]).build(); -const rows = await runtime.query(plan); -// rows[0].title === 'Hello world'; every _id is a raw ObjectId, so use String(...) to compare +const [row] = await runtime.query(plan); +// row.title === 'Hello world'; row._id is a raw ObjectId ``` ### `insertOne()` and `insertMany()` -Insert one or many documents. Both return the driver's own insert result, with `ObjectId` instances for the generated ids. `insertMany()`'s `insertedIds` is a plain array of `ObjectId`, in the order you passed the documents. +Insert one or many documents. Both return the `mongodb` package's own insert result, with `ObjectId` instances for the generated ids. `insertMany()`'s `insertedIds` is a plain array of `ObjectId`, in the order you passed the documents. ```ts const users = db.raw.collection('users'); @@ -302,13 +300,14 @@ To sort, or to read the document after the update, build the command yourself an ```ts import { RawFindOneAndUpdateCommand } from '@prisma/orm-mongo/query-ast/execution'; +// collection, filter, update, upsert, sort, returnDocument const command = new RawFindOneAndUpdateCommand('users', { _id: new ObjectId(aliceId) }, { $inc: { count: 1 } }, true, { count: -1 }, 'after'); const [updated] = await runtime.query(db.query.rawCommand(command)); ``` ##### Upsert counter pattern -The **first** call to an upsert counter returns an **empty** result. The upsert creates a brand-new document, so there is no earlier version to return, the driver returns `null`, and the query yields no row. Read the counter value on the second call and later ones, each of which returns the version before its own increment. The example schema has no counter collection, so `users` stands in for one here. Its `_id` is the plain string `'pageViews'`, which MongoDB accepts: an `_id` can be any BSON value, and a raw command sends the string through unchanged. +The **first** call to an upsert counter returns an **empty** result, because there is no earlier version. Read the counter value on the second call and later ones, each of which returns the version before its own increment. The example schema has no counter collection, so `users` stands in for one here, with the plain string `'pageViews'` as its `_id`. ```ts const filter = { _id: 'pageViews' }; @@ -327,12 +326,13 @@ Atomically delete a matching document and return it. The yielded row is the raw ```ts const plan = db.raw.collection('users').findOneAndDelete({ _id: new ObjectId(aliceId) }).build(); const [deleted] = await runtime.query(plan); -// deleted is the removed document; deleted._id is a raw ObjectId +// deleted is the removed document, and its _id is a raw ObjectId +const isAlice = String(deleted._id) === aliceId; // true ``` ## `db.query.rawCommand()` -`db.query.rawCommand(command)` runs a raw MongoDB aggregate command through the [pipeline builder](/orm/reference/pipeline-builder), with no translation of the command you pass. Unlike `aggregate()` it takes no type parameter, so its rows come back as `unknown` and you cast them yourself. +`db.query.rawCommand(command)` runs a raw MongoDB aggregate command through the [pipeline builder](/orm/reference/pipeline-builder), with no translation of the command you pass. It returns a built query already, so do not call `.build()` on it. Unlike `aggregate()` it takes no type parameter, so its rows come back as `unknown` and you cast them yourself. ```ts import { RawAggregateCommand } from '@prisma/orm-mongo/query-ast/execution'; @@ -342,7 +342,7 @@ const rows = (await runtime.query(plan)) as { total: number }[]; // [{ total: 2 }] ``` -`rawCommand()` is the way out when the typed pipeline builder can't express a query. Two examples: filtering by `_id` equality, and `$redact` with `$$KEEP` / `$$PRUNE`. Both work here because the command you pass is sent unchanged, so a real `ObjectId` in a pipeline document matches: +`rawCommand()` is the way out when the typed pipeline builder can't express a query, such as filtering by `_id` equality. The command you pass is sent unchanged, so a real `ObjectId` in a pipeline document matches. `aggregate()` on `db.raw.collection()` handles `_id` filtering too, and it gives you a row type. Use `rawCommand()` when you are already working in `db.query` and need one raw command there. ```ts const plan = db.query.rawCommand( 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 018fa4e0ad7..a3c30e36712 100644 --- a/apps/docs/content/docs/orm/reference/sql-query-builder.mdx +++ b/apps/docs/content/docs/orm/reference/sql-query-builder.mdx @@ -16,7 +16,7 @@ The SQL query builder targets SQL databases; PostgreSQL and SQLite are supported ## Example schema -All examples on this page run against the following schema. The select and mutation examples use the `user`, `post`, `post_tag`, and `tag` tables. The grouped-query examples use a separate `customer` and `order` schema, shown under [Grouped queries](#grouped-queries). No example uses `Address`, `Task`, `Bug`, or `Feature`. They are here only so that this schema matches the one on the [ORM client reference](/orm/reference/orm-client), and you can ignore them. The `types` block, `@@type`, `@@discriminator`, and `@@base` are contract features documented under [Contract authoring](/orm/contract-authoring/psl-syntax), and no example on this page uses them. +All examples on this page run against the following schema. The select and mutation examples use the `user`, `post`, `post_tag`, and `tag` tables. The grouped-query examples use a separate `customer` and `order` schema, shown under [Grouped queries](#grouped-queries). No example uses `Address`, `Task`, `Bug`, or `Feature`. No example on this page uses `types`, `@@type`, `@@discriminator`, or `@@base`. See [Contract authoring](/orm/contract-authoring/psl-syntax).
@@ -128,17 +128,13 @@ model Feature { ## Entry points -Before you can build queries, run `npx prisma contract emit`. It reads your contract, the `contract.prisma` file that replaced `schema.prisma`, and writes `contract.json` and `contract.d.ts` next to it. The examples below import both. +In Prisma ORM 8 your `schema.prisma` is called `contract.prisma`. [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7) gets you from a Prisma ORM 7 project to that file, and has the rest of the setup steps. Run `npx prisma contract emit` to generate `contract.json` and `contract.d.ts` beside it. The examples below import both. Install `@prisma/orm-postgres`. -Install `@prisma/orm-postgres`. [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7) has the rest of the setup steps. - -You build queries from the client's `sql` property. Create a Postgres client with `postgres(...)`, then reach tables through `db.sql.public`. `public` is the PostgreSQL schema your tables are in. - -Table accessors are keyed by table name, not model name. A table name is the model name with a lowercase first letter unless the model sets `@@map`, so the `User` model mapped to `user` is reached as `db.sql.public.user`, and the model for the `Post`/`Tag` join table, mapped to `post_tag`, is reached as `db.sql.public.post_tag`. Column names are the field names unless a field sets `@map`. +You build queries from the client's `sql` property. Create a Postgres client with `postgres(...)`, then reach tables through `db.sql.public`. `public` is the PostgreSQL schema your tables are in, and Prisma ORM 8 puts every table in `public` today. Table accessors are keyed by table name, not model name. A table name is the model name with a lowercase first letter unless the model sets `@@map`, so the `User` model mapped to `user` is reached as `db.sql.public.user`, and the model for the `Post`/`Tag` join table, mapped to `post_tag`, is reached as `db.sql.public.post_tag`. Column names are the field names unless a field sets `@map`. ### The `db.sql` builder [#the-dbsql-facet] -`db.runtime()` returns the connection that runs a built query. Do not `await` it. Only `runtime.query()` is awaited. A `select()`, `insert()`, `update()`, or `delete()` call starts a query. Finish it with [`build()`](#build), which returns the built query, and pass that to `runtime.query(...)`. The examples name the built query `plan`. You can run the same built query more than once. +`db.runtime()` returns the connection that runs a built query. `db.runtime()` is synchronous and returns immediately. Only `runtime.query()` is awaited. Hold the connection in a variable and reuse it. Write the type import path as `./contract.d`, exactly like that. A `select()`, `insert()`, `update()`, or `delete()` call starts a query. Finish it with [`build()`](#build), which returns the built query, and pass that to `runtime.query(...)`. The examples name the built query `plan`. You can run the same built query more than once. ```ts import postgres from '@prisma/orm-postgres/runtime'; @@ -152,8 +148,6 @@ const plan = db.sql.public.user.select('id', 'email').build(); const users = await runtime.query(plan); ``` -`db.sql` is ready to use as soon as you have a client, so most code needs nothing more. If you wrap your own database client, the `sql(...)` function from `@prisma/orm-postgres/builder/runtime` builds the same query builder around that client. - SQLite works the same way. Create the client with `sqlite(...)` from `@prisma/orm-sqlite/runtime`. SQLite has no schemas, so there is no `public` in the path and the same table is `db.sql.user`. Of the SELECT methods on this page, `lateralJoin()`, `outerLateralJoin()`, and `distinctOn()` are not available on SQLite. Every other one is. The rest of this page shows PostgreSQL and writes the root as `db.sql.public`. ### Table access @@ -167,14 +161,7 @@ db.sql.public.tag.insert([{ label: 'typescript' }]); // start an INSERT ### Aliasing a query with `.as()` -Call `.as(alias)` on a finished `SELECT` query to use that query as a subquery. The aliased query is then something you can pass to `innerJoin()`, `outerLeftJoin()`, and the other join methods. See [Subquery via `.as()`](#subquery-via-as). - -```ts -const highPriorityPosts = db.sql.public.post - .select('id', 'userId') - .where((f, fns) => fns.eq(f.priority, 'high')) - .as('hp'); -``` +Call `.as(alias)` on a finished `SELECT` query to use that query as a subquery. The aliased query is then something you can pass to `innerJoin()`, `outerLeftJoin()`, and the other join methods. For the example, see [Subquery via `.as()`](#subquery-via-as). :::warning[`.as()` is not for lateral joins] Use `.as()` only for subqueries you pass to `innerJoin()`, `outerLeftJoin()`, `outerRightJoin()`, or `outerFullJoin()`. Inside a [`lateralJoin()`](#lateraljoin) callback, return the query chain itself and do not call `.as()` on it. See that method's Remarks. @@ -182,11 +169,11 @@ Use `.as()` only for subqueries you pass to `innerJoin()`, `outerLeftJoin()`, `o ## SELECT queries -These methods build and refine a `SELECT`. They chain, and you resolve the query with [`build()`](#build) followed by `runtime.query(...)`. `SelectQuery` in the tables below is the type they return. You never import it. +These methods build and refine a `SELECT`. They chain, and you resolve the query with [`build()`](#build) followed by `runtime.query(...)`. Chain the methods in any order, except that a join must come before any call that names the joined table's columns, and `distinctOn()` needs its columns first in `orderBy()`. `SelectQuery` in the tables below is the type they return. The set of column names a query can use is its scope. It starts as the columns of the table you began with, and every join adds the joined table's columns to it. -The `where()`, `select()`, `orderBy()`, `update()`, and other callback forms receive two arguments. The first is `f`, which has one property per column in scope. After a join, a column name that both tables have is on `f` only under its table name, such as `f.post.id`. The second is `fns`, an object of [expression helpers](#expressions-and-functions) such as `fns.eq()` and `fns.raw`. +The `where()`, `select()`, `orderBy()`, `update()`, and other callback forms receive two arguments. The first is `f`, which has one property per column in scope. After a join, a column name that both tables have is on `f` only under its table name, such as `f.post.id`. The second is `fns`, an object of [expression helpers](#expressions-and-functions) such as `fns.eq()` and `fns.raw`. For an enum column you pass the value stored in the database, the string after `=` in the enum, so `High = "high"` is matched by `fns.eq(f.priority, 'high')`. ### `select()` @@ -198,13 +185,13 @@ Reduce each row to a chosen set of columns or computed expressions. | Form | Signature | Description | |---|---|---| -| Column names | `select('col', 'col2', ...)` | Keep the named columns. A name that is not in the query's scope throws an error whose `code` is `ORM.COLUMN_UNKNOWN`, at the moment you call `select()`. | +| Column names | `select('col', 'col2', ...)` | Keep the named columns. A name that is not in the query's scope throws an error whose `code` is `ORM.COLUMN_UNKNOWN`. | | Aliased expression | `select(alias, (f, fns) => expr)` | Add one computed column under `alias`. | | Object of expressions | `select((f, fns) => ({ alias: expr, ... }))` | Add several computed columns at once. The row type is inferred from the object's shape. | Every form adds to what the query already returns. Calling `select()` a second time never replaces the first call's columns, whichever form you use. To return fewer columns, start a new query instead. -A computed expression's result type comes from `.returns(...)` on `fns.raw`, or from the operation's own declared return type. `.returns()` takes a type id, such as `'pg/int4@1'`. A type id is the PostgreSQL type name plus `@1`. The common ones are `pg/text@1`, `pg/int4@1`, `pg/int8@1`, `pg/float8@1`, `pg/bool@1`, `pg/uuid@1`, and `pg/timestamptz-temporal@1`. To reuse a column's own type, pass the column to `.returns()`, as in `.returns(db.sql.public.user.columns.id)`, or read the id itself from `db.sql.public.user.columns.id.codecId`. See [`fns.raw` and `.returns()`](#fnsraw-and-returns). +A computed expression's result type comes from `.returns(...)` on `fns.raw`, or from the `fns.*` function you called. Every `fns.raw` fragment needs `.returns()`. A type id is `pg/` plus the PostgreSQL type name plus `@1`, such as `pg/int4@1` or `pg/uuid@1`. A few use a longer name, such as `pg/timestamptz-temporal@1` for `DateTime`. The common ones are `pg/text@1`, `pg/int4@1`, `pg/int8@1`, `pg/float8@1`, `pg/bool@1`, `pg/uuid@1`, and `pg/timestamptz-temporal@1`. To reuse a column's own type, pass the column to `.returns()`, as in `.returns(db.sql.public.user.columns.id)`. `db.sql.public.user.columns.id.codecId` holds the id string. See [`fns.raw` and `.returns()`](#fnsraw-and-returns). #### Return type @@ -214,6 +201,16 @@ A computed expression's result type comes from `.returns(...)` on `fns.raw`, or #### Examples +The examples on this page use these ids, which stand for rows that exist: + +```ts +const aliceId = '00000000-0000-4000-8000-000000000001'; +const carolId = '00000000-0000-4000-8000-000000000003'; +const helloWorldId = '00000000-0000-4000-8000-000000000010'; +const untaggedPostId = '00000000-0000-4000-8000-000000000011'; +const newestPostId = '00000000-0000-4000-8000-000000000012'; +``` + ##### Project a subset of columns ```ts @@ -340,10 +337,10 @@ Correlate a per-row subquery against the outer row: for each outer row, the join #### Remarks -- **Availability:** PostgreSQL only. Not available on SQLite, where the call does not compile. Called from plain JavaScript, or after a cast, it throws an error whose `code` is `ORM.CAPABILITY_MISSING` at the moment you call `lateralJoin()`. `outerLateralJoin()` is the left-join form: same arguments, same availability, and the subquery's columns are nullable. +- **Availability:** PostgreSQL only. On SQLite TypeScript rejects the call. If you get past the types, it throws an error whose `code` is `ORM.CAPABILITY_MISSING`. `outerLateralJoin()` is the left-join form: same arguments, same availability, and the subquery's columns are nullable. - The callback receives a `lateral` builder. Start its subquery with `lateral.from(otherTable)`, then chain the usual `SELECT` methods. Inside the callback, the outer table's columns are on `f` under its table name, such as `f.user.id`, so the subquery can filter on the outer row. - Return the query chain **directly** from the callback. Do **not** call `.as(...)` on it. The first argument of `lateralJoin()` already names the subquery. -- After the join, a column name that both tables have must be written under its table, such as `f.post.id` or `f.post.createdAt`. A bare `select('id')` or `orderBy((f) => f.createdAt, ...)` throws, because a name both tables have is not in the query's scope on its own. +- A column name that both tables have must be written under its table name. Outside the callback, the subquery's columns are under the alias you gave it, such as `f.latestPost.id`. Inside the callback, the joined table is still `post`, so the same column is `f.post.id`. A bare `select('id')` or `orderBy((f) => f.createdAt, ...)` throws, because a name both tables have is not in the query's scope on its own. #### Options @@ -392,7 +389,7 @@ Sort the result set by a column or a computed expression, in a direction you cho | `key` | Column name (`string`) or `(f, fns) => Expression` | Yes | The column or computed value to sort by. | | `options.direction` | `'asc' \| 'desc'` | No | Sort direction. | -TypeScript also accepts a `nulls` option, but the builder ignores it and no `NULLS FIRST` or `NULLS LAST` reaches the SQL. Do not use it. +TypeScript also accepts a `nulls` option, but the builder ignores it and no `NULLS FIRST` or `NULLS LAST` reaches the SQL. Do not use it. To put nulls last, sort first by a computed value: an `fns.raw` fragment of the form `CASE WHEN IS NULL THEN 1 ELSE 0 END` that returns `pg/int4@1`. Then call `orderBy()` again for the column itself. #### Return type @@ -445,8 +442,8 @@ Keep the first row per distinct key, according to the query's `orderBy()` (`DIST #### Remarks -- **Availability:** PostgreSQL only. Not available on SQLite, where the call does not compile. Called from plain JavaScript, or after a cast, it throws an error whose `code` is `ORM.CAPABILITY_MISSING` at the moment you call `distinctOn()`. -- Put the `distinctOn()` columns first in `orderBy()`, then the column that decides which row wins. Nothing checks this for you, and without it you cannot predict which row you get. The order in which you call `distinctOn()` and `orderBy()` does not matter. +- **Availability:** PostgreSQL only. On SQLite TypeScript rejects the call. If you get past the types, it throws an error whose `code` is `ORM.CAPABILITY_MISSING`. +- List the `distinctOn()` columns first in `orderBy()`, then the column that picks the winner. #### Options @@ -483,8 +480,8 @@ Cap the number of returned rows and skip rows in the ordered result set. | Method | Argument | Description | |---|---|---| -| `limit(n)` | `number`, or a numeric expression | Return at most `n` rows. | -| `offset(n)` | `number`, or a numeric expression | Skip the first `n` rows. | +| `limit(n)` | `number` | Return at most `n` rows. | +| `offset(n)` | `number` | Skip the first `n` rows. | #### Return type @@ -541,31 +538,21 @@ const rows = await runtime.query(plan); ## Grouped queries -Grouping starts with [`groupBy()`](#groupby), which turns a select query into a `GroupedQuery`. Call it after your `select()` calls, because a grouped query has no `select()` of its own. A grouped query supports `having()` for group-level filtering, the aggregate functions, and the same `orderBy()`, `limit()`, `offset()`, and `distinct()` methods as a `SelectQuery`. +Grouping starts with [`groupBy()`](#groupby), which turns a select query into a `GroupedQuery`. Call `groupBy()` after all your `select()` calls. A grouped query supports `having()` for group-level filtering, the aggregate functions, and the same `orderBy()`, `limit()`, `offset()`, and `distinct()` methods as a `SelectQuery`. -The examples below use a separate `customer` / `order` schema, where `order.amount` is an integer column: +The examples below use a separate `order` schema, where `order.amount` is an integer column:
Expand for the aggregate example schema ```prisma -model Customer { - id Uuid @id @default(uuid()) - name String - orders Order[] - - @@map("customer") -} - model Order { id Uuid @id @default(uuid()) customerId Uuid amount Int placedAt DateTime @default(now()) - customer Customer @relation(fields: [customerId], references: [id]) - @@map("order") } ``` @@ -593,7 +580,7 @@ Group rows by one or more columns or by a computed expression, producing one row ##### Group by a column with a count -Every example below assumes `const runtime = db.runtime();`, and names the built query `plan`. +The examples below reuse the `runtime` created here, and name each built query `plan`, the object `build()` returns. ```ts const runtime = db.runtime(); @@ -604,19 +591,13 @@ const plan = db.sql.public.order .groupBy('customerId') .build(); const rows = await runtime.query(plan); -// e.g. { customerId: '', orderCount: 5 } +// rows === [{ customerId: '', orderCount: 5 }] ``` -:::note[Most aggregates give you a `number`] -`count()`, `sum()` over an integer column, and `avg()` over an integer column all return a JavaScript `number`. `min()` and `max()` return the input column's own type. -::: - -:::note[Large counts and sums need the `bigint` version] -A `count()` or integer `sum()` result larger than 2^53 - 1 fails with an error whose `code` is `RUNTIME.DECODE_FAILED`. Use `countBigInt()` or `sumBigInt(field)` when totals can get that large. -::: - -:::note[`avg()` loses precision instead of failing] -`avg()` is computed as a floating-point `number`, so a long decimal is rounded. Use `avgDecimal(field)` when you need the exact figure. +:::note[What the aggregates give you back] +- `count()`, `sum()` over an integer column, and `avg()` over an integer column all return a JavaScript `number`. `min()` and `max()` return the input column's own type. +- A `count()` or integer `sum()` result larger than 2^53 - 1 fails with an error whose `code` is `RUNTIME.DECODE_FAILED`. To check for a code, catch the error and compare `error.code`. Use `countBigInt()` or `sumBigInt(field)` when totals can get that large. +- `avg()` is computed as a floating-point `number`, so a long decimal is rounded. Use `avgDecimal(field)` when you need the exact figure. ::: ##### Group by a computed value @@ -625,14 +606,14 @@ A `count()` or integer `sum()` result larger than 2^53 - 1 fails with an error w const plan = db.sql.public.order .select('yearPlaced', (f, fns) => fns.raw`EXTRACT(YEAR FROM ${f.placedAt})`.returns('pg/int4@1')) .select('orderCount', (f, fns) => fns.count(f.id)) - .groupBy((f, fns) => fns.raw`EXTRACT(YEAR FROM ${f.placedAt})`.returns('pg/int4@1')) + .groupBy('yearPlaced') .build(); const rows = await runtime.query(plan); // rows === [{ yearPlaced: '2024', orderCount: 10 }] -// yearPlaced is the string '2024'. Write Number(row.yearPlaced) to get 2024. See `.returns()` below. +// `.returns()` only tells TypeScript what type to expect. It does not convert the value, so yearPlaced is the string '2024'. Write Number(row.yearPlaced) to get 2024. ``` -You can also group by an alias you gave a selected value, such as `.groupBy('yearPlaced')`. A name that is neither a column of the table you started from nor an alias you selected throws an error whose `code` is `ORM.COLUMN_UNKNOWN`, at the moment you call `groupBy()`. If you select a column you did not group or aggregate, the query fails when it runs, not when you build it. +Here `.groupBy('yearPlaced')` groups by the alias given to the selected value. You can also repeat the same `fns.raw` fragment inside `groupBy()`. A name that is neither a column of the table you started from nor an alias you selected throws an error whose `code` is `ORM.COLUMN_UNKNOWN`, at the moment you call `groupBy()`. If you select a column you did not group or aggregate, the query fails when it runs, not when you build it. That failure comes back as PostgreSQL's own error, not a Prisma ORM code. ### `having()` @@ -688,7 +669,7 @@ const rows = await runtime.query(plan); ### Ordering and limiting a grouped query -A `GroupedQuery` supports the same `orderBy()`, `limit()`, `offset()`, `distinct()`, and [`distinctOn()`](#distincton) methods as a `SelectQuery`. Sort by an aggregate alias to order groups. The alias must be one you selected in this query. Any other name throws an error whose `code` is `ORM.COLUMN_UNKNOWN`, at the moment you call `orderBy()`. On a grouped query, `distinct()` and `distinctOn()` drop repeated rows from the groups you already have, which only matters when you select fewer columns than you grouped by. +A `GroupedQuery` supports the same `orderBy()`, `limit()`, `offset()`, `distinct()`, and [`distinctOn()`](#distincton) methods as a `SelectQuery`. Sort by an aggregate alias to order groups. The alias must be one you selected in this query. Any other name throws an error whose `code` is `ORM.COLUMN_UNKNOWN`, at the moment you call `orderBy()`. #### Examples @@ -708,7 +689,7 @@ const rows = await runtime.query(plan); ## Mutations -Mutations start from a table with `insert()`, `update()`, or `delete()`, and end with [`build()`](#build). Add [`returning()`](#returning) to get the affected rows back. The examples below go back to this page's main schema, so they use `db.sql.public.tag` and `db.sql.public.user` again. +Mutations start from a table with `insert()`, `update()`, or `delete()`, and end with [`build()`](#build). Add [`returning()`](#returning) to get the affected rows back. Run a mutation with no `returning()` using `runtime.execute(plan)`, which resolves to `{ affectedRows }`. With `returning()`, run it with `runtime.query(plan)`. The examples below go back to this page's main schema, so they use `db.sql.public.tag` and `db.sql.public.user` again. ### `insert()` @@ -717,7 +698,8 @@ Insert one or more rows in a single statement. #### Remarks - `insert()` always takes an **array**. A single-row insert is a one-element array. There is no separate single-row overload. An empty array throws an error whose `code` is `ORM.MUTATION_DATA_MISSING`. -- Leave a column out and the contract's default for it is applied, such as `@default(uuid())` or `@default(now())`. The value is generated when you call `build()`, so build once and run once. Running the same built insert twice inserts the same generated value twice. Build it again for a new one. +- Leave a column out and the `@default(...)` in `contract.prisma` is applied, such as `@default(uuid())` or `@default(now())`. `build()` generates default values once. Running the same built insert twice writes the same id twice, which fails on a unique column. Call `.build()` again for each run. +- Leave out a required column that has no default and PostgreSQL rejects the insert when the query runs. - There is no `ON CONFLICT` and no upsert here. For "insert or update", use the ORM client's [`upsert()`](/orm/reference/orm-client#upsert) or write the statement with [`db.raw.sql`](/orm/reference/raw-queries). #### Options @@ -741,7 +723,7 @@ Each value is sent as a query parameter, never pasted into the SQL. The column i ```ts const plan = db.sql.public.tag.insert([{ label: 'single-row-tag' }]).build(); await runtime.execute(plan); -// `execute` runs a write that returns no rows, see [Running a built query](#executing-a-plan). id is filled in from @default(uuid()) +// execute runs a write that returns no rows. id is filled in from @default(uuid()) ``` ##### Insert multiple rows in one statement @@ -757,7 +739,8 @@ Return columns from the rows affected by an `insert()`, `update()`, or `delete() #### Remarks -- **Availability:** PostgreSQL and SQLite, the two databases the builder supports today. Both support SQL's `RETURNING`. +- **Availability:** PostgreSQL and SQLite. +- `returning()` takes column names only. There is no `returning('*')`, and you cannot return a computed expression. #### Options @@ -794,7 +777,7 @@ Update matched rows. Set columns with a values object, or derive new values from | Values object | `update({ col: value, ... })` | Set columns to fixed values. | | Expression callback | `update((f, fns) => ({ col: expr }))` | Set columns to expressions computed from existing columns. | -Every value the callback returns must be an expression, so you cannot put a plain JavaScript value beside a computed one. To set a fixed value in the callback form, write it as a `fns.raw` fragment with `.returns(...)`. +Every value the callback returns must be an expression. To set one column to a fixed value in the callback form, wrap that value in a `fns.raw` fragment, as the second example below does. #### Return type @@ -820,12 +803,15 @@ const rows = await runtime.query(plan); ```ts const plan = db.sql.public.user - .update((f, fns) => ({ displayName: fns.raw`UPPER(${f.displayName})`.returns('pg/text@1') })) + .update((f, fns) => ({ + displayName: fns.raw`UPPER(${f.displayName})`.returns('pg/text@1'), + email: fns.raw`${'carol@archived.example.com'}`.returns('pg/text@1'), + })) .where((f, fns) => fns.eq(f.email, 'carol@example.com')) .returning('id', 'displayName') .build(); const rows = await runtime.query(plan); -// one row, with displayName now 'CAROL' +// one row, with displayName now 'CAROL' and a fixed new email ``` ### `delete()` @@ -854,13 +840,13 @@ const rows = await runtime.query(plan); ### `param()` -Give a value its type by hand, for a value with no column to take the type from, such as a literal inside `fns.raw`. +Give a value its type by hand, for a value with no column to take the type from, such as a literal inside `fns.raw`. `param()` works in any query, not only in mutations. #### Remarks -- Import `param` from `@prisma/orm-postgres/relational-core/expression`. -- You rarely need `param()`. Values passed to `insert()`, `update()`, and comparison functions such as `fns.eq(f.col, value)` are already sent as query parameters, never pasted into the SQL, and each takes its type from the column it is used with. `param()` is for the values that have no such column. -- `codecId` is the type id, the name this page uses everywhere else. [Binding a bare value with `param()`](/orm/reference/raw-queries#binding-a-bare-value-with-param) lists the type id each kind of JavaScript value gets, which is where to look for a date, a uuid, a boolean, or an array. +- Import `param` from `@prisma/orm-postgres/relational-core/expression`. `param` and `ResultType` are in different subpaths of the same package, so use the paths shown. +- You rarely need `param()`. Values passed to `insert()`, `update()`, and comparison functions such as `fns.eq(f.col, value)` each take their type from the column they are used with. `param()` is for the values that have no such column. +- The option is called `codecId`, but it takes the same type id used everywhere else on this page. [Binding a bare value with `param()`](/orm/reference/raw-queries#binding-a-bare-value-with-param) lists the type id each kind of JavaScript value gets, which is where to look for a date, a uuid, a boolean, or an array. #### Options @@ -901,10 +887,12 @@ These exist on every database: | Category | Functions | |---|---| | Comparison | `eq(a, b)`, `ne(a, b)`, `gt(a, b)`, `gte(a, b)`, `lt(a, b)`, `lte(a, b)` | -| Membership | `in(expr, values)`, `notIn(expr, values)`, where `values` is an array or a built subquery | +| Membership | `in(expr, values)`, `notIn(expr, values)` | | Boolean | `and(...predicates)`, `or(...predicates)` | -| Existence | `exists(subquery)`, `notExists(subquery)`, taking a select query you do not call `build()` on | -| Raw SQL | `raw` | +| Existence | `exists(subquery)`, `notExists(subquery)` | +| Raw SQL | ``raw`...`.returns(typeId)`` | + +For `in()` and `notIn()`, `values` is an array or a subquery. All four of `in()`, `notIn()`, `exists()`, and `notExists()` take a select query that you do not call `build()` on. ```ts const named = db.sql.public.user @@ -912,6 +900,11 @@ const named = db.sql.public.user .where((f, fns) => fns.in(f.email, ['bob@example.com', 'carol@example.com'])) .build(); +const authorsById = db.sql.public.user + .select('id', 'email') + .where((f, fns) => fns.in(f.id, db.sql.public.post.select('userId'))) + .build(); + const authors = db.sql.public.user .select('id', 'email') .where((f, fns) => fns.exists(db.sql.public.post.select('id').where((inner, innerFns) => innerFns.eq(inner.userId, f.id)))) @@ -931,23 +924,23 @@ Which aggregate functions exist depends on your database. PostgreSQL gives you t | `min(field)` | The input column's own type. A `varchar` column is typed as `text` | | `max(field)` | The input column's own type. A `varchar` column is typed as `text` | -On PostgreSQL you also get `ilike(expr, pattern)` for text columns, as in `fns.ilike(f.displayName, '%alice%')`. `cosineDistance` comes with pgvector, so it is there only if your contract uses pgvector. +On PostgreSQL you also get `ilike(expr, pattern)` for text columns, as in `fns.ilike(f.displayName, '%alice%')`. `cosineDistance(a, b)` compares two vector values and returns a `number`. It comes with pgvector, the PostgreSQL extension for vector columns, so it is there only if your contract uses pgvector. ### `fns.raw` and `.returns()` -Write a raw SQL fragment as a tagged template. Interpolate columns, typed expressions, and bare JavaScript values with `${...}`. Each interpolation is sent as a query parameter, never pasted into the SQL. For a bare value, the builder picks the type id from its JavaScript type. [Binding a bare value with `param()`](/orm/reference/raw-queries#binding-a-bare-value-with-param) lists which JavaScript type becomes which type id, and shows how to choose a different one with `param(...)`. Call `.returns(codecId)` to declare the fragment's result type. +Write a raw SQL fragment as a tagged template. Interpolate columns, typed expressions, and bare JavaScript values with `${...}`. For a bare value, the builder picks the type id from its JavaScript type. [Binding a bare value with `param()`](/orm/reference/raw-queries#binding-a-bare-value-with-param) lists which JavaScript type becomes which type id, and shows how to choose a different one with `param(...)`. Call `.returns(codecId)` to declare the fragment's result type. #### Options | Name | Type | Required | Description | |---|---|---|---| | SQL fragment | Tagged template | Yes | The raw SQL, with `${...}` interpolations for columns and values. | -| `.returns(codecId)` | `string` | Yes, always | Declares the result type, for example `'pg/int4@1'`, `'pg/text@1'`. A fragment used as a `where()` predicate declares `'pg/bool@1'`. | +| `.returns(codecId)` | `string` | Yes, always | Declares the result type, for example `'pg/int4@1'`, `'pg/text@1'`. For a fragment used as a `where()` predicate, write `.returns('pg/bool@1')`. | #### Remarks -- **`.returns()` is always required.** A `fns.raw` fragment only becomes an expression once you call it, so there is nowhere in the builder you can use a fragment without it. -- **`.returns()` only tells TypeScript what type to expect.** It does not change the value you actually get back. PostgreSQL computes `EXTRACT` as a `numeric`, and a `numeric` reaches you as a string even though you annotated `'pg/int4@1'`. Writing `CAST(... AS int4)` inside the fragment does not change that either. To get a number, either convert it in JavaScript with `Number(row.yearPlaced)`, or select a real column of that type, or write the whole statement with [`db.raw.sql` and `.returnsRow()`](/orm/reference/raw-queries), which does read the value back as its declared type. +- **Always call `.returns()`.** Nothing in the builder accepts a fragment without it. +- **`.returns()` only tells TypeScript what type to expect.** It does not convert the value. PostgreSQL computes `EXTRACT(...)` as `numeric`, which arrives as a string, so `yearPlaced` is `'2024'`. Convert it in JavaScript with `Number(...)`, or run the statement with [`db.raw.sql` and `.returnsRow()`](/orm/reference/raw-queries), which does decode. - Use `fns.raw` for any SQL feature without a dedicated helper: `COALESCE`, `CAST`, `LENGTH`, `UPPER`, `EXTRACT`, and so on. There is no `fns.coalesce` and no `fns.cast`. #### Examples @@ -974,11 +967,17 @@ Compile a query so you can run it. | Return type | Example | Description | |---|---|---| -| Built query | `db.sql.public.user.select('id').build()` | A query you run with `runtime.query(...)`. Its per-row type is recoverable with [`ResultType`](#resulttype). | +| Built query | `db.sql.public.user.select('id').build()` | A query you run with `runtime.query(...)`. You can get the TypeScript type of one row with [`ResultType`](#resulttype). | ### Running a built query [#executing-a-plan] -Run a built query with `runtime.query(...)`, where `runtime` is `db.runtime()`. The rule is: use `query` for anything that returns rows, which means a `SELECT` or a write with `returning()`. Use `execute` for a write with no `returning()`. `query` returns an `AsyncIterableResult`. `await` it for an array of rows (`Row[]`), or `for await` over it to take one row at a time. `execute` resolves to `{ affectedRows }`. +Run a built query with `runtime`, which is `db.runtime()`. + +| Call | What you get | +|---|---| +| `runtime.query(plan)` | Rows. `await` it for an array of rows (`Row[]`), or `for await` over it to take one row at a time. | +| `runtime.execute(plan)` | `{ affectedRows }`. | +| Which one | `query` for anything that returns rows, which means a `SELECT` or a write with `returning()`. `execute` for everything else. | ### `ResultType` @@ -1005,8 +1004,6 @@ const rows = await runtime.query(plan); // Row[] ### Streaming vs. collecting -`await runtime.query(plan)` collects every row into an array. This is the common case and what every example on this page uses. You can also loop the same result with `for await` to handle one row at a time. - -`for await` does not save memory on PostgreSQL: the client loads every row first, and both `await` and `for await` read that same in-memory result. The connection goes back to the pool before you read the rows. +`await runtime.query(plan)` collects every row into an array. This is the common case and what every example on this page uses. You can also loop the same result with `for await` to handle one row at a time. Use `for await` when you want to start handling rows before the array is assembled. It does not reduce memory on PostgreSQL, because both `await` and `for await` read the same result the client has already loaded. You can `await` a result as many times as you like. You cannot mix `await` and `for await` on the same result, and you cannot `for await` it twice. Either one throws an error whose `code` is `RUNTIME.ITERATOR_CONSUMED`. See [`AsyncIterableResult`](/orm/reference/orm-client#asynciterableresult). diff --git a/apps/docs/content/docs/orm/reference/transactions-and-runtime.mdx b/apps/docs/content/docs/orm/reference/transactions-and-runtime.mdx index 5454ffd3763..25c424a0aa0 100644 --- a/apps/docs/content/docs/orm/reference/transactions-and-runtime.mdx +++ b/apps/docs/content/docs/orm/reference/transactions-and-runtime.mdx @@ -6,13 +6,11 @@ metaTitle: Prisma ORM transactions and runtime reference metaDescription: 'Reference for the Prisma ORM client lifecycle, transactions, prepared statements, and execution options.' --- -Prisma ORM 8 replaces `schema.prisma` with `contract.prisma`. Run `npx prisma contract emit` to compile that file into `contract.json`, which your app reads at run time, and `contract.d.ts`, which TypeScript reads. A new project keeps all three files in `src/prisma/`. Every client you create needs `contract.json`. [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7) lists the packages to install, the commands to run, and what replaces `prisma.user.findMany()`. +Prisma ORM 8 renames `schema.prisma` to `contract.prisma`. Run `npx prisma contract emit`. It writes two files next to it: `contract.json`, which your app imports, and `contract.d.ts`, which gives you the TypeScript types. A new project keeps all three files in `src/prisma/`. Every client you create needs `contract.json`. The query you came for, `prisma.user.findMany({ where })`, is now `db.orm.public.User.where(...).all()`. [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7) lists the packages to install, the commands to run, and the rest of the renamed calls. -The runtime is the connected object you run queries on. `db.runtime()` returns it. +`db` is the client `postgres(...)` or `mongo(...)` returns. The runtime is the connected object you run queries on, and `db.runtime()` returns it. The import path `@prisma/orm-postgres/runtime` is a package name and is not that object. -This page documents the client lifecycle (create, connect, close), transactions, prepared statements, and per-query execution options across PostgreSQL and MongoDB. Availability is stated per method: where the two databases differ, or where a method exists on only one of them, the Remarks say so. Transactions are fully supported on PostgreSQL. They are not available on MongoDB. To run one there, share one `MongoClient` between Prisma ORM and your own `mongodb` driver code and use a driver session. See [Transactions (MongoDB)](#transactions-mongodb). - -For a task-oriented walkthrough, see the Fundamentals guide to [Transactions](/orm/fundamentals/transactions). For the ways to query inside a transaction, see the [ORM client reference](/orm/reference/orm-client), the [SQL query builder reference](/orm/reference/sql-query-builder), and the [raw queries reference](/orm/reference/raw-queries). +This page documents the client lifecycle (create, connect, close), transactions, prepared statements, and per-query execution options across PostgreSQL and MongoDB. Each method below has a Remarks list. If a method behaves differently on PostgreSQL and MongoDB, or exists on only one of them, that list says so. Transactions are fully supported on PostgreSQL. They are not available on MongoDB. To run one there, pass your own `MongoClient` with the `mongoClient` option and use the driver's `session.withTransaction(...)`. See [Transactions on MongoDB](/orm/fundamentals/transactions#transactions-on-mongodb) for the code. For a task-oriented walkthrough, see the Fundamentals guide to [Transactions](/orm/fundamentals/transactions). For the ways to query inside a transaction, see the [ORM client reference](/orm/reference/orm-client), the [SQL query builder reference](/orm/reference/sql-query-builder), and the [raw queries reference](/orm/reference/raw-queries). ## Client lifecycle on PostgreSQL @@ -24,13 +22,13 @@ Create a PostgreSQL client. #### Remarks -- Import `contract.json` and pass it as `contractJson`, as every example below does. Pass `contract` instead only when you already hold a contract object in your own code, because Prisma ORM converts it to the same JSON. Supply exactly one. +- Import `contract.json` and pass it as `contractJson`, as every example below does. Pass `contract` only if you already hold a contract object; most code passes `contractJson`. Supply exactly one. - The `` type argument is what types `db.orm` and `db.sql`. Pass it yourself whenever you pass `contractJson`, because TypeScript cannot read a type out of a JSON file. It is inferred only on the `contract` path. -- Tell the client which database to use in one of three ways: `url`, `pg`, or `binding`. Use `binding` when your code chooses between them at run time, because it is one object instead of three separate option names. Its three kinds are `{ kind: 'url', url }`, `{ kind: 'pgPool', pool }`, and `{ kind: 'pgClient', client }`. Pass exactly one of `url`, `pg`, and `binding`, or leave all three out and pass one to [`connect()`](#connect). +- Tell the client which database to use in one of three ways: `url`, `pg`, or `binding`. A `binding` is one object that names the source and carries it: `{ kind: 'url', url }`, `{ kind: 'pgPool', pool }`, or `{ kind: 'pgClient', client }`. Use it when your code picks the source at run time. Pass exactly one of `url`, `pg`, and `binding`, or leave all three out and pass one to [`connect()`](#connect). - When you pass a `pg` pool or client, you own it and you close it. When you pass a `url`, the client creates the pool and closes it. Set that pool's timeouts with `poolOptions`. `poolOptions` has no effect on a pool you supply yourself. - Some types come from a PostgreSQL extension, such as a `pgvector.Vector(1536)` type in your `contract.prisma`. Run `npm install @prisma/orm-extension-pgvector` first. Then `import pgvector from '@prisma/orm-extension-pgvector/runtime'` and pass `extensions: [pgvector]`. -- A middleware is an object with a name and one or more hooks, such as `{ name: 'no-big-deletes', beforeQuery: (query) => { if (isForbidden(query)) throw new Error('blocked') } }`. Throw from a hook to block the query. See [How middleware works](/orm/middleware/how-middleware-works). -- Your migrations write a row into your database recording which contract that database matches. With `verifyMarker: 'onFirstUse'`, the default, the first query compares that row with your contract, logs a warning if the row is missing or does not match, and runs the query anyway. A warning usually means you have not run your migrations, so run them. `verifyMarker: false` skips the check. See [How migrations work](/orm/migrations/how-migrations-work). +- A middleware is an object with a name and one or more hooks, such as `{ name: 'no-big-deletes', beforeQuery: (query) => { if (query.sql.includes('DELETE')) throw new Error('blocked') } }`. Throw from a hook to block the query. The hooks are `beforeCompile`, `beforeQuery`, `beforeExecute`, `interceptQuery`, `interceptExecute`, `onRow`, `afterQuery`, and `afterExecute`. See [How middleware works](/orm/middleware/how-middleware-works). +- `verifyMarker: 'onFirstUse'`, the default, makes the first query check that the database was set up for your contract. If it was not, Prisma ORM logs a warning and runs the query anyway. The usual fix is to apply your migrations with `npx prisma db update`. `verifyMarker: false` skips the check. See [How migrations work](/orm/migrations/how-migrations-work). - `postgres(options)` does not open a connection. The client opens one on first use, so [`connect()`](#connect) is optional: call it to open the connection up front and fail early. #### Options @@ -44,7 +42,7 @@ Pass exactly one of `url`, `pg`, and `binding`. | `pg` | `Pool` or `Client` from the `pg` package | See note | An existing `pg` instance to use. You close it yourself. | | `binding` | `{ kind: 'url' \| 'pgPool' \| 'pgClient', ... }` | See note | The same three choices in one object. | | `poolOptions` | `{ connectionTimeoutMillis?: number; idleTimeoutMillis?: number }` | No | Timeouts for the pool the client creates from `url` (defaults `20000` / `30000`). | -| `extensions` | Array of extension packages | No | The packages for PostgreSQL extensions your contract's types need. | +| `extensions` | Array of extension packages | No | The npm packages for the PostgreSQL extensions your contract's types need. Install each one, then import it and put it in this array. | | `middleware` | Array of middleware | No | Code that runs around every query. See [How middleware works](/orm/middleware/how-middleware-works). | | `verifyMarker` | `'onFirstUse'` or `false` | No | Whether the first query checks that the database was set up for this contract. Defaults to `'onFirstUse'`. | @@ -55,10 +53,9 @@ Pass exactly one of `url`, `pg`, and `binding`. | `PostgresClient` | `postgres({ contractJson, url })` | A client that has not connected yet. Its members are listed below. | - `.orm` holds your models by model name. See the [ORM client reference](/orm/reference/orm-client). -- `.sql` is the SQL query builder, keyed by table name. The table name is the model's `@@map` value, or the model name with a lowercase first letter when the model sets no `@@map`. See the [SQL query builder reference](/orm/reference/sql-query-builder). `.raw` writes raw SQL as a template string. See the [raw queries reference](/orm/reference/raw-queries). -- `.enums` holds the enum values from your contract, by PostgreSQL schema: `db.enums.public.Role`. `.nativeEnums` holds the values of enum types that exist in PostgreSQL itself, created with `CREATE TYPE`, also by schema: `db.nativeEnums.public['AalLevel'].values` is `['aal1', 'aal2']`. In `.nativeEnums` each member's name is its own value, because a PostgreSQL enum type has values and no separate names. -- `.contract` is the contract as an object. `.context` is internal, not for you. -- [`connect()`](#connect), [`runtime()`](#runtime), [`transaction()`](#dbtransactioncallback), [`prepare()`](#dbpreparedeclaration-callback), and [`close()`](#close) are documented on this page. +- `.sql` is the SQL query builder, keyed by schema, then table name: `db.sql.public.tag`. The table name is the model's `@@map` value, or the model name with a lowercase first letter when the model sets no `@@map`. See the [SQL query builder reference](/orm/reference/sql-query-builder). `.raw` writes raw SQL as a template string. See the [raw queries reference](/orm/reference/raw-queries). +- `.enums` holds the enum values from your contract, by PostgreSQL schema: `db.enums.public.Role`. `.nativeEnums` holds the values of enum types that exist in PostgreSQL itself, created with `CREATE TYPE`, also by schema. Look the type up by name in brackets: `db.nativeEnums.public['AalLevel'].values` is `['aal1', 'aal2']`. A PostgreSQL enum type has values and no separate names, so each value is its own name: `db.nativeEnums.public['AalLevel'].nameOf('aal1')` is `'aal1'`. +- `.contract` is the contract as an object, and `.context` is internal. [`connect()`](#connect), [`runtime()`](#runtime), [`transaction()`](#dbtransactioncallback), [`prepare()`](#dbpreparedeclaration-callback), and [`close()`](#close) are documented on this page. #### Examples @@ -69,9 +66,11 @@ import postgres from '@prisma/orm-postgres/runtime'; import type { Contract } from './contract.d'; import contractJson from './contract.json' with { type: 'json' }; -const db = postgres({ contractJson, url: process.env.DATABASE_URL }); +const db = postgres({ contractJson, url: process.env.DATABASE_URL! }); ``` +These examples assume the file is beside `contract.json` in `src/prisma/`. From anywhere else, change the relative paths. Write `./contract.d` exactly like that, with no extension. The line `import contractJson from './contract.json' with { type: 'json' }` is what `prisma orm init` writes, and it needs Node.js 22.18 or newer and TypeScript set up as `prisma orm init` sets it up. TypeScript types every `process.env` value as possibly undefined, so the examples write `!` after it, the same way `prisma orm init` does. + ##### Bind to an existing `pg` pool ```typescript @@ -80,7 +79,7 @@ import { Pool } from 'pg'; import type { Contract } from './contract.d'; import contractJson from './contract.json' with { type: 'json' }; -const pool = new Pool({ connectionString: process.env.DATABASE_URL }); +const pool = new Pool({ connectionString: process.env.DATABASE_URL! }); const db = postgres({ contractJson, pg: pool }); // you own `pool`; close it yourself when the client is done ``` @@ -92,14 +91,15 @@ Open a connection and return a runtime. #### Remarks - Returns a `Promise`. The runtime is the connected object that runs queries. It has five calls: - - `runtime.query(built)` runs a built query and returns its rows. A built query is the object you get from `.build()` at the end of a builder or raw chain. See [`runtime.query()`](#runtimequery). - - `runtime.execute(built)` runs a built query that returns no rows, such as an insert, and returns `{ affectedRows }`. + - `runtime.query(built)` runs a built query and returns its rows. A built query is the object you get from `.build()` at the end of a builder or raw chain. See [Running a built query](/orm/reference/sql-query-builder#executing-a-plan). + - `runtime.execute(built)` runs a built query that returns no rows, such as an insert, and returns `{ affectedRows }`. See [Running a built query](/orm/reference/sql-query-builder#executing-a-plan). - `runtime.connection()` takes one connection out of the pool so that several queries run on the same connection. Call `connection.release()` when you are done, to put it back. See [Manual connection and transaction control](#manual-connection-and-transaction-control). - - `runtime.prepare(...)` prepares a statement you run many times with different values. See [Prepared statements (PostgreSQL)](#prepared-statements-postgresql). + - `runtime.prepare(...)` prepares a statement you run many times with different values. The client has a `prepare()` too, and the two do the same thing. See [Prepared statements (PostgreSQL)](#prepared-statements-postgresql). - `runtime.telemetry()` reports how the most recent query went. See [`runtime.telemetry()`](#runtimetelemetry). -- `connect()` takes the same database options as `postgres(...)`, so a client you created without one can be given its database here: `await db.connect({ url: process.env.DATABASE_URL })`, or `await db.connect({ binding: { kind: 'url', url } })`. -- Every error has a `code` property. Catch the error and compare `error.code` with the value shown here, for example `if (error.code === 'DRIVER.NOT_CONNECTED')`. -- Calling `connect()` once the client has already connected rejects with an error whose `code` is `DRIVER.ALREADY_CONNECTED`. Calling `connect()` after the client has been closed rejects with an error whose `code` is `DRIVER.NOT_CONNECTED` (`Postgres client is closed`). +- `connect()` takes the same database options as `postgres(...)`, so a client you created without one can be given its database here: `await db.connect({ url: process.env.DATABASE_URL! })`, or `await db.connect({ binding: { kind: 'url', url } })`. +- Every error has a `code` property, and the [error reference](/orm/reference/error-reference) lists them all. Catch the error and compare `error.code` with the value shown here, for example `if (error.code === 'DRIVER.NOT_CONNECTED')`. +- Call `connect()` before any query, or not at all. Running a query connects the client, so a `connect()` after that rejects with an error whose `code` is `DRIVER.ALREADY_CONNECTED`. So does a second `connect()`. +- Calling `connect()` after the client has been closed rejects with an error whose `code` is `DRIVER.NOT_CONNECTED` (`Postgres client is closed`). #### Return type @@ -113,7 +113,6 @@ Open a connection and return a runtime. ```typescript const runtime = await db.connect(); -// `public` is the PostgreSQL schema; `tag` is the table behind your `Tag` model const tags = await runtime.query(db.sql.public.tag.select('id', 'label').build()); ``` @@ -137,7 +136,9 @@ Close the client and release its pool. #### Remarks -- Returns a `Promise`. It waits for a connection already being opened, then releases the pool it created. After `close()`, `connect()` rejects and `runtime()` throws an error whose `code` is `DRIVER.NOT_CONNECTED` (`Postgres client is closed`). When you bound the client with `pg` (your own pool or client), close that instance yourself, because the client only owns pools it created from a `url`. +- Returns a `Promise`. If a connection is being opened when you call it, `close()` waits for that to finish, then releases the pool it created. +- After `close()`, `connect()` rejects and `runtime()` throws an error whose `code` is `DRIVER.NOT_CONNECTED` (`Postgres client is closed`). +- The client only owns pools it created from a `url`. When you passed your own `pg` pool or client, close that instance yourself. #### Return type @@ -150,7 +151,7 @@ Close the client and release its pool. ##### Close when finished ```typescript -const db = postgres({ contractJson, url: process.env.DATABASE_URL }); +const db = postgres({ contractJson, url: process.env.DATABASE_URL! }); const tags = await db.runtime().query(db.sql.public.tag.select('id', 'label').build()); await db.close(); ``` @@ -170,7 +171,7 @@ The client implements `Symbol.asyncDispose`, so `await using` closes it automati ```typescript { - await using db = postgres({ contractJson, url: process.env.DATABASE_URL }); + await using db = postgres({ contractJson, url: process.env.DATABASE_URL! }); const tags = await db.runtime().query(db.sql.public.tag.select('id', 'label').build()); } // db is closed here ``` @@ -186,10 +187,9 @@ Create a MongoDB client. #### Remarks - Pass the contract with `contractJson` or `contract` (supply exactly one), the same way as on PostgreSQL. -- Tell the client which database to use in one of four ways: `url`, `uri`, `mongoClient`, or `binding`. Use `binding` when your code chooses between them at run time, because it is one object instead of separate option names. It has two kinds, `{ kind: 'url', url, dbName }` and `{ kind: 'mongoClient', client, dbName }`. The `uri` path uses the `url` kind. +- Tell the client which database to use in one of four ways: `url`, `uri`, `mongoClient`, or `binding`. A `binding` is one object that names the source and carries it, in two kinds: `{ kind: 'url', url, dbName }` and `{ kind: 'mongoClient', client, dbName }`. Use it when your code picks the source at run time. The `uri` path uses the `url` kind. - Pass exactly one of `url`, `uri`, `mongoClient`, and `binding`, or leave all four out and pass one to [`connect()`](#connect-1). `dbName` is a separate option and does not count as one of the four, so `{ url, dbName }` is allowed. -- With `url`, the database name comes from the path of the connection string, as in `mongodb://host:27017/app`. Add `dbName` to override that name, or when the string has no name in its path. -- With `uri`, Prisma ORM never reads the database name from the string, so `dbName` is required. That is the only difference between `uri` and `url`. +- With `url`, the database name comes from the path of the connection string, as in `mongodb://host:27017/app`. Add `dbName` to override that name, or when the string has no name in its path. With `uri`, Prisma ORM never reads the database name from the string, so `dbName` is required. That is the only difference between `uri` and `url`. - **Client ownership**: with `url` or `uri`, Prisma ORM creates the underlying `MongoClient` and closes it on `close()`. With `mongoClient`, you supplied the client, so Prisma ORM does **not** close it. Because `close()` leaves your client open, you can use one `MongoClient` both through Prisma ORM and in code you write against the `mongodb` package directly. You close that client yourself. - `mongo(options)` does not open a connection. The runtime is built on first use, or explicitly via [`connect()`](#connect-1). @@ -205,13 +205,13 @@ Pass exactly one of `url`, `uri`, `mongoClient`, and `binding`. `dbName` is sepa | `mongoClient` + `dbName` | `MongoClient` + `string` | See note | An existing `MongoClient` you own, plus the database name. | | `binding` | `{ kind: 'url' \| 'mongoClient', ... }` | See note | The same choices in one object. | | `middleware` | Array of middleware | No | Code that runs around every query. See [How middleware works](/orm/middleware/how-middleware-works). | -| `mode` | `'strict'` or `'permissive'` | No | No built-in behaviour. Your own middleware can read it. Defaults to `'strict'`. | +| `mode` | `'strict'` or `'permissive'` | No | Defaults to `'strict'`. Only your own middleware reads it. | #### Return type | Return type | Example | Description | |---|---|---| -| `MongoClient` | `mongo({ contractJson, url, dbName })` | Prisma ORM's `MongoClient` type, not the driver's. A client that has not connected yet. | +| `MongoClient` | `mongo({ contractJson, url, dbName })` | A client that has not connected yet. This is Prisma ORM's own type. The `mongodb` driver has a class of the same name, so when you import both, name this one `db`. | - `.orm` holds your models by collection name, with no schema segment: `db.orm.users`. The collection name is the model's `@@map` value, or the model name with a lowercase first letter when the model sets no `@@map`. The [example schema](/orm/reference/orm-client#example-schema) sets `@@map("users")` on `User`, which is why the key here is `users`. See the [ORM client reference](/orm/reference/orm-client). - `.query` is the pipeline builder, which builds MongoDB aggregation pipelines. See the [pipeline builder reference](/orm/reference/pipeline-builder). `.raw` sends raw MongoDB commands. See the [raw queries reference](/orm/reference/raw-queries). @@ -226,7 +226,7 @@ import mongo from '@prisma/orm-mongo/runtime'; import type { Contract } from './contract.d'; import contractJson from './contract.json' with { type: 'json' }; -const db = mongo({ contractJson, url: process.env.MONGODB_URL, dbName: 'app' }); +const db = mongo({ contractJson, url: process.env.MONGODB_URL!, dbName: 'app' }); ``` ##### Share an existing `MongoClient` @@ -249,8 +249,8 @@ Open a connection and return a runtime. #### Remarks -- Returns a `Promise`. Calling `connect()` when the client is already connected rejects with an error whose `code` is `DRIVER.ALREADY_CONNECTED`. Running any query connects the client too, so if you then call `connect()`, it fails for the same reason. PostgreSQL raises the same code from the same situation. -- `connect()` takes the same database options as `mongo(...)`, so a client you created without one can be given its database here: `await db.connect({ url: process.env.MONGODB_URL, dbName: 'app' })`. +- Returns a `Promise`. Call `connect()` before any query, or not at all. Running a query connects the client, so a `connect()` after that rejects with an error whose `code` is `DRIVER.ALREADY_CONNECTED`. So does a second `connect()`. +- `connect()` takes the same database options as `mongo(...)`, so a client you created without one can be given its database here: `await db.connect({ url: process.env.MONGODB_URL!, dbName: 'app' })`. - After the client has been closed, `connect()` rejects with an error whose `code` is `DRIVER.NOT_CONNECTED` (`Mongo client is closed`). #### Return type @@ -306,7 +306,7 @@ Close the client. #### Remarks -- Call `db.close()`, not `runtime.close()`. `db.close()` marks the client closed and then closes the `MongoClient` Prisma ORM created. `runtime.close()` closes only the connection Prisma ORM opened and does not mark the client closed, so the client then looks usable and is not. +- Call `db.close()`, not `runtime.close()`. `db.close()` marks the client closed and then closes the `MongoClient` Prisma ORM created. After `runtime.close()` the client still accepts calls and then fails. - Returns a `Promise`. After `close()`, any further use rejects with an error whose `code` is `DRIVER.NOT_CONNECTED` (`Mongo client is closed`). That covers `db.runtime()` and ORM access such as `db.orm.users.first()`, since both go through the same runtime. When you supplied a `mongoClient`, `close()` does not close your client (see [`mongo(options)`](#mongooptions)). #### Return type @@ -338,14 +338,14 @@ The MongoDB client implements `Symbol.asyncDispose` too, so `await using` closes ```typescript { - await using db = mongo({ contractJson, url: process.env.MONGODB_URL, dbName: 'app' }); + await using db = mongo({ contractJson, url: process.env.MONGODB_URL!, dbName: 'app' }); await db.connect(); // ... run queries ... } // db is closed here ``` ## Transactions (PostgreSQL) -A transaction groups several writes into one unit: they all commit together, or they all roll back. Use `db.transaction(...)` in application code. Use `withTransaction(...)` in a helper or a library that is handed a runtime rather than a client. Open a connection and drive the transaction yourself when you need the same connection between statements. +A transaction groups several writes into one unit: they all commit together, or they all roll back. Use `db.transaction(...)` in application code. If you are writing a function that takes a runtime as a parameter instead of the client, use `withTransaction(...)`. Open a connection and drive the transaction yourself when you need statements on one connection outside a transaction, such as a `SET` before you begin. ### `db.transaction(callback)` @@ -354,13 +354,14 @@ Run a callback inside a transaction. The transaction commits when the callback r #### Remarks - Query through `tx`, not `db`. `tx.orm` holds your models. Build queries with `tx.sql`, then run them with `tx.query(...)` to get rows back, or `tx.execute(...)` when the write returns no rows. Every call on `tx` uses the same transaction connection, and queries on `db` run outside the transaction. -- `db.transaction(...)` takes the callback and nothing else. There are no `isolationLevel`, `timeout`, or `maxWait` options, and Prisma ORM offers no way to set the isolation level. Nothing retries a failed transaction for you, so when PostgreSQL reports a serialization failure, call `db.transaction(...)` again yourself. -- To bound the queries inside a transaction, pass a `signal` to each `tx.query(...)` and `tx.execute(...)` call, as the example below does. See [`RuntimeExecuteOptions`](#runtimeexecuteoptions). -- `tx` has no `transaction()` method, so transactions do not nest. If a helper you call starts its own transaction, change it to take `tx` as a parameter instead. Annotate that parameter with `TransactionContext`, imported from `@prisma/orm-postgres/family-runtime`, which covers `query` and `execute`. +- `db.transaction(...)` takes the callback and nothing else. There are no `isolationLevel`, `timeout`, or `maxWait` options, and Prisma ORM offers no way to set the isolation level. Nothing retries a failed transaction for you. A transaction has no time limit by default. To set one, set PostgreSQL's `idle_in_transaction_session_timeout` on the connection. +- To put a time limit on the queries inside a transaction, pass a `signal` to each `tx.query(...)` and `tx.execute(...)` call, as the example below does. See [`RuntimeExecuteOptions`](#runtimeexecuteoptions). +- `tx` has no `transaction()` method, so transactions do not nest. If a helper you call starts its own transaction, change it to take `tx` as a parameter instead. `TransactionContext`, imported from `@prisma/orm-postgres/family-runtime`, has `query` and `execute` only, so a helper typed with it cannot use `tx.orm` or `tx.sql`: `async function addTag(tx: TransactionContext) { ... }`. To pass the full `tx`, take its type from the client: `type Tx = Parameters[0]>[0]`. - `tx.sql` is a full SQL builder, keyed the same way as `db.sql`: use `tx.sql.public.
`, where `public` is the PostgreSQL schema. - Reads inside the transaction see the transaction's own uncommitted writes. +- `tx.enums` and `tx.nativeEnums` are the same enum accessors the client has, and `.values` is the list of an enum's members. - The callback's return value passes through as the result of `db.transaction(...)`. -- Do not let an [`AsyncIterableResult`](#asynciterableresult) escape the callback unread. A result you return and then read after the transaction has ended rejects with an error whose `code` is `RUNTIME.TRANSACTION_CLOSED` (see the Remark below). To act on a code, catch the error and compare `error.code` with the string. Every code is listed in the [error reference](/orm/reference/error-reference). +- Do not let an [`AsyncIterableResult`](#asynciterableresult) escape the callback unread. A result you return and then read after the transaction has ended rejects with an error whose `code` is `RUNTIME.TRANSACTION_CLOSED` (see the warning after the examples). To act on a code, catch the error and compare `error.code` with the string. Every code is listed in the [error reference](/orm/reference/error-reference). #### Options @@ -373,7 +374,6 @@ Run a callback inside a transaction. The transaction commits when the callback r | Return type | Example | Description | |---|---|---| | `Promise` | `await db.transaction(async (tx) => ...)` | Whatever the callback returns, so `R` is your callback's own return type. | -| `tx.enums` / `tx.nativeEnums` | `tx.enums.public.Role.values` | The same enum accessors the client has. `.values` is the list of members. See [`postgres(options)`](#postgresoptions). | #### Examples @@ -424,7 +424,7 @@ await db.transaction(async (tx) => { ``` :::warning[An escaped `AsyncIterableResult` throws after the transaction ends] -An `AsyncIterableResult` is tied to the transaction connection. If you return one from the callback and read it after the transaction has committed, it rejects with an error whose `code` is `RUNTIME.TRANSACTION_CLOSED`, before yielding any row. Collect the rows inside the callback (`await` the result there) and return the array instead. +Collect the rows inside the callback by awaiting the result there, and return the array instead. A result is tied to the transaction connection. If you return one from the callback and read it after the transaction has committed, it rejects with an error whose `code` is `RUNTIME.TRANSACTION_CLOSED`, before yielding any row. ```typescript const escaped = await db.transaction(async (tx) => { @@ -454,11 +454,12 @@ The callback form does something the array form never could: one query's result ### `withTransaction(runtime, callback)` -A transaction helper you import directly. `db.transaction(...)` is built on it. Use it directly in a helper or a library that is handed a runtime rather than a client. +A transaction helper you import directly. Use it when you are writing a function that takes a runtime as a parameter instead of the client. #### Remarks -- Imported from `@prisma/orm-postgres/family-runtime`. The callback receives a transaction handle whose only methods are `query` and `execute`. Unlike `db.transaction(...)`'s `tx`, it has no `.orm` or `.sql`, so only running queries goes through `tx`. Building them still needs a SQL builder, either the client's `db.sql` or one passed in to your helper. +- Imported from `@prisma/orm-postgres/family-runtime`, along with the `Runtime` type. That path holds the SQL runtime API, the same one for every SQL database. +- The callback receives a transaction handle whose only methods are `query` and `execute`. Unlike `db.transaction(...)`'s `tx`, it has no `.orm` or `.sql`, so build your queries with a SQL builder you already hold, such as the client's `db.sql`. - Commits on return, rolls back on throw, the same as `db.transaction(...)`. #### Options @@ -479,49 +480,38 @@ A transaction helper you import directly. `db.transaction(...)` is built on it. ##### Commit two writes with `withTransaction` ```typescript -import { withTransaction } from '@prisma/orm-postgres/family-runtime'; - -const runtime = db.runtime(); -await withTransaction(runtime, async (tx) => { - await tx.execute(db.sql.public.tag.insert([{ id: crypto.randomUUID(), label: 'wt-commit-1' }]).build()); - await tx.execute(db.sql.public.tag.insert([{ id: crypto.randomUUID(), label: 'wt-commit-2' }]).build()); -}); -``` +import { withTransaction, type Runtime } from '@prisma/orm-postgres/family-runtime'; -##### Roll back on error - -```typescript -import { withTransaction } from '@prisma/orm-postgres/family-runtime'; - -const runtime = db.runtime(); -try { - await withTransaction(runtime, async (tx) => { - await tx.execute(db.sql.public.tag.insert([{ id: crypto.randomUUID(), label: 'wt-rollback' }]).build()); - throw new Error('deliberate rollback'); +async function addTags(runtime: Runtime) { + return withTransaction(runtime, async (tx) => { + await tx.execute(db.sql.public.tag.insert([{ id: crypto.randomUUID(), label: 'wt-commit-1' }]).build()); + await tx.execute(db.sql.public.tag.insert([{ id: crypto.randomUUID(), label: 'wt-commit-2' }]).build()); }); -} catch { - // the tag was rolled back } + +await addTags(db.runtime()); ``` ### Manual connection and transaction control -The lowest level: acquire a connection, open a transaction on it, commit or roll back yourself, and return the connection to the pool. Use it when you need the same connection between statements. +The lowest level: acquire a connection, open a transaction on it, commit or roll back yourself, and return the connection to the pool. Use it when you need statements on one connection outside a transaction, such as a `SET` you run with `connection.execute(...)` before you open the transaction. #### Remarks -- `runtime.connection()` returns a dedicated connection. `connection.transaction()` opens a transaction on it. Run built queries with `transaction.query(...)` for rows or `transaction.execute(...)` for writes that return no rows, then call `transaction.commit()` or `transaction.rollback()`. Always `release()` the connection when done, to return it to the pool. -- `connection.destroy()` throws that connection away instead of returning it to the pool. It does not close the pool or the runtime. The runtime opens a replacement connection on the next query. Use `release()` for the normal path and `destroy()` only to discard a connection you no longer trust. +- `runtime.connection()` returns a dedicated connection. `connection.transaction()` opens a transaction on it. Run built queries with `transaction.query(...)` for rows or `transaction.execute(...)` for writes that return no rows, then call `transaction.commit()` or `transaction.rollback()`. Always `release()` the connection when done, to return it to the pool. Commit or roll back first: release it in a `finally` block, as the examples do, so a statement that throws does not leak the connection. +- `connection.destroy()` throws that connection away instead of returning it to the pool. It does not close the pool or the runtime, and the runtime opens a replacement connection on the next query. Use `destroy()` only for a connection you no longer trust. #### Options +These are the methods, not options. + | Method | Type | Description | |---|---|---| | `runtime.connection()` | none | Returns a `Promise` of a dedicated connection. | | `connection.transaction()` | none | Returns a `Promise` of a transaction on that connection. | | `transaction.commit()` / `transaction.rollback()` | none | Commit or discard the transaction. | | `connection.release()` | none | Return the connection to the pool. | -| `connection.destroy()` | none | Evict this connection from the pool. | +| `connection.destroy()` | none | Throw this connection away instead of returning it to the pool. | #### Examples @@ -530,27 +520,33 @@ The lowest level: acquire a connection, open a transaction on it, commit or roll ```typescript const runtime = db.runtime(); const connection = await runtime.connection(); -const transaction = await connection.transaction(); -await transaction.execute( - db.sql.public.tag.insert([{ id: crypto.randomUUID(), label: 'manual-commit' }]).build(), -); -await transaction.commit(); -await connection.release(); +try { + const transaction = await connection.transaction(); + await transaction.execute( + db.sql.public.tag.insert([{ id: crypto.randomUUID(), label: 'manual-commit' }]).build(), + ); + await transaction.commit(); +} finally { + await connection.release(); +} ``` ##### Roll back manually, then release ```typescript const connection = await runtime.connection(); -const transaction = await connection.transaction(); -await transaction.execute( - db.sql.public.tag.insert([{ id: crypto.randomUUID(), label: 'manual-rollback' }]).build(), -); -await transaction.rollback(); -await connection.release(); +try { + const transaction = await connection.transaction(); + await transaction.execute( + db.sql.public.tag.insert([{ id: crypto.randomUUID(), label: 'manual-rollback' }]).build(), + ); + await transaction.rollback(); +} finally { + await connection.release(); +} ``` -##### Evict a connection with `destroy()` +##### Throw a connection away with `destroy()` ```typescript const connection = await runtime.connection(); @@ -561,23 +557,21 @@ await connection.destroy(); ## Transactions (MongoDB) :::note[MongoDB transactions are not available in Prisma ORM yet] -There is no `db.transaction(...)` on the MongoDB client, and the MongoDB runtime has no `connection()`, `prepare()`, or `telemetry()`: it has `query`, `execute`, and `close` and nothing else. Single-document write operations are atomic on their own. Multi-document transactions through Prisma ORM are not shipped. +There is no `db.transaction(...)` on the MongoDB client, and the MongoDB runtime has no `connection()`, `prepare()`, or `telemetry()`: it has `query`, `execute`, and `close` and nothing else. Single-document write operations are atomic on their own. Multi-document transactions through Prisma ORM are planned, not shipped. To run one today, share one `MongoClient` between Prisma ORM and your driver code and use a driver session, as the [Fundamentals transactions guide](/orm/fundamentals/transactions#transactions-on-mongodb) shows. The reading rules under [`AsyncIterableResult`](#asynciterableresult) apply to MongoDB too. ::: -Multi-document transactions for MongoDB are planned, but nothing on this page works on MongoDB today. To run one now, share one `MongoClient` between Prisma ORM and your driver code and use a driver session, as the [Fundamentals transactions guide](/orm/fundamentals/transactions#transactions-on-mongodb) shows. - ## Prepared statements (PostgreSQL) A prepared statement compiles a query once against a declaration of its parameters, then runs it repeatedly with different values. Prepared statements are PostgreSQL only. ### `runtime.prepare(declaration, callback)` -Prepare a statement off a runtime. +Prepare a statement from a runtime. #### Remarks -- The declaration maps each parameter name to a type id, for example `{ label: 'pg/text@1' }`. A type id is the PostgreSQL type plus `@1`, such as `pg/int4@1`, `pg/uuid@1`, `pg/bool@1`, or `pg/timestamptz-temporal@1`. It is called `codecId` in code. To take one straight from a column, read `db.sql.public.user.columns.id`. See [`fns.raw` and `.returns()`](/orm/reference/sql-query-builder#fnsraw-and-returns). -- The callback takes one argument, the declared `params`, and returns the built query, which is what `.build()` gives you and what the tables below call `SqlQueryPlan`. Build it with a SQL builder you already hold, such as `db.sql`. In the `where((f, fns) => ...)` calls below, `f` holds the table's columns and `fns` holds the comparison functions. +- The declaration maps each parameter name to a type id, for example `{ label: 'pg/text@1' }`. A type id is the PostgreSQL type plus a version, always `@1` today, such as `pg/int4@1`, `pg/uuid@1`, or `pg/bool@1`. A few ids carry a longer name, such as `pg/timestamptz-temporal@1` for a `DateTime`. To take one straight from a column, read `db.sql.public.user.columns.id.codecId`, which is that id as a string: `prepare({ id: db.sql.public.user.columns.id.codecId }, ...)`. For the ids the common JavaScript values get, see the [list on the raw queries page](/orm/reference/raw-queries#binding-a-bare-value-with-param). +- The callback takes one argument, the declared `params`, and returns the built query, which is what `.build()` gives you. `SqlQueryPlan` in the tables below is the type of a built query. Build it with a SQL builder you already hold, such as `db.sql`. In the `where((f, fns) => ...)` calls below, `f` holds the table's columns and `fns` holds the comparison functions. - A declared parameter that the callback never uses is rejected at prepare time with an error whose `code` is `RUNTIME.PREPARE_UNUSED_PARAM`, with `details.unused` listing the offending names. The rejection happens at `prepare()`, before any execution. - The resulting `PreparedStatement` runs via `ps.query(target, params)`; see [`PreparedStatement.query`](#preparedstatementquerytarget-params). @@ -624,8 +618,8 @@ Prepare a statement on the client itself. #### Remarks -- Same result as `runtime.prepare(...)`, but off the client the SQL builder is handed to you as the first argument. The callback takes **two** arguments, `(sql, params)`. Build the query with `sql.public.
`. -- `prepare()` starts the connection itself, so calling `db.connect()` afterwards rejects with an error whose `code` is `DRIVER.ALREADY_CONNECTED`. Reach for the runtime with `db.runtime()` instead, which returns the one `prepare()` already built. +- `db.prepare(...)` does the same thing as `runtime.prepare(...)`, but it passes you a SQL builder as the callback's first argument, so you do not need `db.sql`. The callback takes **two** arguments, `(sql, params)`. Build the query with `sql.public.
`. +- `prepare()` starts the connection itself. Call `db.runtime()` for the runtime. `db.connect()` would throw an error whose `code` is `DRIVER.ALREADY_CONNECTED`, because `prepare()` already connected. #### Options @@ -661,14 +655,15 @@ Run a prepared statement against a target, with the parameter values. - A query that returns rows prepares into a `PreparedStatement`, which you run with `query(...)`. A statement written as raw SQL with `db.raw.sql` and ending in `.affectedCount()`, which asks for a row count instead of rows, prepares into a `PreparedExecution` instead. You run that with `execute(target, params)`, and it resolves to `{ affectedRows }`, the number of rows the statement changed. See the [raw queries reference](/orm/reference/raw-queries): ```typescript - const archive = await runtime.prepare({}, () => - db.raw.sql`UPDATE "tag" SET label = ${'archived'} WHERE label = ${'stale'}`.affectedCount().build(), + const archive = await runtime.prepare({ label: 'pg/text@1' }, (params) => + db.raw.sql`UPDATE "tag" SET label = 'archived' WHERE label = ${params.label}`.affectedCount().build(), ); - const { affectedRows } = await archive.execute(runtime, {}); + const { affectedRows } = await archive.execute(runtime, { label: 'stale' }); ``` -- The `target` is explicit: it can be a runtime, a connection, or a transaction. One prepared statement runs against any of them. +- You must pass `target`. It can be a runtime, a connection, or a transaction. One prepared statement runs against any of them. +- A prepared statement has no `close()` method and nothing to dispose. Hold one for as long as you like and reuse it. - A single `PreparedStatement` prepared once runs against both a runtime target and a transaction target, seeing the transaction's writes when run inside it and the committed state when run against the runtime afterward. #### Options @@ -716,8 +711,9 @@ Per-query options for cancellation and scope. #### Remarks - `signal` is an `AbortSignal` for per-query cancellation. A signal that is **already aborted** when you call `query(...)` rejects before any row is fetched, with an error whose `code` is `RUNTIME.ABORTED` and whose `details.phase` is `'stream'`. The error's `cause` is the signal's reason, exactly as you passed it to `controller.abort(...)`. -- An abort that lands after rows have started arriving ends the stream with the same `RUNTIME.ABORTED` error. `details.phase` says where the abort landed. -- There is also a `scope` option. The runtime sets it for you so that [middleware](/orm/middleware/how-middleware-works) can see whether the query ran on the runtime, a connection, or a transaction. Do not pass it yourself. +- An abort that lands after rows have started arriving ends the stream with the same `RUNTIME.ABORTED` error. `details.phase` says where the abort landed: `'encode'`, `'decode'`, `'stream'`, `'beforeQuery'`, `'beforeExecute'`, or `'afterQuery'`. +- Inside `db.transaction(...)` the aborted statement throws, so your callback throws and the transaction rolls back. +- `scope` is set for you. Do not pass it. #### Options @@ -746,7 +742,7 @@ Read telemetry about the most recent query. #### Remarks - PostgreSQL only. `telemetry()` does not exist on the MongoDB runtime. Read it after a query to log how long that query took and whether it succeeded. -- `telemetry()` returns `null` on a freshly-connected runtime, before any query has run. After a query, it returns an object of the shape `{ lane, target: 'postgres', fingerprint, outcome, durationMs? }`. `lane` names which API built the query, such as `orm-client` for `db.orm` or `raw` for `db.raw.sql`. Every run of the same query text shares one `fingerprint`, whatever values you pass, so you can group runs of one query together. `outcome` is `'success'` or `'runtime-error'`. `durationMs` is how long the query took, and the runtime records it for every query it times. The object reflects only the **most recent** query, not a running history. +- `telemetry()` returns `null` on a freshly-connected runtime, before any query has run. After a query, it returns an object of the shape `{ lane, target: 'postgres', fingerprint, outcome, durationMs? }`. `lane` is the name of the API you used, such as `orm-client` for `db.orm` or `raw` for `db.raw.sql`. Every run of the same query text shares one `fingerprint`, whatever values you pass, so you can group runs of one query together. `outcome` is `'success'` or `'runtime-error'`. `durationMs` is how long the query took, and it is present for every query the runtime ran. The object reflects only the **most recent** query, not a running history. #### Return type @@ -771,6 +767,6 @@ const after = runtime.telemetry(); [`all()`](/orm/reference/orm-client#all), [`createAll()`](/orm/reference/orm-client#createall), and the runtime's `query(...)` return an `AsyncIterableResult`. `all()` and `query(...)` read rows, and `createAll()` writes rows and returns the ones it wrote. `await` the result to collect an array, or `for await` it to take rows one at a time. -`for await` does not save memory on PostgreSQL: every row is loaded first. The upside is that a result you hold or read slowly does not keep a pooled connection checked out, because the client hands the connection back before you read the rows. A connection or transaction you opened yourself is unaffected: it stays yours until you release or commit it. +Use `await` unless you want to handle rows as they arrive. `for await` does not reduce memory on PostgreSQL: every row is loaded first. -Pick `await` or `for await` for a given result and do not mix the two. Re-`await`ing a result you already awaited is safe and returns the same array. Switching between `await` and `for await`, or looping a second time with `for await`, throws an error whose `code` is `RUNTIME.ITERATOR_CONSUMED`. For the full rules on reading a result, shared identically by PostgreSQL and MongoDB, see [`AsyncIterableResult`](/orm/reference/orm-client#asynciterableresult) in the ORM client reference. +Pick `await` or `for await` for a given result and do not mix the two. Calling `.toArray()` does the same thing as `await`. Re-`await`ing a result you already awaited is safe and returns the same array. Switching between `await` and `for await`, or looping a second time with `for await`, throws an error whose `code` is `RUNTIME.ITERATOR_CONSUMED`. For the full rules on reading a result, shared identically by PostgreSQL and MongoDB, see [`AsyncIterableResult`](/orm/reference/orm-client#asynciterableresult) in the ORM client reference. diff --git a/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part1.round3.md b/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part1.round3.md new file mode 100644 index 00000000000..845e1b27d4c --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part1.round3.md @@ -0,0 +1,100 @@ +# Reader review: `pipeline-builder.part1.mdx` + +I have used Prisma 7 for two years. I have never used MongoDB aggregation pipelines seriously. Here is what stopped me. + +## 1. Sentences I could not restate after one reading + +**Line 19** — "The `posts` collection holds two kinds of document, `Article` and `Tutorial`, told apart by the value of the `kind` field. No example on this page uses them." + +This is wrong, or I cannot tell what "them" means. The very next stage example is `.match((f) => f.kind.eq('tutorial'))`, and line 726 says "`duration` is a field on `Tutorial`". So examples do use them. I stopped and re-read three times trying to work out whether "them" meant the models, the `kind` field, or something else. Plainer: say which, or delete the sentence. + +**Line 19** — "A type id such as `mongo/string@1` is the stored type plus a version, always `@1` today." + +I do not know what a "type id" is, where it comes from, or why I am being told this on a pipeline-builder page. It appears once in the schema, on `@@type(...)` inside an enum, and is never mentioned again. I cannot restate it because I do not know what it is for. + +**Line 84** — "`db` is the client you create with `mongo(...)`. See [Transactions and runtime] for how to create it. Prisma ORM 8 renames `schema.prisma` to `contract.prisma`. Run `npx prisma contract emit` to generate `contract.json`, used at run time, and `contract.d.ts`, used for types." + +Four unrelated facts in one paragraph: what `db` is, a file rename, a codegen command, and two output files. I had to read it twice to work out that `contract emit` is a thing I must run before any code on this page compiles. That is important and it is buried in the middle of a paragraph about something else. + +**Line 135** — "**After `lookup()` or `group()`, compare ids with `String(a._id) === String(b._id)`.** Both a looked-up sub-document's `_id` and the top-level `_id` are the driver's own `ObjectId` values there, not hex strings." + +`a` and `b` are never defined. I do not know what two things I am comparing. I guessed: a joined sub-document and a parent document. The plain version would be: "After `lookup()` or `group()`, `_id` values come back as the MongoDB driver's `ObjectId` objects, not strings. `==` between two `ObjectId`s is false even when they are the same id, so wrap both sides in `String(...)` before comparing." + +**Line 136** — "Decoding only survives these stages: `match()`, `sort()`, `limit()`, `skip()`, `sample()`, `project()`, `addFields()`, and `vectorSearch()`. After any other stage, including `lookup()` and `group()`, documents come back raw." + +"Decoding only survives these stages" — survives means what? I eventually guessed: if my pipeline contains *only* stages from this list, results are converted; add one other stage anywhere and the whole result is raw. But the sentence could equally mean it survives *past* each of those stages and is lost only at the point of a different stage. Those are different rules and I would write different code for each. Also: "come back raw" — raw meaning what, exactly? `ObjectId` objects and driver `Date`s? Say the two or three things that actually change. + +**Line 158** — "It is why an `_id` filter never matches: your hex string is compared as a string against a stored `ObjectId`." + +Understandable, but then line 199 says the driver's own `ObjectId` does not match either. So the explanation given here does not explain the actual behaviour. After reading both I still do not know why the `ObjectId` form fails. + +**Line 643** — "For `$$KEEP` write `f.rawPath('$KEEP')`." + +One dollar sign in, two dollar signs out. Nothing says why. I assumed a typo, then assumed `rawPath` prepends a `$`, then gave up. If `rawPath` adds a `$`, say so in the sentence. + +**Line 644** — "Every `fn.*` call returns a wrapper. `.node` is the raw expression inside it. `match()` takes the wrapper through `expr(...)`. `fn.cond()`, `redact()`, and the option-object stages take `.node`." + +Four sentences, four new concepts, no names I recognise ("wrapper", "node", "raw expression"). I cannot restate this. What I actually need is a rule I can apply: which argument positions need `.node`. Even the example at line 673 has to re-explain it in a code comment, which tells me the prose did not land. + +**Line 692** — "Build a raw expression with the `fn.*` helpers and pass its `.node` property, or name a field with `MongoAggFieldRef.of('price')`." + +`price` is not a field in the example schema. I looked for it. Use a real field. + +## 2. Words and phrases I had to guess + +- **"compiles to"** (line 11) — guessed: is translated into, at build time or at `build()` time. I do not know which, and the page later says nothing runs until you execute, so "compiles" may mean neither. +- **"plan"** (line 121, 127) — the page calls it "a built query" and then names the variable `plan`. I guessed these are the same thing. Two names for one object on the same line is avoidable. +- **"built query"** vs **"pipeline"** vs **"chain"** vs **"builder"** — four words circulating. I guessed they are stages of the same object. +- **"root"** (lines 93, 101, 357, 454) — the parameter is called `root` but every example passes a collection name positionally and never types the word `root`. The error message says `Valid roots: ...`. I guessed root = collection. Line 454's example `db.query.from(root).unwind('items')` literally tells me to type `root`, which is not valid code. +- **"There is no `find` and no `distinct`"** (line 11) — guessed this refers to the MongoDB driver's methods. If you have not used the driver, this sentence names nothing. +- **`@@discriminator(kind)`, `@@base(Post, "article")`** (lines 59, 67, 74) — never explained anywhere on the page. Guessed: `Article` and `Tutorial` are subsets of `posts` selected by `kind`. That is Prisma 8 vocabulary I have never met. +- **"Leaf fields"** (line 159) — guessed: scalar fields, as opposed to embedded documents. Why not say "scalar fields", which the same page uses at line 158? +- **"system variables"** (line 642) — guessed: MongoDB's `$$`-prefixed names. Fine if you know MongoDB, opaque if you do not. +- **"expression helper" / "accumulator" / "raw expression" / "document-shaped expression" / "key expression"** — five kinds of expression, no definitions on this page. I guessed from the examples. +- **"decoded"** (line 136) — guessed: converted from driver types to my types. +- **"granularity"** (line 683) — passed through with no explanation at all. + +## 3. Places I asked "so what do I actually type?" and got no answer + +- **`graphLookup`, `setWindowFields`, `densify`, `fill`, `geoNear`, `bucketAuto`** (lines 683–688). Each gets one sentence and an options list. `output` in `setWindowFields` and `fill` is an object of *what*? `range` in `densify` is what shape? There is not one example among the six. I could not write any of these calls. +- **`startWith` / `groupBy` / `partitionBy` / `near` are "a raw expression"** — said four times before line 692 tells me how to build one. And line 692's two options (`fn.*` + `.node`, or `MongoAggFieldRef.of`) are not the same thing, so I do not know which applies where. +- **The facet example imports `MongoFieldFilter`** (line 707) but `MongoFieldFilter` is not in the list of importable classes at line 693, and nothing documents `MongoFieldFilter.eq('kind', 'tutorial')`. I would have copied the example and had no idea how to change it — e.g. to a `gt` filter. +- **Constructor signatures.** Line 693 lists 28 classes and says "Import them". `new MongoCountStage('count')`, `new MongoSortStage({createdAt: -1})`, `new MongoLimitStage(2)` — I can copy those three. For the other 25 I have nothing. +- **`for await` over it** (line 133). No example. Over what expression? `for await (const doc of runtime.query(plan))`? I guessed, and `await runtime.query(plan)` returning an array makes me unsure the same call can be iterated. +- **Dropping a field with `project()`.** Line 398 says "keeping, dropping, or computing each field" but no example shows `0`, and I would not have known dropping used `0` if the table at line 405 had not listed the type. +- **`unwind('tags')`** takes a plain string while every other stage takes `(f) => f.something`. Nothing says why, and the one example (line 460) explicitly does not work against the example schema. So there is no runnable example of `unwind` on this page. +- **`contract.json` import** (line 116): `import contractJson from './contract.json' with { type: 'json' }`. This needs specific Node and TypeScript settings. Nothing says which. +- **`mongo({ contractJson, url, dbName })`** (line 118) — `dbName: 'app'`. Where does that come from? Is it required? The page sends me to another page for `mongo()` but still shows a half-explained call I must copy. +- **`search(config, index?)`** — what happens if I omit `index`? +- **`acc.*`** is used at line 480 and 501 and defined nowhere on this page. It says "See part 2" nowhere either. + +## 4. Places that explain how the tool works inside, when I only wanted to know what to do + +- **Line 644**, the wrapper/`.node` paragraph. This is the internal object model. What I need is a rule: "pass `.node` when the argument is X". The code comment at line 673 does a better job than the reference text. +- **Line 158** — "Prisma ORM sends your value to MongoDB unchanged. It does not turn a string into an `ObjectId`, a date, or anything else." This is implementation behaviour. The thing I need is the consequence, and it is in the warning at line 199 anyway. +- **Line 136** — the whole decoding paragraph is framed as "what Prisma does to values" when what I need is "results are normal after these stages; after anything else you get `ObjectId`s and driver dates." +- **Line 131** — "Call `build()`. `aggregate()` is the same method under another name." Knowing there is a second name for the method I should not use is not useful. Say "Call `build()`" and stop. +- **Line 691** — "`geoNear`, `densify`, and `fill` have not been verified against a running MongoDB." This is your internal test status. As a reader I do not know what to do with it — use them or not? +- **Line 199** — "The hex-string form passes TypeScript, so it compiles cleanly and then returns zero documents." The compiler detail is background; the fact I need is "returns zero documents, always", which is already in the first sentence. + +## 5. Contradictions I noticed and could not resolve + +- **Line 136** lists `vectorSearch()` as a stage that preserves decoding. **Line 745** says `vectorSearch()` is Atlas-only. Reading in order, I met the claim before I knew what the stage was. +- **Line 158** says Prisma sends values unchanged, which is why `_id` filters fail. **Line 203** shows `db.orm.posts.where({ _id: postId })` working with a plain hex string. So the ORM client *does* convert and the pipeline builder does not. Nothing says that, and after one reading I believed the two pages behaved the same way. +- **Line 11** promises "This page documents every stage, accumulator, expression helper, and write method." The page ends at line 760 with no accumulators, no expression helpers, no write methods. I would have scrolled back looking for what I missed. + +## Could I do what the page is for, after one reading? + +Partly. I could write and run the simple stages: `match`, `sort`, `limit`, `skip`, `sample`, `project`, `addFields`, `count`, `sortByCount`, `group`, `lookup`, `replaceRoot`. Those all have complete, copyable examples against the real schema, and that is the bulk of what I would actually use. + +I could not do these things: + +1. **Set up the project.** `contract emit`, `mongo(...)` options, and the JSON import are shown but not explained. I would guess and fail at least once. +2. **Write any of the six option-object stages** — `bucketAuto`, `geoNear`, `graphLookup`, `setWindowFields`, `densify`, `fill`. No examples, no shapes for `output` or `range`. +3. **Know when to write `.node`.** The rule at line 644 did not survive one reading. I would trial-and-error against the type checker. +4. **Use `facet` or `unionWith` with anything but the exact stages shown**, because `MongoFieldFilter` is used and never documented, and 25 of the 28 stage classes have no constructor shown. +5. **Use `unwind`**, because the only example needs a field the schema does not have. +6. **Predict when results come back decoded.** I do not know whether the rule is "the whole pipeline" or "up to the first non-listed stage". +7. **Explain `@@base`, `@@discriminator`, or `@@type("mongo/string@1")`** to a colleague, even though they are in the schema every example runs against. + +The biggest single gap is the setup paragraph at line 84. Every code block on the page depends on `db`, `runtime`, and `contract.json`, and all three are handled in four sentences that also contain a file rename. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part2.round3.md b/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part2.round3.md new file mode 100644 index 00000000000..834e45eb40f --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part2.round3.md @@ -0,0 +1,59 @@ +# Reader review: `pipeline-builder.part2.mdx` + +## Sentences I could not restate after one reading + +**Line 33 — the paragraph after the accumulator table.** This is seven separate rules in one block: which helpers share a signature, what `output` means, what `sortBy` means, the `n: fn.literal(2)` rule, where `fn.literal` comes from, and the `sort()`-before-`group()` rule. I had to reread it three times and I still could not tell you from memory which of `top`/`bottom`/`topN`/`bottomN` take `n`. Split it. The `sort()` rule in particular is a real correctness warning buried as the last sentence of a housekeeping paragraph. + +**Line 109 — "Five have a different name: `in` and `typeof` are reserved words in JavaScript, `toString` is already a method on every JavaScript object, and `$first` and `$last` get the suffix `Elem`."** Three reasons, five names, and the reasons are in a different order than the renames that follow. I counted on my fingers. Plainer: give a two-column table — MongoDB operator, helper name — with a one-line reason each. + +**Line 124–130 — the `.node` table.** I do not know what `.node` is. The page never says. It reads as: "sometimes you pass the thing, sometimes you pass a property of the thing, and there is no rule, just memorize this list." Line 129 is a single table cell holding five function names and five option names. I cannot hold that. What I actually wanted: one sentence saying *why* these differ, so I can predict the next case instead of looking it up every time. + +**Line 145 — "So does every other helper not named above, in the order MongoDB documents for that operator."** This tells me the reference is incomplete on purpose and sends me to MongoDB's docs, with no link. To use `fn.substrBytes` I have to leave the page. + +**Line 238 — "Pipeline form calls `f.stage.*`, where `stage` is a literal property name, and can read other fields of the same document."** "Literal property name" stopped me. I guessed it means "you literally type `.stage`, it is not a placeholder for a stage name" — which is worth saying, because everything else in `f.*` is a field name. Say that directly: "type `f.stage` exactly; `stage` is not a placeholder." + +**Line 249 — the `insertOne` remark.** Six ideas in one paragraph: the record is plain, TypeScript checks nothing, `_id` is optional, values are stored raw with no conversion, pass real `Date`/`ObjectId`, `match()` follows the same rule, and `insertMany([])` throws. The "no conversion" point contradicts the "decoded" promise I was given in part 1 and deserves its own callout, not sentence four of seven. + +**Line 399 — "`pop(direction)` | `1` removes the last element, `-1` the first. Optional, and defaults to `1`."** Ambiguous: is the *argument* optional, or the operation? I guessed the argument. + +## Words and phrases I had to guess + +- **`.node`** — guessed: some internal wrapper object, and `.node` unwraps it. Never defined. +- **"built query"** vs the variable named `plan` — guessed these are the same thing. The prose says "built query", every example says `plan`, and neither the writes section nor `out()`/`merge()` calls `.build()`. I eventually worked out that write methods return the built query directly, but only because line 247 says so in passing. +- **"expression"** — used throughout (`sum(expression)`, "written as an expression"). I guessed it means "either an `f.something` or an `fn.something(...)`, never a plain JavaScript value." Line 145 half-confirms this. But `f.bio.set('x')` and `MongoFieldFilter.eq('kind', 'tutorial')` both take plain values, so the rule clearly does not hold everywhere and the page never says where the boundary is. +- **"a plain string, names a different database"** (line 436) — guessed "plain" just means "a string, not an options object." +- **"Root-level writes"** — guessed: called on `from(...)` before any stage. +- **"an `N`-variant"** (line 85 heading) — guessed: the family of accumulators ending in `N`. Coined jargon in a heading. + +## Where I asked "so what do I actually type?" and got no answer + +1. **`$and`, `$or`, `$not`, `$switch`, `$ifNull`, `$map`, `$reduce`, `$filter` have no helper** (line 109). This is the most common thing I would need — an OR condition — and the page stops at "no helper." Do I use `pipe()`? `rawCommand()`? Nest `fn.cond`? Not said. This is the single biggest gap on the page. +2. **The 28 stage classes in line 190.** Only `MongoMatchStage`, `MongoCountStage`, and `MongoFieldFilter.eq` get a shape. I have no idea what `new MongoLookupStage(...)` takes. Same for `MongoFieldFilter` — `eq` is the only method shown; I do not know if `gt` exists. +3. **Getting an id back as a string.** Lines 192 and 304 both tell me the TypeScript type lies and I get an `ObjectId`. Neither tells me what to do about it. +4. **Filtering by `_id`.** Line 467 says the typed builder cannot do `_id` equality filters. That is a severe limitation mentioned as a subclause. The fix is the last example, which is good, but I only found it after reading the whole page. +5. **`fn.convert`'s `to` values** (line 144) — `'int'` and `'objectId'` are shown as examples. The full list is elsewhere, unlinked. +6. **`merge()`'s `on`** (line 437) — "the field or fields to match on." String, or array of strings? The example shows a string only. +7. **`out()` and `merge()` "produce no rows of their own"** (line 439). So what does `await runtime.query(plan)` return? Empty array? Undefined? The example discards it, which does not tell me. +8. **`upsertOne` on the root** (line 290) takes two callbacks with no labels. I inferred filter-then-updater from the heading text, not from the code. + +## Where the page explains internals when I only wanted to know what to do + +- **Line 190's 28-class list.** I wanted "here is how to add a raw stage." I got the module's whole export list. One example plus a pointer would do. +- **The `.node` table (129).** This is a description of how the builder represents expressions internally, presented as a rule I must memorize. +- **Line 222 — "`aggregate()` is an alias that returns the identical query. Write `build()`."** If `build()` is the answer, the alias is trivia. +- **Line 224–228 — the "Return type" table** has one row and restates the sentence above it. +- **Line 362 — "the unknown key is ignored and the default `'after'` applies."** Useful warning, but the mechanism explanation ("a typo produces a wrong but plausible result") is longer than the instruction. + +## Could I do what the page is for, after one reading? + +Partly. I could write a `group()` with `count`, `sum`, `avg`, `min`, `max`, `first`, `last`, `push`, `addToSet`. I could write a simple `project()` with arithmetic or string helpers. I could do `insertOne`, `insertMany`, `updateMany` with operator form, `deleteMany`, and `findOneAndUpdate` with `returnDocument`. The write sections are the strongest part of the page: the examples match the prose and each shows the result shape. + +What I would still not know: + +- Whether my `fn.*` value needs `.node`, plain, or `expr(...)` at any call site I have not memorized — and I would get it wrong, because the page gives me a list, not a rule. +- How to express OR, AND, or a null fallback. I would be stuck the first time I needed `$or`. +- How to build any raw stage other than `MongoMatchStage` and `MongoCountStage`. +- Which helpers take positional arguments in which order, without opening MongoDB's docs. +- What `.node` actually is. +- How to get a string `_id` out of a `findOneAndUpdate()` result. +- Whether values I pass to `insert*` and `match()` are converted (line 249 says no; part 1's "decoded" promise says the read direction is converted). I would have shipped a bug here. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/raw-queries-and-index.round3.md b/docs/orm-docs-audit/c21-orm-reference/raw-queries-and-index.round3.md new file mode 100644 index 00000000000..5e350ec1f1f --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-reference/raw-queries-and-index.round3.md @@ -0,0 +1,119 @@ +## Page 1 — `index.mdx` + +### Sentences I could not restate after one reading + +**"It is not the same as `runtime.query()`, which runs a built query."** +Stopped me because I had no reason to confuse them yet. `db.query` and `runtime.query()` are different objects; the page warns me off a mistake before I know enough to make it, and then doesn't say what `runtime` is. Plainer: put this after the code example, as "`db.query` builds MongoDB pipelines. The separate `runtime.query()` method, below, is what runs any built query." + +**"In the example below, the ORM client runs when you `await` it. The SQL query builder and the pipeline builder do not run on their own: each produces a built query with `.build()`, which you then run with `runtime.query(...)`."** +Three ideas in one place: that awaiting triggers execution, that builders are lazy, and that `.build()` plus `runtime.query()` is the two-step. I got it on the second read only. Splitting it into two sentences would have been enough. + +**The whole of paragraph one (lines 9–11) is two unbroken walls of text.** Nine sentences with no list and no break, covering the v7 migration, three API surfaces, when to use each, raw queries, and transactions. As a reader I lost the thread at "The **pipeline builder** is at `db.query`." A bulleted list of the three builders would have been readable at a glance. + +**"Prisma ORM gives you three ways to write a query."** +Then the next paragraph adds raw queries as a fourth, and the card list shows five pages. After one reading I did not know whether "three" was wrong or whether raw is a special case. Say "three typed ways, plus raw SQL when none of them fits." + +**"The raw queries page is shorter and has fewer tables."** +I can't do anything with this. It tells me about the documentation's formatting, not about the product. + +### Words and phrases I had to guess + +- **"built query"** — guessed: an object holding the SQL and its values that hasn't been sent to the database yet. Never defined. +- **`plan`** — the code names the built query `plan`. I guessed "plan" and "built query" are the same thing. The page never connects the two words. +- **`.all()`** — guessed: "run it and give me every matching row." Never explained. This matters because the page's whole framing is "in v7 you wrote `findMany({ where, include })`" — so I immediately want to know what replaced `findMany`, and `.all()` is the answer, silently. +- **`runtime`** — guessed: a connection object. `db.runtime()` appears in the code with no sentence introducing it. +- **"aggregate over a join"** — fine, I know SQL. +- **"Options, Return type"** as section names — fine. + +### "So what do I actually type?" + +- **`findMany` has no stated replacement.** The migration sentence promises the mapping is elsewhere, but the one example on this page uses `.all()`, which isn't one of the arguments being mapped. I'd have to leave the page to learn the most basic thing. +- **`db.runtime()` — do I call it once per app, once per query, or once per request?** The code makes it a top-level `const` and says nothing. +- **Nothing shows the pipeline builder** even though it's one of the three headline APIs. The example covers the ORM client and the SQL query builder only. + +### Internals where I only wanted to know what to do + +Nothing serious. "each produces a built query with `.build()`" is mechanism, but it's mechanism I have to type, so it earns its place. + +### Could I do what the page is for? + +Yes, mostly. It's an index, and I'd know which of the five pages to click. I would still not know what `.all()` is, what `runtime` is or when to create it, or why `db.query` and `runtime.query()` needed a warning. + +--- + +## Page 2 — `raw-queries.mdx` + +### Sentences I could not restate after one reading + +**"This page covers two of these ways out."** +"These" points backwards to a sentence that listed three *typed* APIs — the opposite of ways out. I re-read the previous paragraph looking for a list of ways out that isn't there. Plainer: "There are two kinds of raw query." + +**"A type id (`codecId` in the code) such as `pg/text@1` says how to turn the database's bytes into a JavaScript value."** +Two names for one thing in the first eight words, and then bytes, which I did not ask about. I wanted to know which string to type. Plainer: "A type id such as `pg/text@1` tells Prisma what JavaScript type a column should become. In code, the property is called `codecId`." + +**"A `numeric` column arrives as a string, and a date arrives as the driver's own value."** +"the driver's own value" is not a type. After one reading I do not know what variable type I will get for a date, so I cannot write the code that consumes it. Say the actual type. + +**"There is no way to send a column name or a table name as a parameter, so never build one from user input with a template."** +"build one" — build *what*? A column name? A template? A query? I had to reconstruct it. Plainer: "Never put user input into the SQL text itself, because a column or table name cannot be a parameter." + +**The CTE paragraph (line 170) — the worst on the page.** Seven sentences and roughly six separate rules: + +> "Interpolate the object that `.returnsRow(spec)` gives you, before you call `.build()` on it, and its SQL and its values are put into the outer statement in order. Only the outer statement is built. The outer statement declares its own columns. For a column the inner query already declared, read it back from the inner object's `.returns` property, which is a record of the columns that query declared, rather than writing the type id out again. That property is a different thing from the `.returns(typeId)` method you call on a fragment." + +I could not restate this after one reading. The hardest part is the last sentence: I now have `.returns()` the method, `.returns` the property, and `.returnsRow()` the method, and the page tells me they differ without giving me a way to keep them straight. "its SQL and its values are put into the outer statement in order" is also internals — I only need to know that it works. + +**"Pass the built query to `runtime.execute(...)`, which resolves to an object, `{ affectedRows }`, whose `affectedRows` is the number of rows the statement changed."** +The sentence names `affectedRows` twice and circles back on itself. Plainer: "`runtime.execute(...)` resolves to `{ affectedRows }` — how many rows the statement changed." + +**"Each entry is either a column taken from your contract, such as `db.sql.public.user.columns.id`, which brings its own conversion, nullability, and TypeScript type, or an explicit type id for a column your contract has no counterpart for."** +One sentence, two branches, four nouns in a subordinate clause, and "a column your contract has no counterpart for" is backwards English. Split it in two. + +**"Every one of these methods goes through `query()`, including the writes."** +"Goes through" is vague. I guessed it means "you run all of them with `runtime.query()`, even the inserts and deletes." Say that. + +### Words and phrases I had to guess + +- **"`codecId`"** — guessed it's simply the code's name for a type id. Confirmed by the parenthetical, but the page then uses both names interchangeably for the rest of the page and I had to keep translating. +- **`@@map`** — "unless the model sets `@@map`" — guessed: a contract annotation that overrides the table name. Never explained, and I'm coming from v7 where the file was `schema.prisma`. +- **`pg/int8number@1`** — guessed: a 64-bit integer that comes back as a JavaScript `number` rather than a `bigint`. Nothing on the page says so, and it matters, because the `.returnsRow` example later uses `pg/int8@1` and the comment says "row.postCount is a bigint". So the two spellings do different things, and I worked that out by inference. +- **"raw fragment"** vs **"whole raw statement"** — the definitions are clear. No complaint. +- **"tagged template"** — assumed knowledge, fair for a JS developer. +- **"native BSON values"** — guessed: whatever the MongoDB driver hands back, unconverted. +- **"the driver"** — guessed: the `mongodb` / `pg` npm package underneath. Never introduced. + +### "So what do I actually type?" + +- **Where is the full list of type ids?** The page gives seven auto-assigned ones plus "four more you are likely to write", then says "Those are the common ones, not the whole set." It never says where the whole set is. I have a `date` column, an enum, and a `text[]`. I am stuck, and there is no link to follow. +- **The link text lies.** "See the [type ids](#binding-a-bare-value-with-param) for the ones you are most likely to write" — the anchor lands on a section titled "Binding a bare value with `param()`". I'd assume I clicked the wrong link. +- **How do I bind a `Date`?** The page tells me to three separate times — "such as a `Date`", "To bind a `Date`, or any value whose type is not in the list, wrap it in `param(value, { codecId })`" — and never once shows it. The only `param()` example is `param(15, { codecId: 'pg/int4@1' })`, a plain number that the table above says is bound automatically. So the single worked example demonstrates the one case where `param()` is unnecessary, and the case it was written for is missing. +- **`.select((f) => ({ id: f.id, serverNow }))`** — this object-returning callback form appears with no introduction. Earlier the page showed `select('id')` and `select('name', callback)`. This is a third shape, used once, unexplained. +- **`db.query.rawCommand(command)` is passed to `runtime.query()` with no `.build()`.** Line 205 told me flatly: "Call `.build()` on it, then run it with `runtime.query(built)`." Then line 306 and line 340 don't call `.build()`. I would have typed `.build()`, got an error, and not known why. +- **How do I compare an `ObjectId`?** The warning says "compare one with `String(...)`", and the only demonstration is inside a code comment. I'd want `String(row._id) === aliceId` in actual code. +- **`$redact` with `$$KEEP` / `$$PRUNE`** is named as one of the two reasons to use `rawCommand()` and never shown. +- **`db.query.rawCommand` under a MongoDB heading, but its own `##` section.** After one reading I could not tell whether `rawCommand` is part of the raw API or the pipeline builder. The page says it "runs ... through the pipeline builder" and then sends me to the pipeline builder page "for the full treatment" — so which page is authoritative? + +### Contradictions I hit + +- **`aggregate()` vs `rawCommand()` on `_id` matching.** Line 226: `aggregate` sends "stages ... as you wrote them, so a filter on `_id` with a real `ObjectId` matches." Line 345: `rawCommand()` is "the way out when the typed pipeline builder can't express a query. Two examples: filtering by `_id` equality". Both claim to be the answer for `_id` filtering. After one reading I do not know which one to use, or whether the second sentence means "the *typed* builder can't, but `aggregate` also can" — in which case `rawCommand` has no advantage over `aggregate()`, which at least gives me types. +- **Update result shape.** The warning box (line 208) says the write methods give you `{ matchedCount, modifiedCount }`. Line 251 says `updateOne()`/`updateMany()` return `{ matchedCount, modifiedCount, upsertedCount, upsertedId }`. Two different answers, ten lines apart. + +### Internals where I only wanted to know what to do + +- **"says how to turn the database's bytes into a JavaScript value"** — I don't care about bytes. Tell me which string to type and what I get back. +- **"its SQL and its values are put into the outer statement in order. Only the outer statement is built."** — this is how the composition is implemented. I just want "you can nest a `.returnsRow()` query inside another one; call `.build()` on the outer one only." +- **"MongoDB has no schema segment, which is why there is no `public` here and none on `db.orm`."** — the "which is why" is an explanation of the API's shape. "MongoDB has no schemas, so there is no `public` segment" would do. +- **"Every one of these methods goes through `query()`, including the writes. A write comes back as an array holding one result object, which is why the examples read it with `const [result] = ...`."** — the "which is why" again explains the plumbing. The actionable part is only the second half: "A write returns a one-element array, so destructure it." +- **"The **first** call to an upsert counter returns an **empty** result. The upsert creates a brand-new document, so there is no earlier version to return, the driver returns `null`, and the query yields no row."** — a four-step causal chain about driver behaviour. The fact I need is the first sentence. The rest is why, and I'd have accepted "because there is no earlier version." + +### Other things that stopped me + +- **The upsert counter example uses the `users` collection as a page-view counter, with `_id: 'pageViews'`.** The page admits this: "The example schema has no counter collection, so `users` stands in for one here." Reading it, I spent time working out whether a users collection really was supposed to hold a counter document. Then a further sentence explains that a string `_id` is legal. Two sentences of apology around a four-line example. +- **`new RawFindOneAndUpdateCommand('users', { _id: ... }, { $inc: ... }, true, { count: -1 }, 'after')`** — six positional arguments. The list above it is clear, but reading the call itself, `true` means nothing without scrolling back up. +- **The whole PostgreSQL half assumes I've read the SQL query builder page**, and the whole MongoDB half assumes I've read the pipeline builder page, for the example schemas. Fine for a reference, but it means I can't run a single example on this page without opening two others. + +### Could I do what the page is for? + +**PostgreSQL: partly.** I could write a `fns.raw` fragment in a `select()` or a `where()`, and I could write a whole `db.raw.sql` statement with `.returnsRow()` or `.affectedCount()` — the examples are close enough to copy. I would still not know: how to bind a `Date` (the thing the page tells me three times to use `param()` for), where the full list of type ids lives, when `pg/int8@1` versus `pg/int8number@1` applies, or what type a date column actually arrives as. The CTE section I would have to read three more times. + +**MongoDB: partly, and I'd guess wrong at least once.** The nine collection methods are clear and copyable. But I would not know whether to use `aggregate()` or `rawCommand()`, because the page gives `_id` filtering as the reason for `rawCommand` and also says `aggregate` handles it. I would call `.build()` on a `rawCommand` because the page told me to, and it would break. And I'd have to pick one of the two conflicting answers about what `updateOne()` returns. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part1.round3.md b/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part1.round3.md new file mode 100644 index 00000000000..92c160920d6 --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part1.round3.md @@ -0,0 +1,96 @@ +I read the file once, straight through, as a Prisma 7 user who has never seen this version. + +## Sentences I could not restate after one reading + +**1.** "Before you can build queries, run `npx prisma contract emit`. It reads your contract, the `contract.prisma` file that replaced `schema.prisma`, and writes `contract.json` and `contract.d.ts` next to it." + +What stopped me: "your contract" is used as if I know what a contract is. I know what `schema.prisma` is. The sentence tells me a file was renamed but not what changed inside it, and "contract" then reappears everywhere (`contract emit`, `contract.json`, `Contract` type, "contract features", "Contract authoring") as a load-bearing concept I was never given. Plainest wording I'd have understood: "In Prisma 8 your `schema.prisma` is called `contract.prisma`. Run `npx prisma contract emit` to generate `contract.json` and `contract.d.ts` beside it; the examples import both." That at least stops "contract" from sounding like a new abstraction. + +**2.** "The `types` block, `@@type`, `@@discriminator`, and `@@base` are contract features documented under Contract authoring, and no example on this page uses them." + +Four unexplained names plus an unexplained category ("contract features") in one sentence, telling me to ignore all of it. I stopped and re-read to check I wasn't meant to do something. + +**3.** "`db.runtime()` returns the connection that runs a built query. Do not `await` it. Only `runtime.query()` is awaited." + +"Do not `await` it" reads as a rule with a reason I'm not given. I could not tell if awaiting it breaks, or is merely pointless. Plainer: "`db.runtime()` is synchronous — it returns immediately." + +**4.** "`db.sql` is ready to use as soon as you have a client, so most code needs nothing more. If you wrap your own database client, the `sql(...)` function from `@prisma/orm-postgres/builder/runtime` builds the same query builder around that client." + +"wrap your own database client" — I don't know what this means or when I'd be doing it. There's no example and no link. After one reading I could not tell whether this concerns me. + +**5.** "Not available on SQLite, where the call does not compile. Called from plain JavaScript, or after a cast, it throws an error whose `code` is `ORM.CAPABILITY_MISSING` at the moment you call `lateralJoin()`." + +Three situations packed together, and "after a cast" assumes I know why anyone would cast. This is describing how the type system fails to protect you rather than telling me what to do. Plainer: "PostgreSQL only. On SQLite TypeScript rejects the call; if you get past the types, it throws `ORM.CAPABILITY_MISSING`." + +**6.** "A computed expression's result type comes from `.returns(...)` on `fns.raw`, or from the operation's own declared return type." + +"the operation's own declared return type" — which operation? The `fns.*` helper? Something I declared? I could not resolve it. + +**7.** "A type id is the PostgreSQL type name plus `@1`." + +The rule is immediately broken by its own list: `pg/timestamptz-temporal@1` is not a PostgreSQL type name, and the real prefix `pg/` is not mentioned in the rule at all. I could not derive the type id for `numeric` or `jsonb` from this sentence, which is the only reason the sentence exists. + +**8.** "or read the id itself from `db.sql.public.user.columns.id.codecId`." + +"codec" appears once, undefined. I guessed it means the thing that converts a database value to a JavaScript value, but the page never says. + +**9.** "After the join, a column name that both tables have must be written under its table, such as `f.post.id` or `f.post.createdAt`. A bare `select('id')` or `orderBy((f) => f.createdAt, ...)` throws, because a name both tables have is not in the query's scope on its own." + +This is in `lateralJoin()`'s Remarks, but the example joins `user` to an alias called `latestPost`, so "both tables" and `f.post.*` don't match the example above it. I could not work out whether `f.post` or `f.latestPost` is correct after the join. The example uses `f.latestPost.id` outside and `f.post.id` inside, which I only spotted on a second pass. + +**10.** "Put the `distinctOn()` columns first in `orderBy()`, then the column that decides which row wins. Nothing checks this for you, and without it you cannot predict which row you get." + +Two rules and a warning in one breath. I had to look at the example to understand the ordering rule at all. + +**11.** "TypeScript also accepts a `nulls` option, but the builder ignores it and no `NULLS FIRST` or `NULLS LAST` reaches the SQL. Do not use it." + +I understood the sentence. What I could not restate is what I'm supposed to do instead — I sort by a nullable column fairly often. + +## Words and phrases I had to guess + +- **"contract"** — guessed: the new name for the schema, plus whatever is generated from it. +- **"facet"**, in the anchor `#the-dbsql-facet` — the word never appears in the text. Guessed: "the `sql` part of `db`". +- **"after a cast"** — guessed: `as any` to get past TypeScript. +- **"codec"** — guessed: the value-conversion layer; `codecId` is the type id string. +- **"type id"** / `'pg/int4@1'` — guessed `@1` is a version number for the encoding, but I don't know what changes at `@2`. +- **"plan"** — the page says "The examples name the built query `plan`", so I read it as just a variable name, but "plan" is suggestive enough that I wondered if it was a query plan in the database sense. +- **"scope"** — defined, and the definition held up. This one worked. +- **`pgvector.Vector(1536)`** and `Embedding1536` — guessed: pgvector extension type; unused here. +- **"numeric expression"** in `limit(n)` — guessed: something built from `fns`, but I have no idea what it looks like. + +## Places I asked "so what do I actually type?" + +1. **Getting a contract at all.** The page says `contract.prisma` "replaced `schema.prisma`". Do I rename mine? Does something convert it? "Coming from Prisma ORM 7" is linked once, for "the rest of the setup steps", after the `contract emit` instruction — so I don't know whether the migration is before or after the command I was just told to run. +2. **Every example variable.** `aliceId`, `carolId`, `helloWorldId`, `untaggedPostId`, `newestPostId` are used in nine examples and defined nowhere. I can infer they're UUIDs I'd have on hand, but no example is copy-runnable. +3. **`import type { Contract } from './contract.d'`** — importing from a path ending `.d` looked wrong to me. I'd have typed `'./contract.js'` and guessed. +4. **A schema that isn't `public`.** "`public` is the PostgreSQL schema your tables are in." Mine is `app`. Is it `db.sql.app.user`? The page never says whether the key is my actual schema name. +5. **`limit()` with an expression.** "or a numeric expression" with no example. +6. **Wrapping my own client** — `sql(...)` is named, with no signature and no example. +7. **`.returns()` when omitted.** Is it required on every `fns.raw`? What is the type if I skip it? Not stated. +8. **Chain order.** Examples show `select().where()`, `innerJoin().select().where()`, and `select().orderBy().distinctOn()`. The only statement about order is that `distinctOn()` and `orderBy()` are order-independent — which made me assume order matters everywhere else, and nothing tells me the rules. +9. **What `runtime.query()` returns and what I do with `runtime` afterwards.** Nothing about closing, pooling, or reuse. `db.runtime()` is created once at the top of the entry-points example and never mentioned again. +10. **Seeing the SQL.** The opening paragraph sells "direct control over the generated SQL", and the page never shows how to look at the SQL. (I accept this may be in the half I don't have.) +11. **Enum values.** The schema declares `Priority { Low = "low" ... }` and examples write `fns.eq(f.priority, 'high')`. So I pass the mapped string, not `Priority.High`. I guessed that; the page doesn't say. + +## Places that explain the tool's insides when I only wanted to know what to do + +- **The example-schema paragraph:** "They are here only so that this schema matches the one on the ORM client reference, and you can ignore them." That's an explanation of how the documentation was assembled. It cost me a paragraph to learn that four models don't matter. +- **`ORM.COLUMN_UNKNOWN` "at the moment you call `select()`"** and the same "at the moment you call" construction for `ORM.CAPABILITY_MISSING`. Twice I was told *when in the builder's lifecycle* an error fires. I only wanted to know the call is invalid. +- **"Not available on SQLite, where the call does not compile. Called from plain JavaScript, or after a cast..."** — this describes the layers of the type system rather than the rule, which is just "PostgreSQL only". +- **The `nulls` option paragraph** — a description of a defect in the builder. Useful as a warning, but it tells me what the builder does internally with my input rather than what to write. +- **"`SelectQuery` in the tables below is the type they return. You never import it."** — the second sentence is about the type system, and I'd already assumed it. + +## Could I do what the page is for, after one reading? + +Partly. I could write a `select`, a `where` with `fns.and`/`fns.or`, an `innerJoin`, an `orderBy`, a `limit`/`offset`, and a `.as()` subquery. The chaining shape is clear and the examples for those are good. + +What I would still not know: + +- How to get from my Prisma 7 project to a working `contract.prisma` and `contract.json`. This is the first instruction on the page and it is the one I'd be stuck on. Everything downstream depends on it. +- What "contract" means, which matters because the word is in the command, the file names, the type parameter, and a linked section. +- What to pass to `.returns()` for any type outside the seven listed, because the stated rule for building a type id contradicts its own examples. +- Whether my non-`public` schema works, and what to type if it does. +- Where the example variables come from, so I could not run a single example as written. +- After a `lateralJoin`, whether my columns are under `f.post` or `f.latestPost` — the Remarks and the example disagree on the face of it. +- How to sort nulls last, which the page tells me I cannot do but does not tell me how to work around. +- Whether chain order matters anywhere other than `distinctOn`/`orderBy`. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part2.round3.md b/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part2.round3.md new file mode 100644 index 00000000000..c6a8aa26e5c --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part2.round3.md @@ -0,0 +1,85 @@ +Read once, top to bottom, as a Prisma 7 user who has never seen this API. + +## Sentences I could not restate after one reading + +**Line 150** — "On a grouped query, `distinct()` and `distinctOn()` drop repeated rows from the groups you already have, which only matters when you select fewer columns than you grouped by." +Stopped by: I cannot picture the case. Grouping already makes one row per group, so "repeated rows" sounds impossible. "Select fewer columns than you grouped by" is a condition I have to reason out myself, and there is no example. Plainer: give the two-column group / one-column select example, or cut the sentence. + +**Line 179** — "The value is generated when you call `build()`, so build once and run once. Running the same built insert twice inserts the same generated value twice. Build it again for a new one." +Stopped by: three ideas and an apparent contradiction. If the id is `@default(uuid())` and it is the primary key, running twice would fail, not "insert the same value twice." I could not tell whether this is a warning about a crash or about duplicate data. Plainer: "`build()` generates default values once and freezes them. If you run the same built insert twice you write the same id twice, which fails on a unique column. Call `.build()` again for each run." + +**Line 256** — "Every value the callback returns must be an expression, so you cannot put a plain JavaScript value beside a computed one." +Stopped by: "beside" — beside in the same object? In the same statement? And the fix given ("write it as a `fns.raw` fragment with `.returns(...)`") has no example, so I do not know what that looks like. + +**Line 322** — "`codecId` is the type id, the name this page uses everywhere else." +Stopped by: I read this three times. It parses as "codecId is what the rest of the page calls type id," but the wording makes "the name" point at nothing clear. Plainer: "The option is called `codecId`, but it takes the same type id used everywhere else on this page." + +**Line 363 vs 365** — "`in(expr, values)`, `notIn(expr, values)`, where `values` is an array or a built subquery" against "`exists(subquery)`, `notExists(subquery)`, taking a select query you do not call `build()` on". +Stopped by: `in()` wants a **built** subquery, `exists()` wants an **unbuilt** one. That is a real trap and the page states it in passing, in two table cells, with no example of `in()` with a subquery. I would have got this wrong. + +**Line 408** — "A `fns.raw` fragment only becomes an expression once you call it, so there is nowhere in the builder you can use a fragment without it." +Stopped by: "once you call it" — call what? The fragment or `.returns()`? Two "it"s in one clause pointing at different things. Also this is an internal reason for a rule I already accepted from the bold sentence before it. Plainer: "Always call `.returns()`. Nothing in the builder accepts a fragment without it." + +**Line 409** — the whole `.returns()` remark. +Stopped by: it is one paragraph carrying five claims (it is TypeScript-only; `EXTRACT` gives `numeric`; `numeric` arrives as a string; `CAST` does not help; three separate workarounds). One of the workarounds, "or select a real column of that type," is not a workaround at all for a computed value — I cannot select a real column that holds `EXTRACT(YEAR ...)`. That broke my reading of the list. + +**Line 440** — the whole "Running a built query" paragraph. +Stopped by: six facts jammed together (query vs execute, the rule, `AsyncIterableResult`, `await` gives `Row[]`, `for await` gives one row, `execute` gives `{ affectedRows }`). This is the most important paragraph on the page for "how do I run it," and it is the least broken up. It needs a short table. + +**Line 469** — "`for await` does not save memory on PostgreSQL: the client loads every row first, and both `await` and `for await` read that same in-memory result. The connection goes back to the pool before you read the rows." +Stopped by: this tells me how the client works internally and then leaves me with the obvious question unanswered — if it saves nothing, why would I ever use `for await`? The connection-pool sentence is unconnected to anything I have to decide. + +## Words and phrases I had to guess + +- **"plan"** (line 55 and everywhere). I guessed "the object `.build()` returns." The page never says the word means that; line 55 says the examples "name the built query `plan`," which reads as a naming convention, but then the anchor `#executing-a-plan` and the phrase "Running a built query" use it as if it were a defined term. +- **`runtime.execute`** (line 202). First appears inside a code comment. I guessed it is the sibling of `query` for writes. It has no entry of its own anywhere on the page — only prose at line 440. +- **`AsyncIterableResult`** (line 440). Guessed: a thing you can both `await` and `for await`. The page links it to a different page instead of saying so. +- **`codecId` / "codec"** (line 329). Guessed: codec = the thing that encodes and decodes a value, so `codecId` = the type id. Never stated. +- **pgvector** (line 393). Guessed: a PostgreSQL extension for vector columns. Not explained, and `cosineDistance` is named with no signature at all — I do not know if it takes one argument or two. +- **"the contract's default"** (line 179). Guessed: the `@default(...)` in `contract.prisma`. +- **"per-row type is recoverable"** (line 436). Guessed: "you can get the TypeScript type of one row." +- **`ORM.COLUMN_UNKNOWN`, `RUNTIME.DECODE_FAILED`, `RUNTIME.ENCODE_FAILED`, `ORM.MUTATION_DATA_MISSING`, `RUNTIME.ITERATOR_CONSUMED`**. I guessed these are the `code` property on a thrown error object. The page never shows one error being caught, so I do not know what I actually write to check for them. + +## "So what do I actually type?" + +1. **Line 256** — setting a fixed value inside the `update()` callback form. Told to "write it as a `fns.raw` fragment with `.returns(...)`". No example. This is the only place on the page that names a workaround and then does not show it. +2. **Line 363** — `in()` with a built subquery. Stated in a table cell, never shown. +3. **Line 393** — `cosineDistance`. No arguments, no example, no return type. +4. **Line 366** — the `raw` row of the built-in table gives no signature, unlike every other row. +5. **Line 186** — "Every column is optional." So what happens if I leave out a required column that has no default? Not said. I would have to try it. +6. **`returning()`** — can I write `returning('*')`? Can I return a computed expression? Only "Column names (`string`)" is given, and I do not know whether that is a restriction or just the common case. +7. **Line 94** — "You can also group by an alias you gave a selected value, such as `.groupBy('yearPlaced')`." The example directly above (lines 85–87) writes the same eight-token raw fragment twice instead. The page shows me the worse way and mentions the better one afterwards, so I do not know which one I am supposed to type. +8. **Any error code** — I never see `try { ... } catch (e) { if (e.code === ...) }`. Five codes are named and none is shown in use. +9. **Line 94** — "If you select a column you did not group or aggregate, the query fails when it runs." Fails with what? Every other failure on the page comes with a code. This one does not. + +## Places explaining the inside when I wanted the outside + +- **Line 188, 321, 397** — "Each value is sent as a query parameter, never pasted into the SQL" appears three times. Once, near the top, is enough. What I need to know is only the consequence: do not call `.returns()` on insert values. +- **Line 408** — "A `fns.raw` fragment only becomes an expression once you call it." The rule ("always call `.returns()`") is all I need. +- **Line 469** — "the client loads every row first," "the connection goes back to the pool before you read the rows." Pure internals. Neither changes anything I type. +- **Line 219** — "PostgreSQL and SQLite, the two databases the builder supports today. Both support SQL's `RETURNING`." Telling me *why* it is available on both is not something I asked. +- **Line 3** — "Call it after your `select()` calls, because a grouped query has no `select()` of its own." The reason is implementation trivia; "call `groupBy()` after all your `select()` calls" is the instruction. + +## Other things that tripped me + +- **Line 55 against line 58.** "Every example below assumes `const runtime = db.runtime();`" and then the very next line writes `const runtime = db.runtime();` anyway. On first read I assumed the assumption did not hold and looked for the difference. +- **Line 66** — `// e.g. { customerId: ..., orderCount: 5 }` shows a bare object, but `rows` is an array. Line 90 shows it correctly as `[{ ... }]`. Inconsistent within four lines of code. +- **Line 203** — a markdown link inside a code comment: `// see [Running a built query](#executing-a-plan)`. In a code block that renders as literal brackets. It is also where `execute` is introduced for the first time, buried in a comment. +- **`param()` is filed under "Mutations."** It has nothing to do with mutations — its own example (line 345) is a `select` with a `where`. I would never have found it looking for it. +- **Line 91** — "yearPlaced is the string '2024'" immediately after I was told to write `.returns('pg/int4@1')`. The explanation is 300 lines later at line 409, reached by "See `.returns()` below." On first read this looks like a bug in the example. +- **Line 404** — "A fragment used as a `where()` predicate declares `'pg/bool@1'`." "Declares" reads as something the tool does. I want "write `.returns('pg/bool@1')`." +- **Two different deep import paths** (`@prisma/orm-postgres/relational-core/expression` and `@prisma/orm-postgres/components/runtime`) with no hint why they differ. I would guess wrong when reaching for a third import. + +## Could I do what the page is for, after one reading? + +Mostly yes for the plain cases: group by a column with a count, filter with `having()`, insert, update, delete, and get rows back with `returning()`. Those examples are complete and I could copy them. + +What I still would not know: + +- How to set a fixed value alongside a computed one in `update()`. The page tells me it is possible and does not show it. +- Whether to pass `in()` a built or unbuilt subquery — I would have got this backwards from the `exists()` example. +- What `execute` is, in full. It is never given its own entry, and I met it in a code comment. +- What to actually type to check for any of the five error codes. +- Whether my number is going to come back as a string. I would have shipped `yearPlaced` as a string and found out in production, because the explanation is far from the example that hits it. +- When `for await` is worth using. The page tells me it saves no memory and then leaves the choice to me. +- What `cosineDistance` takes. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part1.round3.md b/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part1.round3.md new file mode 100644 index 00000000000..c682749f4f5 --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part1.round3.md @@ -0,0 +1,93 @@ +Read once, top to bottom, as a Prisma 7 user. Report below. + +## Sentences I could not restate after one reading + +1. **Line 9:** "Prisma ORM 8 replaces `schema.prisma` with `contract.prisma`. Run `npx prisma contract emit` to compile that file into `contract.json`, which your app reads at run time, and `contract.d.ts`, which TypeScript reads." + Four new things in two sentences: a renamed schema file, a new command, and two generated files with different consumers. I had to re-read to work out which file goes where. Plainer: "Prisma ORM 8 renames `schema.prisma` to `contract.prisma`. Run `npx prisma contract emit`. It writes two files next to it: `contract.json`, which your app imports, and `contract.d.ts`, which gives you the TypeScript types." + +2. **Line 11:** "The runtime is the connected object you run queries on. `db.runtime()` returns it." + `db` has not appeared yet. I do not know what `db` is at this point in the page. I guessed it was a client, which turned out right nine paragraphs later. + +3. **Line 13:** "Availability is stated per method: where the two databases differ, or where a method exists on only one of them, the Remarks say so." + "the Remarks" is capitalised as if it is a thing I already know. I have not seen a Remarks section yet. Plainer: "Each method below has a Remarks list. If a method behaves differently on PostgreSQL and MongoDB, or only exists on one, that list says so." + +4. **Line 13:** "To run one there, share one `MongoClient` between Prisma ORM and your own `mongodb` driver code and use a driver session." + Three instructions compressed into one sentence, and I cannot act on any of them. What is "a driver session"? How do I "share" a client — is that the `mongoClient` option? The page does not say here, and the link goes to a section I do not have. + +5. **Line 27:** "Pass `contract` instead only when you already hold a contract object in your own code, because Prisma ORM converts it to the same JSON." + I do not know what "a contract object" is or how I would come to hold one. Nothing on the page produces one. I could not restate this except as "ignore this option." + +6. **Line 59:** "In `.nativeEnums` each member's name is its own value, because a PostgreSQL enum type has values and no separate names." + I read this three times. "each member's name is its own value" is circular phrasing. Plainer: "`.nativeEnums` maps each value to itself — `db.nativeEnums.public.AalLevel.aal1` is `'aal1'` — because PostgreSQL enums have no separate names." + +7. **Line 140:** "It waits for a connection already being opened, then releases the pool it created. After `close()`, `connect()` rejects and `runtime()` throws an error whose `code` is `DRIVER.NOT_CONNECTED` (`Postgres client is closed`). When you bound the client with `pg` (your own pool or client), close that instance yourself, because the client only owns pools it created from a `url`." + One bullet carrying four separate facts: in-flight connect handling, post-close behaviour of two different methods, and ownership rules. "waits for a connection already being opened" is also the only mention of a race I did not know existed, and it is buried mid-sentence. + +8. **Line 208:** "`mode` | `'strict'` or `'permissive'` | No built-in behaviour. Your own middleware can read it." + I could not restate the point of this option. If it does nothing, why is it in the table, and how does middleware read it? Neither is answered. + +9. **Line 252:** "Running any query connects the client too, so if you then call `connect()`, it fails for the same reason. PostgreSQL raises the same code from the same situation." + The second sentence contradicts what I took from line 34 ("`connect()` is optional: call it to open the connection up front and fail early"). Now I learn that on PostgreSQL too, a query followed by `connect()` throws. That is a trap, stated as an aside in the MongoDB section, in a page where the PostgreSQL section came first. + +10. **Line 309:** "`runtime.close()` closes only the connection Prisma ORM opened and does not mark the client closed, so the client then looks usable and is not." + Useful warning, badly phrased. "looks usable and is not" made me stop. Plainer: "After `runtime.close()` the client will accept further calls and then fail." + +## Words and phrases I had to guess + +- **"contract"** — guessed: the new name for the schema, and also for the compiled artefact, and also for a TypeScript type (``), and also for a runtime object (`.contract`, `contract:` option). Four uses of one word, never distinguished. +- **"runtime"** — guessed: a connection handle. The word also names the import path (`@prisma/orm-postgres/runtime`), which is not the same thing and is never flagged. +- **"binding"** — guessed: a wrapper object so you can pick the connection source with an `if`. The word itself told me nothing. +- **"marker"** in `verifyMarker` — guessed: the row the migration writes. The option name only makes sense after reading the internals note. +- **"built query" / `.build()`** — guessed: a builder produces a plain object you hand to `runtime.query()`. Line 95 defines it, but it is used at line 95 before I have ever seen a builder chain. +- **"middleware"** — guessed from Express. The page says "an object with a name and one or more hooks" but never lists which hooks exist. I know `beforeQuery` only because it is in the example. +- **"telemetry"** — guessed: timings? Row counts? "reports how the most recent query went" is not a definition. +- **"extension packages"** — guessed: npm packages you import and pass in an array. The pgvector example carried this, not the table row. +- **`Symbol.asyncDispose` / `await using`** — I have not used these. The page assumes I have. +- **`AsyncIterableResult`** — guessed from the name; the definition is in the half I do not have. + +## Places I asked "so what do I actually type?" + +- **Line 9, "what replaces `prisma.user.findMany()`"** — the page raises the exact question I came with and sends me elsewhere. One line showing the new call would have answered it. +- **Line 27, the `contract` option** — no example anywhere on the page. If I have a contract object, what does the call look like, and where do I get `Contract` from without a `.d.ts`? +- **Line 33, "so run them"** — which command? `npx prisma migrate dev` still? Prisma 8 renamed the schema file, so I do not assume the migrate commands survived. +- **Line 32, middleware** — `isForbidden` is undefined, `query` has no shape, and no hook besides `beforeQuery` is named. I cannot write a middleware from this. +- **Line 46, `poolOptions`** — no example. I can guess from the type, but it is the one option with defaults worth seeing in context. +- **Line 13, MongoDB transactions** — no code at all. This is the loudest gap on the page: it tells me transactions are unavailable, tells me there is a workaround, and shows me none of it. +- **Line 70, `import contractJson from './contract.json' with { type: 'json' }`** — this syntax fails in most setups I have. What `module`/`target` in `tsconfig.json`, what Node version? Not said, while the far less risky `await using` does get a tsconfig note. +- **Line 69, `from './contract.d'`** — importing from a path ending in `.d` looks wrong. Is it? The page never says. +- **Line 72 vs line 241** — `url: process.env.DATABASE_URL` with no `!`, but `new MongoClient(process.env.MONGODB_URL!)` with one. One of these does not typecheck. Which? +- **Line 9 says the files live in `src/prisma/`, every example imports `./contract.json`** — so what is the real import path from my `src/index.ts`? +- **Line 97, `runtime.connection()`** — "Call `connection.release()` when you are done." No example, and the link points into the half I do not have. +- **PostgreSQL `runtime.execute()`** — mentioned at line 96, never shown. Every PostgreSQL example is a read. +- **Line 117, `db.sql.public.tag`** — line 58 told me `.sql` is "keyed by table name". It is not; there is a schema segment first. The schema segment appears only in a code comment. Same for `db.nativeEnums.public['AalLevel']` — bracket notation here, dot notation for `.enums.public.Role`, with no explanation of why. +- **Line 61 vs line 98** — line 61 lists `prepare()` as a member of the *client*; line 98 describes `runtime.prepare(...)`. Which object do I call it on? +- **Line 95 links to `#runtimequery`** — the only `runtime.query()` section on this page is under MongoDB. Following that link from the PostgreSQL section lands me on MongoDB documentation. +- **Line 101, "Every error has a `code` property"** — where is the list of codes? I see three. +- **Line 216** — "The [example schema] sets `@@map("users")` on `User`, which is why the key here is `users`." I cannot see that schema, so the sentence explains nothing to me. + +## Places explaining internals when I wanted instructions + +- **Line 33, `verifyMarker`:** "Your migrations write a row into your database recording which contract that database matches. With `verifyMarker: 'onFirstUse'`, the default, the first query compares that row with your contract, logs a warning if the row is missing or does not match, and runs the query anyway." I wanted: leave this alone; if you see the warning, run your migrations. The row, the comparison, and the timing are machinery. +- **Line 27:** "because Prisma ORM converts it to the same JSON" — internal detail justifying a recommendation I would have taken anyway. +- **Line 29 and 189:** "because it is one object instead of three separate option names" — that is a description of the API shape, not a reason to pick it. +- **Line 309:** the explanation of *why* `runtime.close()` is wrong ("does not mark the client closed") is longer than the instruction ("Call `db.close()`, not `runtime.close()`"). +- **Line 214:** "Prisma ORM's `MongoClient` type, not the driver's." True and important, but it reads as an implementation note where I expected to be told what to do about the name clash. + +## Could I do what the page is for? + +Partly. After one reading I could create a PostgreSQL client from a connection string, run a read query, and close it, because that exact path is shown three times. The same is true for creating a MongoDB client. + +What I would still not know: + +- How to write anything to either database. There is no `execute()` example on the page. +- How to run a transaction on MongoDB, which line 13 promises and never delivers. +- Whether to call `connect()` at all. Line 34 says it is optional; line 252 says calling it after a query throws. I do not know the safe pattern. +- How to write a middleware, or which hooks exist. +- What `mode` does. +- What `telemetry()` returns. +- Whether `db.sql` is keyed by table or by schema-then-table. The prose and the examples disagree. +- Whether `prepare()` is on the client or the runtime. +- What tsconfig I need for the JSON import in every single example. +- What `prisma.user.findMany()` becomes — the question the page opens with and never answers. + +The biggest structural problem: the page defines `runtime`, `built query`, `contract object`, and `Remarks` *after* it first uses them, and it puts the PostgreSQL/MongoDB `connect()` contradiction in the second section only. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part2.round3.md b/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part2.round3.md new file mode 100644 index 00000000000..bd6b36deb1e --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part2.round3.md @@ -0,0 +1,98 @@ +Read once, top to bottom, as a Prisma 7 user. + +## Sentences I could not restate after one reading + +**1.** "Use `withTransaction(...)` in a helper or a library that is handed a runtime rather than a client." +Stopped by: I do not know why anything would be handed a runtime instead of the client, or how that happens. The page never shows a helper receiving one. Plainer: "If you are writing a function that takes a runtime as a parameter instead of the client, use `withTransaction(...)`." And then show that function signature. + +**2.** "Open a connection and drive the transaction yourself when you need the same connection between statements." +Stopped by: this contradicts the next section. Remark 1 under `db.transaction` says "Every call on `tx` uses the same transaction connection." So `db.transaction` already gives me the same connection between statements. I cannot tell what the manual path actually buys me. The same sentence repeats at line 164, so the contradiction is doubled, not clarified. + +**3.** "Nothing retries a failed transaction for you, so when PostgreSQL reports a serialization failure, call `db.transaction(...)` again yourself." +Stopped by: two ideas fighting. The sentence before says there is "no way to set the isolation level." Serialization failures are a serializable-isolation thing. So either the default isolation can produce them and the page should say so, or this advice is unreachable. I also do not know what error code a serialization failure arrives as — the page tells me to compare `error.code` with a string everywhere else, but not here. + +**4.** "Annotate that parameter with `TransactionContext`, imported from `@prisma/orm-postgres/family-runtime`, which covers `query` and `execute`." +Stopped by: "covers" — covers what? I assume it means the type only has those two methods, but that contradicts the `tx` I get from `db.transaction`, which has `orm` and `sql` too. So if I annotate my helper's parameter with `TransactionContext` and pass it my `tx`, do I lose `tx.orm` inside the helper? The page does not say. No code example. + +**5.** "`for await` does not save memory on PostgreSQL: every row is loaded first." +Stopped by: this tells me the one reason I would reach for `for await` does not apply, then gives a different upside about pooled connections that I cannot map to a decision. After one reading I do not know which one to use. + +**6.** "Same result as `runtime.prepare(...)`, but off the client the SQL builder is handed to you as the first argument." +Stopped by: "off the client" used as a preposition twice on the page ("Prepare a statement off a runtime", line 230) and I had to guess it means "from". Plainer: "`db.prepare(...)` does the same thing, but it passes you a SQL builder as the callback's first argument, so you do not need `db.sql`." + +**7.** "`prepare()` starts the connection itself, so calling `db.connect()` afterwards rejects with an error whose `code` is `DRIVER.ALREADY_CONNECTED`. Reach for the runtime with `db.runtime()` instead, which returns the one `prepare()` already built." +Stopped by: three ideas in two sentences, and "the one `prepare()` already built" has no antecedent I can resolve — the one *what*? The runtime? The connection? I think it means `db.runtime()` returns the already-connected runtime, but I am guessing. + +**8.** "A signal that is **already aborted** when you call `query(...)` rejects before any row is fetched, with an error whose `code` is `RUNTIME.ABORTED` and whose `details.phase` is `'stream'`." +Stopped by: the phase says `'stream'` but nothing streamed. If `'stream'` is the value even when zero rows were fetched, what other values exist? The next line says "`details.phase` says where the abort landed" without listing the possible values. Useless to me as written — I cannot branch on it. + +**9.** "`lane` names which API built the query, such as `orm-client` for `db.orm` or `raw` for `db.raw.sql`." +Stopped by: "lane" is a word I have never seen used this way. The examples rescue it, but I had to read it twice. Plainer: call the field what it is, or say "`lane` is the name of the API you used." + +**10.** "`durationMs` is how long the query took, and the runtime records it for every query it times." +Stopped by: circular. It records the duration for every query it times — which queries does it not time? The type is written `durationMs?`, so it is sometimes absent, and this sentence is exactly the sentence that should tell me when, and does not. + +**11.** "Multi-document transactions for MongoDB are planned, but nothing on this page works on MongoDB today." +Stopped by: flatly contradicted by the last line of the page: "the full rules on reading a result, shared identically by PostgreSQL and MongoDB." The `AsyncIterableResult` section is on this page and does work on MongoDB. It also repeats the callout directly above it almost word for word. + +## Words and phrases I had to guess + +- **`family-runtime`** in `@prisma/orm-postgres/family-runtime`. I have no idea what "family" means. I guessed it is just an import path I copy and never think about. If that is true, fine, but it reads like it means something. +- **`@1` in `pg/text@1`.** I guessed a version number for the encoding. Never explained. +- **`pg/timestamptz-temporal@1`.** I guessed "temporal" means it maps to the TC39 Temporal API rather than `Date`. Pure guess, and if I guessed wrong my column types are wrong. +- **`codecId`** ("It is called `codecId` in code"). Guessed: the internal name of the same string. I do not know why I was told. +- **`SqlQueryPlan`.** Introduced only as "what the tables below call `SqlQueryPlan`". I guessed it is the return type of `.build()`, the thing part 1 called "the built query". Two names for one thing. +- **"plan"** in the anchor `#run-a-sql-builder-plan-with-txsql-and-txexecute` while the visible heading says "query". I guessed they are the same and the anchor is stale. +- **"bound the queries"** (line 13). Guessed: put a time limit on them. +- **"The `target` is explicit"** (line 326). Explicit as opposed to what? I guessed it means "you must pass it, it is not inferred." +- **`toArray()`** in the warning example (line 90). Nowhere defined. The `AsyncIterableResult` section says `await` the result or `for await` it — `toArray()` is a third thing that appears once, in the one example I most need to trust. +- **"Evict"** in `connection.destroy()`. Guessed: remove from the pool permanently. The Remarks say "throws that connection away", the table says "Evict" — pick one word. + +## Places I asked "so what do I actually type?" + +1. **`TransactionContext`.** "Annotate that parameter" — show me the function. `async function addTag(tx: TransactionContext) { ... }`. This is the recommended fix for nested transactions and it has zero code. +2. **Getting a type id from a column.** "To take one straight from a column, read `db.sql.public.user.columns.id`." Read it and do what with it? Does it *return* `'pg/uuid@1'`? Can I write `prepare({ id: db.sql.public.user.columns.id }, ...)`? One line of code would settle it. +3. **The full list of type ids.** I get four examples plus "the PostgreSQL type plus `@1`". Is it literally `pg/` + the PostgreSQL type name + `@1`? Then what is `timestamptz-temporal`? That is not a PostgreSQL type name. I cannot construct one for `jsonb` or `numeric` with confidence. +4. **MongoDB transactions.** "share one `MongoClient` between Prisma ORM and your driver code and use a driver session." No code at all. This is the only thing a MongoDB reader came to this section for, and it is one sentence and a link. +5. **Cleaning up a manual connection when something throws.** Both manual examples are happy-path straight lines. If `transaction.execute(...)` throws, `connection.release()` never runs and I have leaked a pooled connection. The page tells me "Always `release()` the connection when done" but shows no `try`/`finally`. Show the `try`/`finally`. +6. **Does `release()` after `rollback()` differ from after `commit()`?** And what happens if I release without committing — implicit rollback, or an error? Not stated. +7. **Preparing a write from the SQL builder.** Line 316 says a `PreparedExecution` comes from "raw SQL with `db.raw.sql` and ending in `.affectedCount()`". Can I prepare `db.sql.public.tag.insert([...])`? The section on `db.transaction` uses exactly that shape with `tx.execute`. Unanswered. +8. **What `prepare` does with parameters in the raw example.** `db.raw.sql\`... label = ${'archived'} ...\`` with `prepare({}, ...)` — the values are baked into the statement and the declaration is empty. So how do I write a prepared *execution* that takes parameters? The one example shows the case where I would not need prepare at all. +9. **Disposing a prepared statement.** Is there a `close()`? Do they leak? Do I keep one per process? Nothing. +10. **Does `signal` abort the transaction or only the statement?** Line 13 tells me to pass a signal to each call inside `db.transaction`. If one statement aborts mid-transaction, does the transaction roll back? I assume yes because the callback throws — but "assume" is the word. +11. **Transaction timeouts.** "There are no `isolationLevel`, `timeout`, or `maxWait` options." Coming from Prisma 7, where `timeout` defaults to 5 seconds, my first question is: does a transaction now run forever? Not answered. + +## Places the page explains internals when I only wanted to know what to do + +- **"It is called `codecId` in code."** I do not read the source. Delete it. +- **"`db.transaction(...)` is built on it."** (line 112) True but irrelevant to choosing between them. +- **The whole `scope` remark** (line 375). Three lines to introduce an option, explain that middleware consumes it, and then tell me "Do not pass it yourself." If I must not pass it, it does not belong in a reference of options I pass. One sentence under the table would do: "`scope` is set for you; do not pass it." +- **"`for await` does not save memory on PostgreSQL: every row is loaded first. The upside is that a result you hold or read slowly does not keep a pooled connection checked out, because the client hands the connection back before you read the rows."** This is connection-pool mechanics. What I need is the rule: use `await`; use `for await` when X. +- **"An `AsyncIterableResult` is tied to the transaction connection."** (line 82) The rule that matters is "collect rows inside the callback." The reason is fine as a half-clause; it currently opens the callout. + +## Broken or mislabeled references + +- **"(see the Remark below)"** at line 18. The thing below is a `:::warning` callout, not a Remark, and it sits after the Examples. I scrolled looking for a bullet. +- **"See [`postgres(options)`](#postgresoptions)"** in the return-type table for `tx.enums`. That anchor is on the half of the page I do not have, and the link text tells me nothing about why I would follow it. +- **The `tx.enums` / `tx.nativeEnums` row is in a table headed "Return type".** They are not return types of `db.transaction`. It confused me into thinking `db.transaction` returns them. +- **The manual-control table is headed "Options"** but its columns are `Method | Type | Description` and every Type cell says "none". It is a method list wearing an options heading. +- **`#run-a-sql-builder-plan-with-txsql-and-txexecute`** — anchor says "plan", heading says "query". + +## Could I do what this page is for, after one reading? + +Partly. I could write `db.transaction(async (tx) => ...)` with `tx.orm` and `tx.execute`, and I could port my `$transaction([...])` array. The migration diff at line 96 is the single most useful thing on the page — it is the only place that connects what I already know to what I now type. + +Beyond that, no. + +What I still would not know: + +- **When to use the manual connection path.** The stated reason ("the same connection between statements") is the same thing `db.transaction` already gives me. I would use `db.transaction` for everything and never find out what I was missing. +- **How to write a helper that takes a transaction.** The page tells me to do this to avoid nesting and then shows no code and leaves me unsure whether the helper can still use `tx.orm`. +- **How to build a type id for any column that is not `text`, `int4`, `uuid`, or `bool`.** So I could not prepare a statement against my own schema. +- **How to prepare anything parameterized that writes.** The only `PreparedExecution` example hard-codes both its values. +- **Whether my transactions can hang.** No timeout, no default stated. +- **What to catch and retry.** I am told to retry serialization failures and given no code to compare against, on a page where every other error hands me an exact string. +- **How to clean up safely.** Every manual example leaks the connection if a statement throws. +- **Anything about MongoDB transactions** beyond "not yet", stated three times. + +One correctness problem worth checking with the author: `escaped.rows.toArray()` at line 90. `toArray()` appears exactly once on this page and is absent from the `AsyncIterableResult` section that claims to give the reading rules. Either that section is missing a method or the example is wrong. \ No newline at end of file From 6c7f8e765157900ad035998d7ef9cd0582ae7e25 Mon Sep 17 00:00:00 2001 From: willbot Date: Sun, 13 Sep 2026 09:31:56 +0200 Subject: [PATCH 08/14] docs(orm8): orm/reference pages, reader-review round four applied Last wording round, cutting preferred. Each rule now stated once: the joined-column rule, the distinctOn() ordering, the .node table (one copy near match(), one in the expression helpers), the type-id convention with the contract type named beside the PostgreSQL type. Facts changed only where a reader's question had a verified answer: annotate a raw fragment with the type PostgreSQL computes (pg/numeric@1 for EXTRACT), param() is for a value whose inferred id is not the column's, countBigInt() compares against a bigint, the tx.execute() result, a nullable entry in a .returnsRow() spec, the RUNTIME.RAW_ROW_COLUMN_MISSING and SqlQueryError.sqlState failures, sort() keeps findOneAndUpdate() available. Pages total 3,412 lines. Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- .../docs/content/docs/orm/reference/index.mdx | 36 ++-- .../docs/orm/reference/pipeline-builder.mdx | 176 +++++++++--------- .../docs/orm/reference/raw-queries.mdx | 38 ++-- .../docs/orm/reference/sql-query-builder.mdx | 175 +++++++++-------- .../reference/transactions-and-runtime.mdx | 109 ++++++----- .../pipeline-builder.part1.round4.md | 133 +++++++++++++ .../pipeline-builder.part2.round4.md | 112 +++++++++++ .../raw-queries-and-index.round4.md | 146 +++++++++++++++ .../sql-query-builder.part1.round4.md | 75 ++++++++ .../sql-query-builder.part2.round4.md | 66 +++++++ .../transactions-and-runtime.part1.round4.md | 83 +++++++++ .../transactions-and-runtime.part2.round4.md | 81 ++++++++ 12 files changed, 958 insertions(+), 272 deletions(-) create mode 100644 docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part1.round4.md create mode 100644 docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part2.round4.md create mode 100644 docs/orm-docs-audit/c21-orm-reference/raw-queries-and-index.round4.md create mode 100644 docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part1.round4.md create mode 100644 docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part2.round4.md create mode 100644 docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part1.round4.md create mode 100644 docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part2.round4.md diff --git a/apps/docs/content/docs/orm/reference/index.mdx b/apps/docs/content/docs/orm/reference/index.mdx index d167eb0498d..51cf5ee9269 100644 --- a/apps/docs/content/docs/orm/reference/index.mdx +++ b/apps/docs/content/docs/orm/reference/index.mdx @@ -9,10 +9,12 @@ metaDescription: 'Reference index for the Prisma ORM client, SQL query builder, Prisma ORM gives you three typed ways to write a query. In Prisma ORM 7 you wrote `findMany({ where, include })`. Now each of those arguments is a method you chain: `.where(...).include(...)`. See [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7), which maps each argument to its method. - The **ORM client** is at `db.orm`. It has model methods such as `where()`, `create()`, and `include()`, it works on both PostgreSQL and MongoDB, and it covers everyday reads and writes across your models. -- The **SQL query builder** is at `db.sql`. It has table methods that match SQL, such as `select()`, `innerJoin()`, and `groupBy()`, it works on PostgreSQL only, and it returns results that are not model rows, such as an aggregate over a join. -- The **pipeline builder** is at `db.query`. It builds MongoDB aggregation pipelines. +- The **SQL query builder** is at `db.sql`. It has table methods that match SQL, such as `select()`, `innerJoin()`, and `groupBy()`, it is PostgreSQL only, and it returns whatever shape your `select()` asks for, not objects shaped like your models. +- The **pipeline builder** is at `db.query`. It builds MongoDB aggregation pipelines, and it is MongoDB only. -Plus raw SQL and raw commands when none of them fits. See [Raw queries](/orm/reference/raw-queries). For connecting and disconnecting the client, transactions, and prepared statements, see [Transactions and runtime](/orm/reference/transactions-and-runtime). In the example below, `db.runtime()` returns the connection that runs a built query. Create it once and reuse it. `.all()` runs the query and returns every row. It is what replaced `findMany`. The ORM client runs when you `await` it. The SQL query builder and the pipeline builder do not run on their own. Each produces a built query with `.build()`, which you then run with `runtime.query(...)`. Watch the casing: `User` is the model name and `user` is the table name. +Plus raw SQL and raw commands when none of them fits. See [Raw queries](/orm/reference/raw-queries). For connecting and disconnecting the client, transactions, and prepared statements, see [Transactions and runtime](/orm/reference/transactions-and-runtime). + +How a query runs: the ORM client runs when you `await` it. The SQL query builder and the pipeline builder each give you a built query from `.build()`, which you run with `runtime.query(...)`, or with `runtime.execute(...)` for a write that returns no rows. Get `runtime` from `db.runtime()` once and reuse it. ```ts const runtime = db.runtime(); @@ -21,26 +23,20 @@ const plan = db.sql.public.user.select('id', 'email').where((f, fns) => fns.eq(f const rows = await runtime.query(plan); ``` -`db.query` builds MongoDB pipelines. The separate `runtime.query()` method is what runs any built query. +`const runtime = db.runtime();` is the PostgreSQL form. On MongoDB the call returns a promise, so there you write `const runtime = await db.runtime();`. The two callbacks differ: the ORM client compares on the column (`u.kind.eq(...)`), and the SQL query builder compares through `fns` (`fns.eq(f.kind, ...)`). `.all()` runs the query and returns every row. It is what replaced `findMany`. + +Watch the casing: `User` is the model name and `user` is the table name. + +Two different things are both named `query`. `db.query` builds a MongoDB pipeline. `runtime.query(...)` runs a query that has already been built. ## How database differences are documented Every method on a reference page has the same four parts: Remarks, Options, Return type, and Examples. If a method works differently on PostgreSQL and MongoDB, or exists on only one of them, its Remarks say so. For a walkthrough of reading and writing data rather than a method list, see [Reading data](/orm/fundamentals/reading-data), [Writing data](/orm/fundamentals/writing-data), [Relations and joins](/orm/fundamentals/relations-and-joins), [Transactions](/orm/fundamentals/transactions), and [Advanced queries](/orm/fundamentals/advanced-queries). - }> - Every ORM client method, with PostgreSQL and MongoDB behavior documented side by side. - - }> - Every SQL query builder method for building typed, table-level queries against PostgreSQL. - - }> - Every MongoDB pipeline-builder stage, accumulator, expression helper, and write method. - - }> - Raw SQL on PostgreSQL and raw commands on MongoDB, for queries the typed APIs cannot express. - - }> - Connecting and disconnecting the client, transactions, prepared statements, and execution options. - - + }>Every ORM client method, with PostgreSQL and MongoDB behavior documented side by side. + }>Every SQL query builder method for building typed, table-level queries against PostgreSQL. + }>Every MongoDB pipeline-builder stage, accumulator, expression helper, and write method. + }>Raw SQL on PostgreSQL and raw commands on MongoDB, for queries the typed APIs cannot express. + }>Connecting and disconnecting the client, transactions, prepared statements, and execution options. + \ No newline at end of file diff --git a/apps/docs/content/docs/orm/reference/pipeline-builder.mdx b/apps/docs/content/docs/orm/reference/pipeline-builder.mdx index c33750b4d20..bbd46a62260 100644 --- a/apps/docs/content/docs/orm/reference/pipeline-builder.mdx +++ b/apps/docs/content/docs/orm/reference/pipeline-builder.mdx @@ -8,7 +8,7 @@ metaDescription: 'Reference for the Prisma ORM MongoDB pipeline builder''s stage The pipeline builder gives you a typed way to build MongoDB aggregation pipelines. You reach it through `db.query`, chain aggregation stages onto a starting collection, and finish the chain with the call that builds the query. It is the MongoDB counterpart to the [SQL query builder](/orm/reference/sql-query-builder): a lower-level API for the queries the [ORM client](/orm/reference/orm-client) doesn't express. -Everything the builder produces becomes a MongoDB aggregation pipeline. There is no `find()` or `distinct()` here. Use `db.orm` or [`rawCommand()`](#rawcommand) for those. To fetch a single document, filter and then `limit(1)`. Use the ORM client (`db.orm`) for everyday reads and writes across models and relations, and the pipeline builder when you need a stage the ORM client doesn't expose: grouping, `$lookup` joins with post-processing, computed projections, multi-stage transformations, or stages that write the results into a collection (`$out` and `$merge`). This page documents every stage, accumulator, expression helper, and write method. +Everything the builder produces becomes a MongoDB aggregation pipeline. There is no `find()` or `distinct()` here. Use `db.orm` or [`rawCommand()`](#rawcommand) for those. To fetch a single document, filter and then `limit(1)`. Use the ORM client (`db.orm`) for everyday reads and writes across models and relations. Reach for the pipeline builder when you need to group documents, to join another collection and keep transforming the result, or to write the result into a collection (`$out` and `$merge`). This page documents every stage, accumulator, expression helper, and write method. :::note[This is the MongoDB pipeline builder] The pipeline builder targets MongoDB only. PostgreSQL has no pipeline builder: use the [SQL query builder](/orm/reference/sql-query-builder) for PostgreSQL joins and aggregates, and the [ORM client](/orm/reference/orm-client) for everyday queries on either database. @@ -16,7 +16,7 @@ The pipeline builder targets MongoDB only. PostgreSQL has no pipeline builder: u ## Example schema -All examples on this page run against the schema below. It is the same MongoDB schema as the [ORM client](/orm/reference/orm-client) page, so examples carry over between the two pages. `Article` and `Tutorial` are variants of `Post`. They are stored in the same `posts` collection and told apart by the value of the `kind` field. See [`variant()`](/orm/reference/orm-client#variant) on the ORM client page for how to query one variant. You do not need `variant()` on this page, because `kind` is an ordinary field you can filter and group on. +All examples on this page run against the schema below. It is the same MongoDB schema as the [ORM client](/orm/reference/orm-client) page, so examples carry over between the two pages. `Article` and `Tutorial` are two kinds of `Post`, stored in one `posts` collection and told apart by the value of the `kind` field. `@@base(Post, "article")` and `@@discriminator(kind)` set that up. See [`@@base`](/orm/reference/orm-client#variant) on the ORM client page. `@@type("mongo/string@1")` says the enum is stored as a MongoDB string, and `@1` is the version of that type id, always `@1` today. On this page you never need any of that, because `kind` is an ordinary field you can filter and group on.
@@ -81,7 +81,7 @@ model Tutorial { Every pipeline starts with `db.query.from('posts')`, naming the collection to aggregate over. It ends with a call that turns the chain into a query you can run. Between them you chain stages. -`db` is the client you create with `mongo(...)`, whose `dbName` option is the name of the MongoDB database to use. See [Transactions and runtime](/orm/reference/transactions-and-runtime#mongooptions) for every option and for how to create the client. +`db` is the client you create with `mongo(...)`, whose `dbName` option is the name of the MongoDB database to use. See [Transactions and runtime](/orm/reference/transactions-and-runtime#mongooptions) for every option and for how to create the client. Three import paths appear on this page: `@prisma/orm-mongo/runtime` holds the `mongo()` client, `@prisma/orm-mongo/query-builder` holds `mongoQuery`, `fn`, `acc`, and `expr`, and `@prisma/orm-mongo/query-ast/execution` holds the `Mongo*Stage` classes, `MongoAggFieldRef`, and `MongoFieldFilter`. Every example holds the built query in a variable named `plan`. Prisma ORM 8 renames `schema.prisma` to `contract.prisma`. Run `npx prisma contract emit` before any code on this page compiles: it writes `contract.json`, used at run time, and `contract.d.ts`, used for types. The example below imports `contract.json` with `with { type: 'json' }`, which works with the TypeScript settings `prisma orm init` writes for you. [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7) has the setup steps. @@ -92,9 +92,9 @@ Enter the pipeline builder on a collection. #### Remarks - `from()` takes the collection name, the same name you use on `db.orm` (`'posts'`, `'users'`), not the model name `Post`. The collection name is the model's `@@map` value, or the model name with a lowercase first letter when there is no `@@map`. -- Passing an unknown collection name throws right away, with an error whose `code` is `ORM.MODEL_UNKNOWN` and whose message is `Unknown root: "". Valid roots: ...`. +- Passing an unknown collection name throws right away, with an error whose `code` is `ORM.MODEL_UNKNOWN` and whose message is `Unknown root: "". Valid roots: ...` (the message says root; it means the collection name). - `from()` returns a builder you chain stages onto. Call `insertOne()` and `insertMany()` on `from()` directly, before any stage. -- You can build the same pipelines without a client. `import { mongoQuery } from '@prisma/orm-mongo/query-builder'`, then `mongoQuery({ contractJson }).from('posts')`. Import `contractJson` the same way the example below does. +- You can build the same pipelines without a client. `import { mongoQuery } from '@prisma/orm-mongo/query-builder'`, then `mongoQuery({ contractJson }).from('posts')`. Import `contractJson` the same way the example below does. `mongoQuery` only builds queries. It never runs one, so you still need a client's `runtime` to get results. Use it in tests, or to build a query once and run it through any client you like. #### Options @@ -126,15 +126,14 @@ const posts = await runtime.query(plan); ### Building and executing a pipeline -Nothing runs until you execute the pipeline. `build()` turns the chain into a built query, which the examples hold in a variable named `plan`. `runtime.query(...)` runs it. Every later example reuses the `db` and `runtime` from the example above. +`build()` returns the built query, which the examples hold in a variable named `plan`. Nothing runs until you pass it to `runtime.query(...)`. Every later example reuses the `db` and `runtime` from the example above. #### Remarks - On MongoDB, `db.runtime()` returns a promise, so you must await it: `const runtime = await db.runtime()`. Hold it in a variable and reuse it. - `await` that call for an array of documents, or write `for await (const doc of runtime.query(plan)) { ... }` to take one document at a time. - There is no `db.execute` on the MongoDB client. -- **After `lookup()` or `group()`, compare ids with `String(a._id) === String(b._id)`**, where `a` is a looked-up sub-document and `b` is the parent document it came from. Both `_id` values are the driver's own `ObjectId` objects there, and two `ObjectId` objects are never `===` even when they hold the same id. -- **Read results are decoded.** If every stage in your pipeline is one of `match()`, `sort()`, `limit()`, `skip()`, `sample()`, `project()`, `addFields()`, and `vectorSearch()`, results are decoded: `_id` is a hex string and dates are your contract's date type. If any stage is not in that list, including `lookup()` and `group()`, every document comes back raw: `_id` is the driver's `ObjectId` and dates are the driver's `Date`. +- **Read results are decoded.** If every stage in your pipeline is one of `match()`, `sort()`, `limit()`, `skip()`, `sample()`, `project()`, `addFields()`, and `vectorSearch()`, `_id` comes back as a hex string. If any stage is not in that list, including `lookup()` and `group()`, every document comes back raw and `_id` is an `ObjectId` object from the `mongodb` package, the MongoDB driver Prisma ORM uses. Compare those ids as strings, as [`lookup()`](#lookup) shows. A `DateTime` field comes back as a JavaScript `Date` either way. #### Examples @@ -156,9 +155,16 @@ Filter documents by a predicate. #### Remarks - `match()` takes a callback whose `f` argument has one property per field, and returns a filter expression (`(f) => f.kind.eq('tutorial')`). -- Neither a hex string nor a driver `ObjectId` matches in `match()`. The builder sends the value as a plain object, so an `ObjectId` arrives as its bytes. See the warning below. - Scalar fields expose these filter operators: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`, `exists`, and `type`. They work like the MongoDB query operators of the same name (`$ne`, `$gt`, `$in`, `$exists`, `$type`, and so on). -- To compare computed values rather than a field against a constant, wrap the comparison in `expr(...)`: `match((f) => expr(fn.gt(fn.year(f.createdAt), fn.literal(2023))))`. `fn` holds the helpers that build expressions, and `expr()` wraps one so `match()` accepts it. The `fn.*` helpers take expressions, not plain values, so wrap every constant in `fn.literal()`. See [Expression helpers](#expression-helpers). +- To compare two computed values rather than a field against a constant, wrap the comparison in `expr(...)`: `match((f) => expr(fn.gt(fn.year(f.createdAt), fn.literal(2023))))`. `fn` holds the helpers that build expressions, one per MongoDB operator. `expr()` wraps one of those values so `match()` accepts it. The `fn.*` helpers take expressions, not plain values, so wrap every constant in `fn.literal()`. See [Expression helpers](#expression-helpers). + +Where each form goes. `.node` is the raw MongoDB expression inside an `fn.*` value: + +| Call site | What you pass | +|---|---| +| `project()`, `addFields()`, and the arguments of an accumulator | The `fn.*` value itself. | +| The condition argument of `fn.cond()`, `redact()`, and the stages that take an options object | The `.node` property of the `fn.*` value. | +| `match()` | The `fn.*` value wrapped in `expr(...)`. | #### Options @@ -197,7 +203,7 @@ const recent = await runtime.query(plan); ``` :::warning[`_id` equality filters do not work through the pipeline builder] -Filtering by `_id` equality inside `match()` **never matches any document**. Neither a hex string (`f._id.eq('507f...')`) nor the driver's own `ObjectId` matches. There is no supported way to filter by `_id` equality through the typed pipeline builder. To fetch one document by its id, use the [ORM client](/orm/reference/orm-client), which converts the hex string for you. The pipeline builder does not convert it: +Filtering by `_id` equality inside `match()` **never matches any document**. Neither a hex string (`f._id.eq('507f...')`) nor an `ObjectId` matches. There is no supported way to filter by `_id` equality through the typed pipeline builder. To fetch one document by its id, use the [ORM client](/orm/reference/orm-client), which converts the hex string for you. The pipeline builder does not convert it: ```typescript const postId = '6650f1c2a1b2c3d4e5f60003'; @@ -357,7 +363,8 @@ Join documents from another collection (`$lookup`). - `lookup()` takes a callback that builds the join with `from(collection).on((local, foreign) => ({ local, foreign })).as(name)`: the other collection, the two fields to match on, and the name of the output array field. - The object returned by `on()` has two fixed keys, `local` and `foreign`. Write `({ local: local.authorId, foreign: foreign._id })`. -- The joined documents arrive in an array under the `as` name. Compare their ids with `String(a._id) === String(b._id)`. +- The joined documents arrive in an array under the `as` name. When nothing matches, that array is empty. +- A joined document's `_id` and the `_id` of the document it was joined to are both `ObjectId` objects, and two `ObjectId` objects are never `===` even when they hold the same id. Compare them as strings: `String(post.author[0]._id) === String(post.authorId)`. #### Options @@ -536,7 +543,7 @@ Promote a computed sub-document to the top level. #### Remarks -- `replaceRoot()` takes a callback returning a **document-shaped** expression. A bare scalar is rejected by MongoDB at runtime (`'newRoot' expression must evaluate to an object`). +- `replaceRoot()` takes a callback returning an expression that evaluates to an object. A bare scalar is rejected by MongoDB at runtime (`'newRoot' expression must evaluate to an object`). - A common pattern is to promote the first element of a `lookup()` array with `fn.arrayElemAt(f.author, fn.literal(0))`. #### Options @@ -573,7 +580,7 @@ const authors = await runtime.query(plan); ### `count()` -Reduce the pipeline to a single document holding the count. +Reduce the pipeline to a single document holding the count. It is not `acc.count()`, which counts the documents in one group inside `group()`. #### Options @@ -626,7 +633,6 @@ const plan = db.query .sortByCount((f) => f.kind) .build(); const byKind = await runtime.query(plan); -// two buckets, each { _id: , count: 1 }. Order of tied counts is not guaranteed. ``` ### `redact()` @@ -636,14 +642,8 @@ Keep or prune documents (and sub-documents) based on an expression. #### Remarks - `redact()` maps to MongoDB's `$redact`, whose expression must evaluate to one of MongoDB's `$$` variables: `$$KEEP`, `$$DESCEND`, or `$$PRUNE`. -- `f.rawPath(path)` names a field or a MongoDB `$$` variable by its raw string, with no checking against your model. It prefixes `$` to the string you give it, so `f.rawPath('$KEEP')` reaches MongoDB as `$$KEEP`. There is no expression helper for a `$$` variable, and `fn.literal(...)` cannot stand in. The server rejects both `fn.literal('KEEP')` and `fn.literal('$$KEEP')`. -- For anything more involved, use [`rawCommand()`](#rawcommand), where you write the `$redact` stage with `$$KEEP` and `$$PRUNE` directly. - -| Call site | What you pass | -|---|---| -| `project()`, `addFields()`, and the arguments of an accumulator | The `fn.*` value itself. | -| The condition argument of `fn.cond()`, `redact()`, and the option-object stages | The `.node` property of the `fn.*` value. | -| `match()` | The `fn.*` value wrapped in `expr(...)`. | +- Write those variables with `f.rawPath('$KEEP')` and `f.rawPath('$PRUNE')`. `f.rawPath(path)` names a field or a MongoDB `$$` variable by its raw string, with no checking against your model, and it prefixes `$` to the string you give it. There is no expression helper for a `$$` variable, and `fn.literal(...)` cannot stand in: the server rejects both `fn.literal('KEEP')` and `fn.literal('$$KEEP')`. +- For which `fn.*` values need their `.node` property here, see the table under [`match()`](#match). For anything more involved, use [`rawCommand()`](#rawcommand), where you write the `$redact` stage with `$$KEEP` and `$$PRUNE` directly. #### Options @@ -675,21 +675,20 @@ const tutorials = await runtime.query(plan); ### Option-object stages -Several stages take a single MongoDB stage options object rather than a typed `(f) => ...` callback. Each one carries the same meaning it has in MongoDB. +Several stages take a single MongoDB stage options object rather than a typed `(f) => ...` callback. Each one carries the same meaning it has in MongoDB. Where an option below is called a raw expression, it wants a MongoDB expression object rather than a typed builder value: build one with the `fn.*` helpers and pass its `.node` property, or name a field with `MongoAggFieldRef.of('duration')`, which is already a raw expression and takes no `.node`. #### Remarks - `bucket({ groupBy, boundaries, default_?, output? })` sorts documents into the ranges you name. `default` is a reserved word in JavaScript, so the key is `default_`. `groupBy` is a raw expression. -- `bucketAuto({ groupBy, buckets, output?, granularity? })` sorts them into a given number of ranges, chosen so each holds about the same number of documents. `groupBy` is a raw expression. `granularity` picks the bucket size from one of MongoDB's preferred-number series. +- `bucketAuto({ groupBy, buckets, output?, granularity? })` sorts them into a given number of ranges, chosen so each holds about the same number of documents. `groupBy` is a raw expression. `granularity` is a string that MongoDB reads as the series it rounds the bucket boundaries to. Prisma ORM passes it through unchanged, accepts any string, and MongoDB rejects the ones it does not know. - `geoNear({ near, distanceField, spherical?, maxDistance?, minDistance?, query?, key?, distanceMultiplier?, includeLocs? })` sorts documents by their distance from a point and writes that distance into `distanceField`. `near` is a raw expression. - `graphLookup({ from, startWith, connectFromField, connectToField, as, maxDepth?, depthField?, restrictSearchWithMatch? })` follows links from document to document through one collection and collects what it reaches into `as`. `startWith` is a raw expression. -- `setWindowFields({ partitionBy?, sortBy?, output })` computes a value for each document from the documents around it, such as a running total. `partitionBy` is a raw expression. `output` is an object mapping each new field name to `{ operator, window? }`, where `operator` is the accumulator or expression to compute. +- `setWindowFields({ partitionBy?, sortBy?, output })` computes a value for each document from the documents around it, such as a running total. `partitionBy` is a raw expression. `output` is an object mapping each new field name to `{ operator, window? }`, where `operator` is a raw expression, usually an accumulator: `{ runningTotal: { operator: MongoAggAccumulator.sum(MongoAggFieldRef.of('duration')), window: { documents: ['unbounded', 0] } } }`. - `densify({ field, partitionByFields?, range })` adds documents to fill the gaps in a sequence of numbers or dates. `range` is `{ step, unit?, bounds }`, where `bounds` is `'full'`, `'partition'`, or a pair of start and end values. -- `fill({ partitionBy?, partitionByFields?, sortBy?, output })` replaces missing or null values with a value you choose. `partitionBy` is a raw expression. `output` is an object mapping each field name to `{ method }` or `{ value }`, where `value` is an expression. +- `fill({ partitionBy?, partitionByFields?, sortBy?, output })` replaces missing or null values with a value you choose. `partitionBy` is a raw expression. `output` is an object mapping each field name to `{ method }` or `{ value }`, where `value` is an expression and `method` is a string that Prisma ORM passes through to MongoDB unchanged. - `facet(facets)` takes an object mapping each output field name to an array of stages. - `unionWith(collection, pipeline?)` takes a collection name and an optional array of stages. -- Build a raw expression with the `fn.*` helpers and pass its `.node` property, or name a field with `MongoAggFieldRef.of('duration')`. -- The stages you pass to `facet` and `unionWith` are classes: `MongoMatchStage`, `MongoProjectStage`, `MongoSortStage`, `MongoLimitStage`, `MongoSkipStage`, `MongoLookupStage`, `MongoUnwindStage`, `MongoGroupStage`, `MongoAddFieldsStage`, `MongoReplaceRootStage`, `MongoCountStage`, `MongoSortByCountStage`, `MongoSampleStage`, `MongoRedactStage`, `MongoOutStage`, `MongoUnionWithStage`, `MongoBucketStage`, `MongoBucketAutoStage`, `MongoGeoNearStage`, `MongoFacetStage`, `MongoGraphLookupStage`, `MongoMergeStage`, `MongoSetWindowFieldsStage`, `MongoDensifyStage`, `MongoFillStage`, `MongoSearchStage`, `MongoSearchMetaStage`, and `MongoVectorSearchStage`. Import them, and `MongoAggFieldRef`, from `@prisma/orm-mongo/query-ast/execution`. Each stage class takes the same arguments as the MongoDB stage it builds. The three used below are `MongoCountStage(field)`, `MongoSortStage(spec)`, and `MongoLimitStage(n)`. +- The stages you pass to `facet` and `unionWith` are classes, one class per MongoDB stage, named `MongoStage`. Import them, along with `MongoAggFieldRef` and `MongoAggAccumulator`, from `@prisma/orm-mongo/query-ast/execution`. Each class takes the same arguments as the MongoDB stage it builds. The three used below are `MongoCountStage(field)`, `MongoSortStage(spec)`, and `MongoLimitStage(n)`. #### Examples @@ -719,7 +718,7 @@ const plan = db.query }) .build(); const [facets] = await runtime.query(plan); -// facets.totalCount is [{ count: 2 }]; facets.newest is the two newest posts +// facets.totalCount is [{ count: }]; facets.newest is the two newest posts ``` ##### Sort into ranges with `bucket()` @@ -747,7 +746,7 @@ const buckets = await runtime.query(plan); #### Remarks -- These stages need MongoDB Atlas. The query builds, and MongoDB rejects it when it runs. +- These stages need MongoDB Atlas. Nothing stops you from building them against a database that is not on Atlas. The error only appears when you run the query. - `search(config, index?)` takes an Atlas Search operator object, for example `{ text: { query: 'hello', path: 'title' } }`, and an optional `index`, which names the Atlas Search index to use. - `searchMeta(config, index?)` takes the same two arguments and returns counts and facets instead of documents. - `vectorSearch({ index, path, queryVector, numCandidates, limit, filter? })` takes the index name, the field holding the vectors, the vector to search for as an array of numbers, how many candidates to consider, how many documents to return, and an optional filter object. @@ -792,7 +791,7 @@ The builder exposes nineteen accumulators: - `firstN`, `lastN`, `maxN`, and `minN` take `{ input, n }`. `input` is the expression whose values you collect. - `top` and `bottom` take `{ output, sortBy }`. `topN` and `bottomN` take the same two plus `n`. -- `output` is the expression whose value you want back from each chosen document. `sortBy` is a plain object such as `{ createdAt: 1 }`, where `1` sorts ascending and `-1` sorts descending. +- `output` is the expression whose value you want back from each chosen document. `sortBy` is a plain object such as `{ createdAt: 1 }`, where `1` sorts ascending and `-1` sorts descending. Its keys are plain strings because they name fields, not values. - Wherever a signature takes `n`, wrap the number: write `n: fn.literal(2)`, not `n: 2`. `fn.literal` comes from the [expression helpers](#expression-helpers) below. **Put a `sort()` before the `group()` when you use `acc.first()` or `acc.last()`.** Without one, the document MongoDB calls first is arbitrary and can change between runs. @@ -801,7 +800,7 @@ The builder exposes nineteen accumulators: ##### `count()` and `sum()` -`acc.sum(fn.literal(1))` adds 1 for each document, which counts them. To add up a field instead, pass the field, as in `acc.sum(f.views)` on a collection with a numeric `views` field. +`acc.sum(fn.literal(1))` adds 1 for each document, which counts them. To add up a field instead, pass the field, as in `acc.sum(f.views)` on a collection with a numeric `views` field. The example schema's `posts` has no such field, so that call needs a collection of your own. ```typescript import { acc, fn } from '@prisma/orm-mongo/query-builder'; @@ -852,7 +851,7 @@ const [{ firstTwo, newestTitle }] = await runtime.query(plan); ## Expression helpers -Expression helpers (`fn.*`) build the computed values used inside stages like `addFields()`, `project()`, `group()` accumulators, and `match()` (via `expr()`). An expression is an `f.field` value or an `fn.*` value, never a plain JavaScript value. Stage callbacks and these helpers take expressions. The update operators on the [write methods](#write-methods) and the `MongoFieldFilter` methods take plain JavaScript values instead. Import the helpers from `@prisma/orm-mongo/query-builder`: +Expression helpers (`fn.*`) build the computed values used inside stages like `addFields()`, `project()`, `group()` accumulators, and `match()` (via `expr()`). Inside a stage, write `f.title` or `fn.toUpper(...)`, never `'hello'` or `3` on their own. Wrap a constant in `fn.literal('hello')`. The update operators on the [write methods](#write-methods) and the `MongoFieldFilter` methods work the other way round. They take ordinary JavaScript values, so write `f.bio.set('hello')`, not `f.bio.set(fn.literal('hello'))`. Import the helpers from `@prisma/orm-mongo/query-builder`: ```typescript import { fn } from '@prisma/orm-mongo/query-builder'; @@ -860,12 +859,14 @@ import { fn } from '@prisma/orm-mongo/query-builder'; Each helper is the camelCase name of the MongoDB aggregation operator without its `$` prefix, so `$toUpper` is `fn.toUpper` and `$dateToString` is `fn.dateToString`. Five helpers are named differently: -| MongoDB operator | Helper | Why | -|---|---|---| -| `$in` | `fn.isIn` | `in` is a reserved word in JavaScript. | -| `$type` | `fn.typeOf` | `typeof` is a reserved word in JavaScript. | -| `$toString` | `fn.toString_` | `toString` is already a method on every JavaScript object. | -| `$first` and `$last` | `fn.firstElem` and `fn.lastElem` | none | +| MongoDB operator | Helper | +|---|---| +| `$in` | `fn.isIn` | +| `$type` | `fn.typeOf` | +| `$toString` | `fn.toString_` | +| `$first` and `$last` | `fn.firstElem` and `fn.lastElem` | + +The first three are renamed because `in` and `typeof` are reserved words in JavaScript and `toString` is already a method on every JavaScript object. Here is every helper. Anything missing from this list has no helper, including `$and`, `$or`, `$not`, `$switch`, `$ifNull`, `$map`, `$reduce`, and `$filter`. @@ -882,13 +883,14 @@ Here is every helper. Anything missing from this list has no helper, including ` | Objects | `objectToArray`, `arrayToObject`, `getField`, `setField` | | Constants and branching | `literal`, `cond` | -`fn.literal(value)` wraps a constant so MongoDB reads it as a value and not as the name of a field. Every `fn.*` helper returns a typed wrapper, and `.node` is the raw MongoDB expression inside that wrapper. Arguments typed as an expression take the wrapper. Arguments typed as a raw MongoDB expression take `.node`, and those are the condition of `fn.cond()`, `redact()`, and the option-object stages: `bucket()`, `bucketAuto()`, `graphLookup()`, `setWindowFields()`, and `fill()`. Passing an `expr(...)` value where `.node` is expected throws a `TypeError`. This is what you pass at each call site: +`fn.literal(value)` wraps a constant so MongoDB reads it as a value and not as the name of a field. `.node` is the raw MongoDB expression inside an `fn.*` value, and a field reference such as `f.title` has one too. Passing an `expr(...)` value where `.node` is expected throws a `TypeError`. This table is the whole rule for when to write `.node`: | Call site | What you pass | |---|---| | `project()`, `addFields()`, and the arguments of an accumulator | The `fn.*` value itself. | -| The condition argument of `fn.cond()`, and `f.stage.set()`, `f.stage.replaceRoot()`, `f.stage.replaceWith()` | The `.node` property of the `fn.*` value. | -| The `groupBy`, `startWith`, and `partitionBy` options of the option-object stages | The `.node` property of the `fn.*` value. | +| The condition argument of `fn.cond()` | Its `.node` property. | +| `f.stage.set()`, `f.stage.replaceRoot()`, and `f.stage.replaceWith()`, documented under [Update operation forms](#update-operation-forms) | The `.node` property. | +| `redact()`, and the `groupBy`, `startWith`, and `partitionBy` options of `bucket()`, `bucketAuto()`, `graphLookup()`, `setWindowFields()`, and `fill()` | The `.node` property. | | `match()` | The `fn.*` value wrapped in `expr(...)`. | `expr()` is what lets a `match()` filter compare computed values. Import it beside `fn`: @@ -902,7 +904,7 @@ const plan = db.query.from('posts').match((f) => expr(fn.gt(fn.year(f.createdAt) For OR, AND, or NOT, pass `match()` a filter you build yourself instead of a callback: ```typescript -import { MongoFieldFilter, MongoOrExpr } from '@prisma/orm-mongo/query-ast/execution'; +import { MongoAndExpr, MongoFieldFilter, MongoOrExpr } from '@prisma/orm-mongo/query-ast/execution'; const kindFilter = MongoOrExpr.of([ MongoFieldFilter.eq('kind', 'tutorial'), @@ -916,11 +918,29 @@ const rows = await runtime.query(plan); #### Remarks - There is no `fn.*` helper for OR, AND, or NOT. In a `match()` filter, build the filter yourself: `MongoOrExpr.of([...])` is OR, `MongoAndExpr.of([...])` is AND, and `.not()` on any filter is NOT. The example above shows OR. -- Each entry in those lists is a `MongoFieldFilter`, which has `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`, `isNull`, and `isNotNull`. These are the same combinators the ORM client uses, described under [Combinators](/orm/reference/orm-client#combinators). +- Each entry in those lists is a `MongoFieldFilter`, which has `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`, `isNull`, and `isNotNull`. These are the same filter methods the ORM client uses, described under [Combinators](/orm/reference/orm-client#combinators). A filter you build yourself names the field as a string, as in `MongoFieldFilter.eq('kind', 'tutorial')`, and nothing checks that string against your model. +- "Not equal" has two spellings on this page. The filter method is `MongoFieldFilter.neq` and the expression helper is `fn.ne`. - Inside a computed value in `project()` or `addFields()` there is no way to write OR, AND, or NOT. Write that stage yourself with [`pipe()`](#pipe), or write the whole command with [`rawCommand()`](#rawcommand). -- Some helpers take one object of named arguments rather than positional ones: `fn.dateDiff({ startDate, endDate, unit, timezone?, startOfWeek? })`, `fn.dateTrunc({ date, unit, binSize?, timezone?, startOfWeek? })`, `fn.dateAdd({ startDate, unit, amount, timezone? })`, `fn.dateSubtract` with the same keys as `fn.dateAdd`, `fn.dateToString({ date, format })`, `fn.trim({ input })`, `fn.regexMatch({ input, regex })`, `fn.zip({ inputs, useLongestLength?, defaults? })`, `fn.getField({ field, input? })`, `fn.setField({ field, input, value })`, and `fn.convert({ input, to, onError?, onNull? })`. In `fn.convert`, `to` takes any MongoDB type name, written as an expression, such as `fn.literal('int')` or `fn.literal('objectId')`. -- `fn.slice(array, ...rest)` and `fn.range(start, end, step)` take positional arguments. So does every other helper not named above, in the order given for that operator in [MongoDB's aggregation operator reference](https://www.mongodb.com/docs/manual/reference/operator/aggregation/). Each argument is an expression from the `f` argument or a `fn.literal(...)` value. -- **`fn.toObjectId()` cannot take a `fn.literal()` value.** `fn.toObjectId(fn.literal(id))` throws. To turn a string id from your own code into an `ObjectId`, build the `ObjectId` yourself and put it in a raw pipeline with [`rawCommand()`](#rawcommand). The last example on this page does exactly that. +- `fn.slice(array, ...rest)` and `fn.range(start, end, step)` take positional arguments, and so does every helper not listed in the table below. This page does not give the positional order for those helpers. Look each one up in [MongoDB's aggregation operator reference](https://www.mongodb.com/docs/manual/reference/operator/aggregation/). Each argument is an expression from the `f` argument or a `fn.literal(...)` value. +- **`fn.toObjectId()` cannot take a `fn.literal()` value.** `fn.toObjectId(fn.literal(id))` throws a `TypeError`. There is no way to turn a string id into an `ObjectId` inside a pipeline. Build the `ObjectId` in your own code instead and put it in a raw `$match` with [`rawCommand()`](#rawcommand), as the last example on this page does. + +These helpers take one object of named arguments instead of positional ones: + +| Helper | Arguments | +|---|---| +| `fn.dateDiff` | `{ startDate, endDate, unit, timezone?, startOfWeek? }` | +| `fn.dateTrunc` | `{ date, unit, binSize?, timezone?, startOfWeek? }` | +| `fn.dateAdd` | `{ startDate, unit, amount, timezone? }` | +| `fn.dateSubtract` | The same keys as `fn.dateAdd`. | +| `fn.dateToString` | `{ date, format }` | +| `fn.trim` | `{ input }` | +| `fn.regexMatch` | `{ input, regex }` | +| `fn.zip` | `{ inputs, useLongestLength?, defaults? }` | +| `fn.getField` | `{ field, input? }` | +| `fn.setField` | `{ field, input, value }` | +| `fn.convert` | `{ input, to, onError?, onNull? }` | + +In `fn.convert`, `to` takes any MongoDB type name written as an expression, such as `fn.literal('int')` or `fn.literal('objectId')`. #### Examples @@ -936,10 +956,7 @@ const rows = await runtime.query(plan); ##### String composition -```typescript -const plan = db.query.from('posts') - .project((f) => ({ shout: fn.concat(fn.toUpper(f.title), fn.literal('!')) })).build(); -``` +`project((f) => ({ shout: fn.concat(fn.toUpper(f.title), fn.literal('!')) }))` puts the title in upper case next to a `!`. ##### Branch with `cond()` @@ -959,9 +976,9 @@ Append an arbitrary raw pipeline stage the typed methods don't cover. #### Remarks -- `pipe(stage)` appends a raw stage that you construct yourself, and keeps the row type you already had. The stages are the `Mongo*Stage` classes, one per MongoDB stage, from `@prisma/orm-mongo/query-ast/execution`. The example below uses `MongoMatchStage` and `MongoCountStage`. The same module exports `MongoFieldFilter`, which builds the filter a `MongoMatchStage` takes. +- `pipe(stage)` appends a raw stage that you construct yourself, and keeps the row type you already had. The stages are the `Mongo*Stage` classes, one per MongoDB stage, from `@prisma/orm-mongo/query-ast/execution`. The class name is `Mongo`, then the stage name without its `$`, then `Stage`, so `$unwind` is `MongoUnwindStage`. The example below uses `MongoMatchStage` and `MongoCountStage`. The same module exports `MongoFieldFilter`, which builds the filter a `MongoMatchStage` takes. - `pipe(stage)` declares the row type the rows have after that stage. `NewShape` is a type you write yourself, as at the end of the example below. -- After a `pipe()` stage the rows come back exactly as MongoDB returns them, whatever the TypeScript type claims. A date is a `Date` and an id is an `ObjectId` from the `mongodb` package. +- After a `pipe()` stage the TypeScript type no longer describes the rows. They come back exactly as MongoDB returns them, so a date is a `Date` and an id is an `ObjectId` from the `mongodb` package, the driver Prisma ORM runs your queries through. - Prefer a typed stage where one exists. `pipe()` adds one raw stage to a typed chain, and [`rawCommand()`](#rawcommand) replaces the whole command. #### Examples @@ -993,13 +1010,13 @@ Compile the pipeline into a query you can run. ## Write methods -The pipeline builder can write, not just read. Write methods are available at three points: on the root collection, after a `match()` filter, and as the last call in a pipeline. A write method returns the built query on its own, so there is no `build()` call after one. The examples below name that built query `plan`. +The pipeline builder can write, not just read. Write methods are available at three points: on the root collection, after a `match()` filter, and at the end of any pipeline, where `out()` and `merge()` write the output into a collection. A write method returns the built query on its own, so there is no `build()` call after one. The examples below name that built query `plan`. Updater callbacks (the `(f) => [...]` argument to the update methods) must return an **array** of operations. This is a firm rule: - Always return an array. Returning a bare operation, `f.bio.set('x')` rather than `[f.bio.set('x')]`, throws as soon as you call the write method. - An empty array throws an error whose `code` is `ORM.MUTATION_DATA_MISSING`: `Updater returned no operations. Return at least one update from the callback ...`. -- An updater is either all operator form or all pipeline form. Operator form calls an operator on a field, as in `f.bio.set(value)`. Pipeline form calls `f.stage.*`, and can read other fields of the same document. Type `f.stage` exactly: `stage` is not a placeholder for a stage name. You cannot mix the two in one updater. TypeScript rejects the mixed array, and at run time it throws an error whose `code` is `ORM.ARGUMENT_INVALID`: `Cannot mix ...`. +- An updater is either all operator form or all pipeline form. Operator form calls an operator on a field, as in `f.bio.set(value)`. Pipeline form can read other fields of the same document, and it calls `f.stage.*`. Type `f.stage` exactly: `stage` is not a placeholder for a stage name. You cannot mix the two in one updater. TypeScript rejects the mixed array, and at run time it throws an error whose `code` is `ORM.ARGUMENT_INVALID`: `Cannot mix ...`. - Applying the same operator to the same field twice in one updater throws an error whose `code` is `ORM.ARGUMENT_INVALID`: `Update spec collision: ...`. ### Root-level writes @@ -1010,10 +1027,10 @@ Updater callbacks (the `(f) => [...]` argument to the update methods) must retur - These return a built query. Run it with `runtime.query(...)`. - Write results are result objects, not documents: `insertOne` gives `{ insertedId }`, `insertMany` gives `{ insertedIds, insertedCount }`, the update methods and `upsertOne` give `{ matchedCount, modifiedCount, upsertedCount, upsertedId }`, and the delete methods give `{ deletedCount }`. A write returns one result object, as the single row of the result, which is why every example here reads it with `const [result] = ...`. -- The document you pass to `insertOne` / `insertMany` is a plain record. TypeScript requires no particular field, and the builder does not check the record against your contract. +- The document you pass to `insertOne` / `insertMany` is a plain record. TypeScript requires no particular field, and the builder does not check the record against your contract. You can leave nullable fields out. The examples below pass `null` for them to make the document shape explicit. - You can leave `_id` out and let MongoDB assign one. - **On the way in, values are stored exactly as you pass them, so pass a `Date` for a date field and an `ObjectId` from the `mongodb` package for an id field.** Nothing turns a string into either. This is the write direction only. Reads are decoded as the first half of this page describes. -- [`match()`](#match) filter values follow the same rule as the values you insert. The one value you cannot filter on this way is `_id`, which needs [`rawCommand()`](#rawcommand). The last example on this page shows it. +- [`match()`](#match) filter values follow the same rule as the values you insert. The one value you cannot filter on this way is `_id`. An `_id` filter written in `match()` never matches anything, so use [`rawCommand()`](#rawcommand) instead. The [`match()`](#match) warning gives the detail, and the last example on this page shows the form that works. - `insertMany([])` throws an error whose `code` is `ORM.MUTATION_DATA_MISSING`. #### Examples @@ -1036,18 +1053,11 @@ const [insertManyResult] = await runtime.query(manyPlan); ##### `updateAll()` (array-of-ops updater) -```typescript -const plan = db.query.from('users').updateAll((f) => [f.bio.set('everyone now has bio')]); -await runtime.query(plan); -``` +`db.query.from('users').updateAll((f) => [f.bio.set('everyone now has bio')])` builds the update. Pass it to `runtime.query(...)` to run it. ##### `deleteAll()` -```typescript -const plan = db.query.from('users').deleteAll(); -const [result] = await runtime.query(plan); -// { deletedCount: 2 } -``` +`db.query.from('users').deleteAll()` builds a delete for every document. Pass it to `runtime.query(...)` and read `deletedCount` from the single result object. ##### `upsertOne()` on the root (filter, then updater) @@ -1068,8 +1078,8 @@ After a `match()` filter, `updateMany`, `updateOne`, `deleteMany`, `deleteOne`, - The `match()` filter supplies the write's filter, so you do not repeat it. - After `match()`, `upsertOne()` takes the updater callback only (the filter comes from `match()`). -- `updateOne` / `deleteOne` affect at most one matching document. `updateMany` / `deleteMany` affect all matches. Which document `updateOne` / `deleteOne` picks is up to the driver. Use `findOneAndUpdate()` after a `sort()` when the choice matters. -- `findOneAndUpdate()` and `findOneAndDelete()` return the document exactly as the driver gives it, so its `_id` is an `ObjectId` from the `mongodb` package even though the TypeScript type says `string`. To get the id as a string, write `String(result._id)`. +- `updateOne` / `deleteOne` affect at most one matching document. `updateMany` / `deleteMany` affect all matches. When it matters which document you change, put a `sort()` between the `match()` and the write and use `findOneAndUpdate()`, as in `.match(...).sort(...).findOneAndUpdate(...)`. +- `findOneAndUpdate()` and `findOneAndDelete()` return the document exactly as the `mongodb` driver gives it, so its `_id` is an `ObjectId` from the `mongodb` package even though the TypeScript type says `string`. To get the id as a string, write `String(result._id)`. - You cannot call `findOneAndUpdate()` or `findOneAndDelete()` after `skip()`, and TypeScript does not offer them there. If a cast gets one past TypeScript, it throws an error whose `code` is `ORM.OPERATION_UNSUPPORTED`. #### Examples @@ -1097,12 +1107,7 @@ const [result] = await runtime.query(plan); ##### `upsertOne()` after `match()` (updater only) -```typescript -const plan = db.query.from('users').match((f) => f.email.eq('alice@example.com')) - .upsertOne((f) => [f.bio.set('upserted via match')]); -const [result] = await runtime.query(plan); -// updates on a hit: result.modifiedCount is 1 -``` +`db.query.from('users').match((f) => f.email.eq('alice@example.com')).upsertOne((f) => [f.bio.set('upserted via match')])` builds the upsert. On a hit, `result.modifiedCount` is 1. ##### `findOneAndUpdate()` and `returnDocument` @@ -1121,12 +1126,7 @@ Use `returnDocument: 'before' | 'after'`. `returnNewDocument` is not a valid opt ##### `findOneAndDelete()` -```typescript -const plan = db.query.from('users').match((f) => f.email.eq('alice@example.com')) - .findOneAndDelete(); -const [deleted] = await runtime.query(plan); -// deleted is the removed document -``` +`db.query.from('users').match((f) => f.email.eq('alice@example.com')).findOneAndDelete()` builds the delete. Its single result row is the removed document. ### Update operation forms @@ -1135,7 +1135,7 @@ Inside an updater callback, each operation targets a field. There are two mutual #### Remarks - **Operator form**: call an operator on the `f` argument, one operator per field. There are fourteen, in the table below. -- **Pipeline form**: aggregation-pipeline update stages through `f.stage.*`, for example `f.stage.set({ bio: f.name.node })`. The four stages are `set`, `unset`, `replaceRoot`, and `replaceWith`. Pipeline-form operations let an update read other fields of the same document. +- **Pipeline form**: aggregation-pipeline update stages through `f.stage.*`, for example `f.stage.set({ bio: f.name.node })`. The four stages are `set`, `unset`, `replaceRoot`, and `replaceWith`. - The two forms cannot be mixed in a single updater (see the write-methods intro). An updater is entirely operator form or entirely pipeline form. Operator form takes these arguments: @@ -1155,7 +1155,7 @@ Operator form takes these arguments: | `pull(value)` | The value to remove from an array field. | | `pullAll(values)` | An array of values to remove. | | `currentDate()` | Nothing. Writes the current date. | -| `setOnInsert(value)` | A value written only when an upsert inserts a new document. | +| `setOnInsert(value)` | A value written only when an upsert inserts a new document, so it does nothing outside `upsertOne()`. | #### Examples @@ -1165,7 +1165,7 @@ Operator form takes these arguments: const plan = db.query.from('users').match((f) => f.role.eq('author')) .updateMany((f) => [f.bio.set('operator form')]); await runtime.query(plan); -// on a collection with a numeric views field and an array tags field: +// on a collection with a numeric views field and an array tags field (posts has neither): // updateMany((f) => [f.views.inc(1), f.tags.push('mongodb')]) ``` @@ -1186,18 +1186,14 @@ await runtime.query(plan); - `out(collection)` writes the pipeline output into a destination collection, replacing its contents. Pass a second argument, the database name as a string, to write into a different database: `out('users_snapshot', 'archive')`. - `merge({ into })` writes the pipeline output into a target collection, merging with existing documents. `into` is a collection name or `{ db, coll }`. The options object also takes `on`, which is one field name as a string or several as an array of strings, plus `whenMatched` and `whenNotMatched`. -- `whenMatched` takes `'replace'`, `'keepExisting'`, `'merge'`, or `'fail'`, or an array of update stages, which are `MongoAddFieldsStage`, `MongoProjectStage`, and `MongoReplaceRootStage` objects from `@prisma/orm-mongo/query-ast/execution`. `whenNotMatched` takes `'insert'`, `'discard'`, or `'fail'`. A misspelled value fails at the database rather than in TypeScript. +- `whenMatched` takes `'replace'`, `'keepExisting'`, `'merge'`, or `'fail'`, or an array of update stages, which are `MongoAddFieldsStage`, `MongoProjectStage`, and `MongoReplaceRootStage` objects from `@prisma/orm-mongo/query-ast/execution`. `MongoAddFieldsStage` takes one object of field names and expressions, as in `new MongoAddFieldsStage({ ... })`. `whenNotMatched` takes `'insert'`, `'discard'`, or `'fail'`. A misspelled value fails at the database rather than in TypeScript. - Both end the chain. They return a built query you run with `runtime.query(...)`, and that call returns an empty array, because the output goes to the destination collection and not to your program. #### Examples ##### Write the output with `out()` -```typescript -const plan = db.query.from('users').out('users_snapshot'); -await runtime.query(plan); -// the users_snapshot collection now holds the pipeline output -``` +`db.query.from('users').out('users_snapshot')` builds the write. After you pass it to `runtime.query(...)`, the `users_snapshot` collection holds the pipeline output. ##### Merge with `merge()` diff --git a/apps/docs/content/docs/orm/reference/raw-queries.mdx b/apps/docs/content/docs/orm/reference/raw-queries.mdx index 602a2fba30b..9128971dde2 100644 --- a/apps/docs/content/docs/orm/reference/raw-queries.mdx +++ b/apps/docs/content/docs/orm/reference/raw-queries.mdx @@ -16,10 +16,10 @@ Raw SQL comes in two forms. A **raw fragment** is a piece of SQL you put inside a builder query. Write it as a tagged template with `fns.raw` and pass it to a `select()`, `where()`, `orderBy()`, or `update()` call. The rest of the query stays typed. -A **whole raw statement** is a complete SQL statement written with `db.raw.sql`. If it returns rows, end it with `.returnsRow(spec).build()` and pass the result to `runtime.query(...)`. If it does not, end it with `.affectedCount().build()` and pass the result to `runtime.execute(...)`. +A **whole raw statement** is a complete SQL statement written with `db.raw.sql`. If it returns rows, end it with `.returnsRow(spec).build()`, where the spec is a column list [defined below](#the-client-level-dbrawsql-tag), and pass the result to `runtime.query(...)`. If it does not, end it with `.affectedCount().build()` and pass the result to `runtime.execute(...)`. :::warning[Only `.returnsRow()` converts values for you] -A type id such as `pg/text@1` tells Prisma ORM what JavaScript type a column should become. In code, the property is called `codecId`. `.returnsRow(spec)` names a type id for every column of a whole `db.raw.sql` statement, so you get real JavaScript values back. A fragment's `.returns(...)` does not convert anything. It only tells TypeScript what type to expect, so the value is whatever the `pg` package, the PostgreSQL driver underneath, gives you. For a `text` or an `integer` column that is already a JavaScript string or number. A `numeric` column arrives as a string. A date column arrives as whatever the `pg` package produces for it, with no conversion by Prisma ORM. +A type id such as `pg/text@1` tells Prisma ORM what JavaScript type a column should become. The property is named `codecId`. `.returnsRow(spec)` names a type id for every column of a whole `db.raw.sql` statement, so you get real JavaScript values back. A fragment's `.returns(...)` does not convert anything. It only tells TypeScript what type to expect, so the value is whatever the `pg` package, the PostgreSQL driver underneath, gives you. For a `text` or an `integer` column that is already a JavaScript string or number. A `numeric` column arrives as a string. A date column gets no conversion by Prisma ORM, so you get the `pg` package's own value for that column. ::: The examples in this section run against the `user` / `post` schema from the SQL query builder page. See its [example schema](/orm/reference/sql-query-builder#example-schema) for the full model definitions. As on that page, create the client with `postgres(...)` and reach tables through `db.sql.public`, which is keyed by table name (the model name with a lowercase first letter unless the model sets the `@@map` attribute, which renames a table). `db.runtime()` gives you the connection that runs a built query. On PostgreSQL it gives you that connection directly. On MongoDB it returns a promise, so there you write `const runtime = await db.runtime();`. @@ -35,7 +35,7 @@ const aliceId = '00000000-0000-4000-8000-000000000001'; ### `fns.raw` in a projection -A `select()` callback receives two arguments: `f`, with one property per column, and `fns`, with the expression helpers. `fns.raw` is a tagged template. Write a SQL fragment, interpolate columns and values with `${...}`, and declare the fragment's result type with `.returns(typeId)`. See [`select()`](/orm/reference/sql-query-builder#select) for every form it accepts. +A projection is the `select()` list. A `select()` callback receives two arguments: `f`, with one property per column, and `fns`, with the expression helpers. `fns.raw` is a tagged template. Write a SQL fragment, interpolate columns and values with `${...}`, and declare the fragment's result type with `.returns(typeId)`. See [`select()`](/orm/reference/sql-query-builder#select) for every form it accepts. ```ts const plan = db.sql.public.user @@ -64,7 +64,7 @@ const rows = await runtime.query(plan); ### Interpolating a typed expression -You can interpolate another expression, such as a comparison, a column reference, or another raw fragment. Interpolated values are always sent as query parameters, never pasted into the SQL. Never put user input into the SQL text itself, because a column or table name cannot be a parameter. When the column is chosen at run time, pick it in your own code from a fixed set of columns you control, and choose the matching `fns.raw` fragment or `select()` call for each one. +You can interpolate another expression, such as a comparison, a column reference, or another raw fragment. Interpolated values are sent as query parameters, so they are safe. Column and table names cannot be parameters, so they would be pasted into the SQL text. Never build an identifier from user input. When the column is chosen at run time, pick it in your own code from a fixed set of columns you control, and choose the matching `fns.raw` fragment or `select()` call for each one. ```ts const plan = db.sql.public.user @@ -91,7 +91,7 @@ A bare value that you do interpolate directly gets the type id Prisma ORM picks | a `boolean` | `pg/bool@1` | | a `Uint8Array` | `pg/bytea@1` | -A type id is the PostgreSQL type name plus a version, always `@1` today. Four more you are likely to write are `pg/uuid@1`, `pg/numeric@1`, `pg/jsonb@1`, and `pg/timestamptz-temporal@1`. Those are the common ones, not the whole set. The ids your contract uses are in `contract.json`. For any column, `db.sql.public.
.columns..codecId` is its id. A point in time is the case `param()` exists for, and `pg/timestamptz-temporal@1` takes a `Temporal.Instant`, the standard JavaScript object for a point in time, not a `Date`. +A type id is `pg/`, a name, and `@1`. The name is usually the PostgreSQL type, and a few carry a suffix that says which JavaScript type you get, such as `pg/int8number@1` for a `number` and `pg/timestamptz-temporal@1` for a `Temporal.Instant`. Between those two, pick `pg/int8@1` when the value can exceed 2^53. Three more you are likely to write are `pg/uuid@1`, `pg/numeric@1`, and `pg/jsonb@1`. Those are the common ones, not the whole set. For any column, `db.sql.public.
.columns..codecId` is its id, and the same ids are in `contract.json` under `storage.namespaces..entries.table.
.columns..codecId`. A point in time is the case `param()` exists for, and `pg/timestamptz-temporal@1` takes a `Temporal.Instant`, the standard JavaScript object for a point in time, not a `Date`. `Temporal` is a global in Node.js 26 and later, and on earlier versions you install `temporal-polyfill` and add `import 'temporal-polyfill/full/global'`. ```ts import { param } from '@prisma/orm-postgres/relational-core/expression'; @@ -133,7 +133,7 @@ const plan = db.sql.public.user const rows = await runtime.query(plan); ``` -Use `.returnsRow(spec)` to run a whole statement and get converted rows back. The spec names the columns of every returned row. Each entry is either a column from your contract or a type id. A column such as `db.sql.public.user.columns.id` brings its own conversion, nullability, and TypeScript type. Write a type id for a column your contract has no match for. +Use `.returnsRow(spec)` to run a whole statement and get converted rows back. The spec names the columns of every returned row. Each entry is either a column from your contract or a type id. A column such as `db.sql.public.user.columns.id` brings its own conversion, nullability, and TypeScript type. Write a type id for a column your contract has no match for, and the object form `{ codecId: 'pg/text@1', nullable: true }` when that column can be `NULL`, as a `LEFT JOIN` column can. If a returned row has no column your spec names, the query throws an error whose `code` is `RUNTIME.RAW_ROW_COLUMN_MISSING`. A failing statement throws a `SqlQueryError` whose `sqlState` is the PostgreSQL error code for the failure, such as `23505` for a unique-constraint violation. ```ts const user = db.sql.public.user; @@ -167,7 +167,11 @@ const stats = await runtime.execute(plan); // stats.affectedRows ``` -You can nest a `.returnsRow()` query inside another. That is how you write a subquery or a CTE. Interpolate it before `.build()`, and call `.build()` on the outer one only. Its `.returns` property holds its declared columns, so `outer.returnsRow({ postCount: inner.returns.postCount })` reuses a declaration. Three names look alike. `.returns(typeId)` is a method on a fragment, `.returnsRow(spec)` is a method on a statement, and `.returns` is a property on the object `.returnsRow()` gives you. You cannot nest a statement that ends with `.affectedCount()`, because it produces no rows. You can nest an `UPDATE ... RETURNING`, because it does. +You can nest a `.returnsRow()` query inside another. That is how you write a subquery or a CTE, which is a `WITH` query. Interpolate it before `.build()`, and call `.build()` on the outer one only. Its `.returns` property holds its declared columns, so `outer.returnsRow({ postCount: inner.returns.postCount })` reuses a declaration. You cannot nest a statement that ends with `.affectedCount()`, because it produces no rows. You can nest an `UPDATE ... RETURNING`, because it does. + +:::note[Three names look alike] +`.returns(typeId)` is a method on a fragment, `.returnsRow(spec)` is a method on a statement, and `.returns` is a property on the object `.returnsRow()` gives you. +::: ```ts const authorsWithPosts = db.raw.sql` @@ -198,12 +202,12 @@ To bind any value whose type is not in the list, wrap it in [`param(value, { cod ## MongoDB raw commands -`db.raw.collection(collectionName)` returns an object with nine methods for running MongoDB commands directly against a collection. The collection name is the one your contract declares (`'users'`, `'posts'`), the same name the ORM client uses (`db.orm.users`). MongoDB has no schemas, so there is no `public` segment here and none on `db.orm`. +`db.raw.collection(collectionName)` returns an object with nine methods for running MongoDB commands directly against a collection. The collection name is the one your contract declares (`'users'`, `'posts'`), the same name the ORM client uses (`db.orm.users`). MongoDB has no schemas, so there is no `public` segment here and none on `db.orm`. There is no raw `find()`, so use `aggregate()` with a `$match` stage. MongoDB has no transactions in Prisma ORM, so a raw command cannot run inside one. See [Transactions (MongoDB)](/orm/reference/transactions-and-runtime#transactions-mongodb). -Each of these nine methods builds a command. Call `.build()` on it, then run it with `runtime.query(built)`, the inserts, updates, and deletes included. A write returns a one-element array, so destructure it with `const [result] = ...`. +Each of these nine methods builds a command. Call `.build()` on it, then run every one of them with `runtime.query(...)`, including the writes. The raw collection methods all run through `query()`. A write returns a one-element array, so destructure it with `const [result] = ...`. :::warning[MongoDB raw results are native BSON values] -Nothing on the MongoDB side of this page is converted for you. An `_id` comes back as an `ObjectId` instance from the `mongodb` package, the MongoDB driver underneath, not a hex string. Compare one with `String(row._id) === aliceId`. Raw filters take native BSON values too, so build ids with that package's `ObjectId` class. The write methods give you its own result objects, `{ insertedId }`, `{ matchedCount, modifiedCount, upsertedCount, upsertedId }`, and `{ deletedCount }`, and you read those keys directly. +Nothing on the MongoDB side of this page is converted for you. An `_id` comes back as an `ObjectId` instance from the `mongodb` package, the MongoDB driver underneath, not a hex string. Compare one with `String(row._id) === aliceId`. Raw filters take native BSON values too, so build ids with that package's `ObjectId` class. The write methods give you the `mongodb` package's own result objects, `{ insertedId }`, `{ matchedCount, modifiedCount, upsertedCount, upsertedId }`, and `{ deletedCount }`, and you read those keys directly. ::: The examples in this section run against the `users` / `posts` schema from the pipeline builder page. See its [example schema](/orm/reference/pipeline-builder#example-schema) for the full model definitions. Create the client with `mongo(...)`, get the connection that runs a built query from `await db.runtime()`, and declare the two example ids once: @@ -286,7 +290,7 @@ Atomically update a matching document and return the document as it was before t #### Remarks -- `findOneAndUpdate()` accepts only `{ upsert }` as its third argument. It cannot sort, and it cannot give you the document as it is after the update. TypeScript rejects `sort` or `returnDocument` in the options object. If you get past the type check, say by casting the object to `any`, those keys are ignored at run time with no error. +- `findOneAndUpdate()` accepts only `{ upsert }` as its third argument. It cannot sort, and it cannot give you the document as it is after the update. TypeScript rejects `sort` or `returnDocument` in the options object. To sort, or to read the document after the update, build the command yourself and run it through `db.query.rawCommand(...)`. `new RawFindOneAndUpdateCommand(...)` takes six arguments: @@ -307,7 +311,7 @@ const [updated] = await runtime.query(db.query.rawCommand(command)); ##### Upsert counter pattern -The **first** call to an upsert counter returns an **empty** result, because there is no earlier version. Read the counter value on the second call and later ones, each of which returns the version before its own increment. The example schema has no counter collection, so `users` stands in for one here, with the plain string `'pageViews'` as its `_id`. +`findOneAndUpdate()` returns the document as it was **before** your update. The first call creates the document, so there is nothing to return and you get `[]`. The second call returns `count: 1`, which is the value before that call added one. The example schema has no counter collection, so `users` stands in for one here. MongoDB allows a plain string `_id`, so `'pageViews'` is a valid id. The advice to build ids with `ObjectId` applies to ids MongoDB generated. ```ts const filter = { _id: 'pageViews' }; @@ -332,7 +336,7 @@ const isAlice = String(deleted._id) === aliceId; // true ## `db.query.rawCommand()` -`db.query.rawCommand(command)` runs a raw MongoDB aggregate command through the [pipeline builder](/orm/reference/pipeline-builder), with no translation of the command you pass. It returns a built query already, so do not call `.build()` on it. Unlike `aggregate()` it takes no type parameter, so its rows come back as `unknown` and you cast them yourself. +`db.query.rawCommand(command)` runs a raw MongoDB aggregate command, sent exactly as you wrote it. See the [pipeline builder](/orm/reference/pipeline-builder). It returns a built query already, so do not call `.build()` on it. Unlike `aggregate()` it takes no type parameter, so its rows come back as `unknown` and you cast them yourself. ```ts import { RawAggregateCommand } from '@prisma/orm-mongo/query-ast/execution'; @@ -342,13 +346,9 @@ const rows = (await runtime.query(plan)) as { total: number }[]; // [{ total: 2 }] ``` -`rawCommand()` is the way out when the typed pipeline builder can't express a query, such as filtering by `_id` equality. The command you pass is sent unchanged, so a real `ObjectId` in a pipeline document matches. `aggregate()` on `db.raw.collection()` handles `_id` filtering too, and it gives you a row type. Use `rawCommand()` when you are already working in `db.query` and need one raw command there. +`rawCommand()` is the way out when the typed pipeline builder can't express a query, such as filtering by `_id` equality. The command you pass is sent unchanged, so a real `ObjectId` in a pipeline document matches. `aggregate()` on `db.raw.collection()` handles `_id` filtering too, and it gives you a row type. Use `rawCommand()` when you are already working in `db.query` and need one raw command there. For the full treatment see the pipeline builder's [`rawCommand()`](/orm/reference/pipeline-builder#rawcommand) section. ```ts -const plan = db.query.rawCommand( - new RawAggregateCommand('posts', [{ $match: { _id: new ObjectId('6539a1f2c3d4e5f60718293c') } }]), -); +const plan = db.query.rawCommand(new RawAggregateCommand('posts', [{ $match: { _id: new ObjectId('6539a1f2c3d4e5f60718293c') } }])); const rows = await runtime.query(plan); ``` - -For the full treatment see the pipeline builder's [`rawCommand()`](/orm/reference/pipeline-builder#rawcommand) section. 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 a3c30e36712..4c895e87322 100644 --- a/apps/docs/content/docs/orm/reference/sql-query-builder.mdx +++ b/apps/docs/content/docs/orm/reference/sql-query-builder.mdx @@ -8,15 +8,15 @@ metaDescription: 'Reference for the Prisma ORM SQL query builder''s select, muta The SQL query builder builds typed queries from a table, with one method per SQL clause: `select()`, `innerJoin()`, `groupBy()`, and the rest. It is closer to SQL than the [ORM client](/orm/reference/orm-client) is, and the same `db` gives you both. Reach for it when you need a join, aggregate, or SQL feature the ORM client doesn't expose, or when you want direct control over the generated SQL. -This page documents every method, its options, and its return type. Some methods work only on some databases, and some have a caveat that shows up only when the query runs. Each method's **Remarks** section says so. For a task-oriented guide to when and how to reach for the builder, see [Advanced queries](/orm/fundamentals/advanced-queries). +This page documents every method, its options, and its return type. Some methods work only on some databases, and some fail only when the query runs. Each method's **Remarks** section says so. For a task-oriented guide to when and how to reach for the builder, see [Advanced queries](/orm/fundamentals/advanced-queries). -:::note[This is the SQL-family builder] +:::note[The builder for SQL databases] The SQL query builder targets SQL databases; PostgreSQL and SQLite are supported today. MongoDB has no SQL builder: use the [ORM client](/orm/reference/orm-client) for MongoDB queries, and the [pipeline builder](/orm/reference/pipeline-builder) for aggregation pipelines. ::: ## Example schema -All examples on this page run against the following schema. The select and mutation examples use the `user`, `post`, `post_tag`, and `tag` tables. The grouped-query examples use a separate `customer` and `order` schema, shown under [Grouped queries](#grouped-queries). No example uses `Address`, `Task`, `Bug`, or `Feature`. No example on this page uses `types`, `@@type`, `@@discriminator`, or `@@base`. See [Contract authoring](/orm/contract-authoring/psl-syntax). +All examples on this page run against the schema below, which is the full example schema shared across the reference pages. This page uses only the `user`, `post`, `post_tag`, and `tag` tables. The grouped-query examples use a separate `customer` and `order` schema, shown under [Grouped queries](#grouped-queries). `PostTag` is the join table. The two list fields and the `PostTag` model together are how Prisma ORM 8 declares a many-to-many, which [Relations and joins](/orm/fundamentals/relations-and-joins#many-to-many) explains. For the schema syntax itself, see [Contract authoring](/orm/contract-authoring/psl-syntax).
@@ -128,13 +128,15 @@ model Feature { ## Entry points -In Prisma ORM 8 your `schema.prisma` is called `contract.prisma`. [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7) gets you from a Prisma ORM 7 project to that file, and has the rest of the setup steps. Run `npx prisma contract emit` to generate `contract.json` and `contract.d.ts` beside it. The examples below import both. Install `@prisma/orm-postgres`. +In Prisma ORM 8 your `schema.prisma` is called `contract.prisma`. [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7) gets you from a Prisma ORM 7 project to that file, and has the rest of the setup steps. Install the client with `npm install @prisma/orm-postgres`. Run `npx prisma contract emit` to write `contract.json` and `contract.d.ts` beside `contract.prisma`. The examples below assume your own file is in `src/prisma/`, next to those two files, and they write the type import path as `./contract.d`, exactly like that. -You build queries from the client's `sql` property. Create a Postgres client with `postgres(...)`, then reach tables through `db.sql.public`. `public` is the PostgreSQL schema your tables are in, and Prisma ORM 8 puts every table in `public` today. Table accessors are keyed by table name, not model name. A table name is the model name with a lowercase first letter unless the model sets `@@map`, so the `User` model mapped to `user` is reached as `db.sql.public.user`, and the model for the `Post`/`Tag` join table, mapped to `post_tag`, is reached as `db.sql.public.post_tag`. Column names are the field names unless a field sets `@map`. +You build queries from the client's `sql` property. Create a Postgres client with `postgres(...)`, then reach tables through `db.sql.public`. `public` is the PostgreSQL schema your tables are in. Prisma ORM 8 puts every table in `public`. Table accessors are keyed by table name, not model name. A table name is the model name with a lowercase first letter unless the model sets `@@map`, so the `User` model mapped to `user` is reached as `db.sql.public.user`, and the model for the `Post`/`Tag` join table, mapped to `post_tag`, is reached as `db.sql.public.post_tag`. Column names are the field names unless a field sets `@map`. ### The `db.sql` builder [#the-dbsql-facet] -`db.runtime()` returns the connection that runs a built query. `db.runtime()` is synchronous and returns immediately. Only `runtime.query()` is awaited. Hold the connection in a variable and reuse it. Write the type import path as `./contract.d`, exactly like that. A `select()`, `insert()`, `update()`, or `delete()` call starts a query. Finish it with [`build()`](#build), which returns the built query, and pass that to `runtime.query(...)`. The examples name the built query `plan`. You can run the same built query more than once. +`db.runtime()` returns the connection that runs a built query. Hold it in a variable and reuse it. When your app shuts down, close the client with [`db.close()`](/orm/reference/transactions-and-runtime#close). + +A `select()`, `insert()`, `update()`, or `delete()` call starts a query. [`build()`](#build) turns the chain into a query object, which the examples name `plan`. Nothing touches the database until you pass that object to `runtime.query(...)`. You can run the same built query more than once. ```ts import postgres from '@prisma/orm-postgres/runtime'; @@ -161,19 +163,15 @@ db.sql.public.tag.insert([{ label: 'typescript' }]); // start an INSERT ### Aliasing a query with `.as()` -Call `.as(alias)` on a finished `SELECT` query to use that query as a subquery. The aliased query is then something you can pass to `innerJoin()`, `outerLeftJoin()`, and the other join methods. For the example, see [Subquery via `.as()`](#subquery-via-as). - -:::warning[`.as()` is not for lateral joins] -Use `.as()` only for subqueries you pass to `innerJoin()`, `outerLeftJoin()`, `outerRightJoin()`, or `outerFullJoin()`. Inside a [`lateralJoin()`](#lateraljoin) callback, return the query chain itself and do not call `.as()` on it. See that method's Remarks. -::: +Call `.as(alias)` on a `SELECT` chain before `build()` to use that query as a subquery. The aliased query is then something you can pass to `innerJoin()`, `outerLeftJoin()`, and the other join methods. Do not call `.as()` inside a [`lateralJoin()`](#lateraljoin) callback. For the example, see [Subquery via `.as()`](#subquery-via-as). ## SELECT queries -These methods build and refine a `SELECT`. They chain, and you resolve the query with [`build()`](#build) followed by `runtime.query(...)`. Chain the methods in any order, except that a join must come before any call that names the joined table's columns, and `distinctOn()` needs its columns first in `orderBy()`. `SelectQuery` in the tables below is the type they return. +These methods build and refine a `SELECT`. They chain, and you resolve the query with [`build()`](#build) followed by `runtime.query(...)`. Chain the methods in any order, with two exceptions. A join must come before any call that names the joined table's columns. The columns you pass to `distinctOn()` must be the first sort keys in `orderBy()`. `SelectQuery` in the tables below is the type they return. The set of column names a query can use is its scope. It starts as the columns of the table you began with, and every join adds the joined table's columns to it. -The `where()`, `select()`, `orderBy()`, `update()`, and other callback forms receive two arguments. The first is `f`, which has one property per column in scope. After a join, a column name that both tables have is on `f` only under its table name, such as `f.post.id`. The second is `fns`, an object of [expression helpers](#expressions-and-functions) such as `fns.eq()` and `fns.raw`. For an enum column you pass the value stored in the database, the string after `=` in the enum, so `High = "high"` is matched by `fns.eq(f.priority, 'high')`. +The `where()`, `select()`, `orderBy()`, `update()`, and other callback forms receive two arguments. The first is `f`, which has one property per column in scope. After a join, every column is on `f` under its table name, such as `f.user.email` or `f.post.id`. A column name that only one table has is also on `f` directly. The second is `fns`, an object of [expression helpers](#expressions-and-functions) such as `fns.eq()` and `fns.raw`. For an enum column you pass the value stored in the database. That is the string after `=`, so `High = "high"` is matched by `fns.eq(f.priority, 'high')`. A member with no `=`, such as `admin` in `user_type`, is matched by its own name, `fns.eq(f.kind, 'admin')`. ### `select()` @@ -191,7 +189,7 @@ Reduce each row to a chosen set of columns or computed expressions. Every form adds to what the query already returns. Calling `select()` a second time never replaces the first call's columns, whichever form you use. To return fewer columns, start a new query instead. -A computed expression's result type comes from `.returns(...)` on `fns.raw`, or from the `fns.*` function you called. Every `fns.raw` fragment needs `.returns()`. A type id is `pg/` plus the PostgreSQL type name plus `@1`, such as `pg/int4@1` or `pg/uuid@1`. A few use a longer name, such as `pg/timestamptz-temporal@1` for `DateTime`. The common ones are `pg/text@1`, `pg/int4@1`, `pg/int8@1`, `pg/float8@1`, `pg/bool@1`, `pg/uuid@1`, and `pg/timestamptz-temporal@1`. To reuse a column's own type, pass the column to `.returns()`, as in `.returns(db.sql.public.user.columns.id)`. `db.sql.public.user.columns.id.codecId` holds the id string. See [`fns.raw` and `.returns()`](#fnsraw-and-returns). +A computed expression's result type comes from `.returns(...)` on `fns.raw`, or from the `fns.*` function you called. Every `fns.raw` fragment needs `.returns()`. A type id is `pg/` plus the PostgreSQL type name plus a version, always `@1` today, such as `pg/int4@1` or `pg/uuid@1`. A few use a longer name, such as `pg/timestamptz-temporal@1` for `DateTime`. The common ones are `pg/text@1`, `pg/int4@1`, `pg/int8@1`, `pg/float8@1`, `pg/bool@1`, `pg/uuid@1`, and `pg/timestamptz-temporal@1`. The full list is what your contract uses. Read any column's id from `db.sql.public.
.columns..codecId`, and see [Binding a bare value with `param()`](/orm/reference/raw-queries#binding-a-bare-value-with-param) for which JavaScript type becomes which id. See [`fns.raw` and `.returns()`](#fnsraw-and-returns). #### Return type @@ -280,7 +278,7 @@ const rows = await runtime.query(plan); ### `innerJoin()` -Combine matching rows from two tables. After a join, a column name that both tables have must be written under its table, such as `f.post.id` or `f.user.id`. A name only one table has stays on `f` directly. +Combine matching rows from two tables. Every column is then on `f` under its table name, such as `f.user.email` or `f.post.id`. A column name that only one table has is also on `f` directly. #### Options @@ -311,11 +309,7 @@ const rows = await runtime.query(plan); ### `outerLeftJoin()`, `outerRightJoin()`, `outerFullJoin()` -Keep unmatched rows from one or both sides, filling the missing side's columns with `null`. Each takes the same `(other, on)` arguments and returns a `SelectQuery` as [`innerJoin()`](#innerjoin) does. - -- `outerLeftJoin()` is SQL's `LEFT OUTER JOIN`. It keeps every left-table row, and unmatched right columns are `null`. -- `outerRightJoin()` is SQL's `RIGHT OUTER JOIN`. It keeps every right-table row, and unmatched left columns are `null`. -- `outerFullJoin()` is SQL's `FULL OUTER JOIN`. It keeps unmatched rows from both sides. +Keep unmatched rows from one or both sides, filling the missing side's columns with `null`. Each takes the same `(other, on)` arguments and returns a `SelectQuery` as [`innerJoin()`](#innerjoin) does. `outerLeftJoin()` is SQL's `LEFT OUTER JOIN` and keeps every left-table row. `outerRightJoin()` is `RIGHT OUTER JOIN` and keeps every right-table row. `outerFullJoin()` is `FULL OUTER JOIN` and keeps unmatched rows from both sides. #### Examples @@ -333,14 +327,15 @@ const rows = await runtime.query(plan); ### `lateralJoin()` -Correlate a per-row subquery against the outer row: for each outer row, the joined subquery can reference that row's columns. Useful for a top-N-per-group query, such as each user's most recent post. +Correlate a per-row subquery against the outer row: for each outer row, the joined subquery can reference that row's columns. Use it for a query such as the newest post for each user. #### Remarks -- **Availability:** PostgreSQL only. On SQLite TypeScript rejects the call. If you get past the types, it throws an error whose `code` is `ORM.CAPABILITY_MISSING`. `outerLateralJoin()` is the left-join form: same arguments, same availability, and the subquery's columns are nullable. +- **Availability:** PostgreSQL only. On SQLite TypeScript rejects the call. If you cast, or call it from plain JavaScript, it throws an error whose `code` is `ORM.CAPABILITY_MISSING`. - The callback receives a `lateral` builder. Start its subquery with `lateral.from(otherTable)`, then chain the usual `SELECT` methods. Inside the callback, the outer table's columns are on `f` under its table name, such as `f.user.id`, so the subquery can filter on the outer row. - Return the query chain **directly** from the callback. Do **not** call `.as(...)` on it. The first argument of `lateralJoin()` already names the subquery. -- A column name that both tables have must be written under its table name. Outside the callback, the subquery's columns are under the alias you gave it, such as `f.latestPost.id`. Inside the callback, the joined table is still `post`, so the same column is `f.post.id`. A bare `select('id')` or `orderBy((f) => f.createdAt, ...)` throws, because a name both tables have is not in the query's scope on its own. +- Every column is on `f` under its table name, and a column name that only one table has is also on `f` directly. Outside the callback, the subquery's columns are under the alias you gave it, such as `f.latestPost.id`. Inside the callback, the joined table is still `post`, so the same column is `f.post.id`. Because both tables have `id` and `createdAt`, a bare `select('id')` or `orderBy((f) => f.createdAt, ...)` throws. +- `outerLateralJoin(alias, callback)` is the `LEFT JOIN LATERAL` form: same arguments, and rows with no match keep the outer row. #### Options @@ -382,14 +377,18 @@ Sort the result set by a column or a computed expression, in a direction you cho #### Options -`orderBy()` accepts a column name or an expression callback, followed by options. - | Name | Type | Required | Description | |---|---|---|---| | `key` | Column name (`string`) or `(f, fns) => Expression` | Yes | The column or computed value to sort by. | | `options.direction` | `'asc' \| 'desc'` | No | Sort direction. | -TypeScript also accepts a `nulls` option, but the builder ignores it and no `NULLS FIRST` or `NULLS LAST` reaches the SQL. Do not use it. To put nulls last, sort first by a computed value: an `fns.raw` fragment of the form `CASE WHEN IS NULL THEN 1 ELSE 0 END` that returns `pg/int4@1`. Then call `orderBy()` again for the column itself. +TypeScript also accepts a `nulls` option. It does nothing, and no `NULLS FIRST` or `NULLS LAST` reaches the SQL. To put nulls last, sort by a computed value first, then by the column: + +```ts +db.sql.public.post + .orderBy((f, fns) => fns.raw`CASE WHEN ${f.title} IS NULL THEN 1 ELSE 0 END`.returns('pg/int4@1')) + .orderBy('title', { direction: 'asc' }); +``` #### Return type @@ -442,8 +441,8 @@ Keep the first row per distinct key, according to the query's `orderBy()` (`DIST #### Remarks -- **Availability:** PostgreSQL only. On SQLite TypeScript rejects the call. If you get past the types, it throws an error whose `code` is `ORM.CAPABILITY_MISSING`. -- List the `distinctOn()` columns first in `orderBy()`, then the column that picks the winner. +- **Availability:** PostgreSQL only. On SQLite TypeScript rejects the call. If you cast, or call it from plain JavaScript, it throws an error whose `code` is `ORM.CAPABILITY_MISSING`. +- Sort keys that come after the `distinctOn()` columns in `orderBy()` decide which row is kept. #### Options @@ -505,7 +504,7 @@ const rows = await runtime.query(plan); ### Subquery via `.as()` -Call `.as(alias)` on a finished `SELECT` query to use it as a subquery. Pass the result to any join method as the `other` argument. `SELECT` queries and [grouped queries](#grouped-queries) have `.as()`. Queries started with `insert()`, `update()`, or `delete()` do not, so you cannot join against the rows they return. +Call `.as(alias)` on a `SELECT` chain before `build()` to use it as a subquery. Pass the result to any join method as the `other` argument. `SELECT` queries and [grouped queries](#grouped-queries) have `.as()`. Queries started with `insert()`, `update()`, or `delete()` do not, so you cannot join against the rows they return. #### Options @@ -538,13 +537,9 @@ const rows = await runtime.query(plan); ## Grouped queries -Grouping starts with [`groupBy()`](#groupby), which turns a select query into a `GroupedQuery`. Call `groupBy()` after all your `select()` calls. A grouped query supports `having()` for group-level filtering, the aggregate functions, and the same `orderBy()`, `limit()`, `offset()`, and `distinct()` methods as a `SelectQuery`. +Grouping starts with [`groupBy()`](#groupby), which turns a select query into a `GroupedQuery`. Write your aggregates in `select()` first, then call `groupBy()`. After that you can add `having()`, `orderBy()`, `limit()`, `offset()`, and `distinct()`. -The examples below use a separate `order` schema, where `order.amount` is an integer column: - -
- -Expand for the aggregate example schema +The examples below use an extra table, `order`, whose `amount` column is an integer: ```prisma model Order { @@ -557,8 +552,6 @@ model Order { } ``` -
- ### `groupBy()` Group rows by one or more columns or by a computed expression, producing one row per distinct group. @@ -595,25 +588,36 @@ const rows = await runtime.query(plan); ``` :::note[What the aggregates give you back] -- `count()`, `sum()` over an integer column, and `avg()` over an integer column all return a JavaScript `number`. `min()` and `max()` return the input column's own type. -- A `count()` or integer `sum()` result larger than 2^53 - 1 fails with an error whose `code` is `RUNTIME.DECODE_FAILED`. To check for a code, catch the error and compare `error.code`. Use `countBigInt()` or `sumBigInt(field)` when totals can get that large. +- A `count()` or integer `sum()` result larger than 2^53 - 1 throws an error whose `code` is `RUNTIME.DECODE_FAILED`. Use `countBigInt()` or `sumBigInt(field)` when totals can get that large. - `avg()` is computed as a floating-point `number`, so a long decimal is rounded. Use `avgDecimal(field)` when you need the exact figure. +- Every error this page names is an `Error` with a `code` property, so check for one as below. ::: +```ts +try { await runtime.query(plan); } catch (error) { + if (error instanceof Error && 'code' in error && error.code === 'RUNTIME.DECODE_FAILED') { /* the count is too big for a number */ } +} +``` + ##### Group by a computed value ```ts const plan = db.sql.public.order - .select('yearPlaced', (f, fns) => fns.raw`EXTRACT(YEAR FROM ${f.placedAt})`.returns('pg/int4@1')) + .select('yearPlaced', (f, fns) => fns.raw`EXTRACT(YEAR FROM ${f.placedAt})`.returns('pg/numeric@1')) .select('orderCount', (f, fns) => fns.count(f.id)) .groupBy('yearPlaced') .build(); const rows = await runtime.query(plan); -// rows === [{ yearPlaced: '2024', orderCount: 10 }] -// `.returns()` only tells TypeScript what type to expect. It does not convert the value, so yearPlaced is the string '2024'. Write Number(row.yearPlaced) to get 2024. +// rows === [{ yearPlaced: '2024', orderCount: 10 }]. PostgreSQL computes EXTRACT as numeric, which arrives as the string '2024'. Write Number(row.yearPlaced) for 2024. +``` + +Annotate with the type PostgreSQL computes, not the type you want. Here `pg/numeric@1` types `yearPlaced` as `string`, which is what you receive, while `.returns('pg/int4@1')` would type it `number` and still hand you a string. `.groupBy('yearPlaced')` groups by the alias given to the selected value. To group by the expression itself, repeat the fragment inside `groupBy()`, `.returns()` and all: + +```ts + .groupBy((f, fns) => fns.raw`EXTRACT(YEAR FROM ${f.placedAt})`.returns('pg/numeric@1')) ``` -Here `.groupBy('yearPlaced')` groups by the alias given to the selected value. You can also repeat the same `fns.raw` fragment inside `groupBy()`. A name that is neither a column of the table you started from nor an alias you selected throws an error whose `code` is `ORM.COLUMN_UNKNOWN`, at the moment you call `groupBy()`. If you select a column you did not group or aggregate, the query fails when it runs, not when you build it. That failure comes back as PostgreSQL's own error, not a Prisma ORM code. +A name that is neither a column of the table you started from nor an alias you selected throws an error whose `code` is `ORM.COLUMN_UNKNOWN`, at the moment you call `groupBy()`. If you select a column you did not group or aggregate, the query fails when it runs, not when you build it. That failure comes back as PostgreSQL's own error, not a Prisma ORM code. ### `having()` @@ -621,8 +625,8 @@ Filter groups by an aggregate comparison. Build the predicate from the aggregate #### Remarks -- Compare against a plain JavaScript number, as in `fns.gt(fns.count(), 1)`. Passing a `bigint` literal such as `1n` throws an error whose `code` is `RUNTIME.ENCODE_FAILED` when the query runs. -- `having()` has no column-name form, so write the aggregate again in the predicate. TypeScript does let you name a selected alias on the `f` argument here, but PostgreSQL does not accept an alias in `HAVING`, and the query then fails when it runs. +- Compare against a plain JavaScript number, as in `fns.gt(fns.count(), 1)`. Passing a `bigint` literal such as `1n` throws an error whose `code` is `RUNTIME.ENCODE_FAILED` when the query runs. `fns.countBigInt()` and `fns.sumBigInt()` are the other way round: compare those against a `bigint` literal, as in `fns.gt(fns.countBigInt(), 1n)`. +- Do not use a selected alias in `having()`. TypeScript will let you, but PostgreSQL rejects it and the query fails when it runs. Write the aggregate again instead. #### Options @@ -647,8 +651,7 @@ const plan = db.sql.public.order .groupBy('customerId') .having((f, fns) => fns.gt(fns.sum(f.amount), 1000)) .build(); -const rows = await runtime.query(plan); -// only the customer whose orders sum over 1000; totalAmount is 1500 +const rows = await runtime.query(plan); // only the customer whose orders sum over 1000, with totalAmount 1500 ``` ##### Compare with `count()`, `avg()`, `min()`, `max()` @@ -663,8 +666,7 @@ const plan = db.sql.public.order .groupBy('customerId') .having((f, fns) => fns.and(fns.gt(fns.count(), 1), fns.gt(fns.avg(f.amount), 100))) .build(); -const rows = await runtime.query(plan); -// avgAmount is 300, exactAvg is the exact string '300.0000000000000000', minAmount is 100, maxAmount is 500 +const rows = await runtime.query(plan); // avgAmount is 300, exactAvg is the exact string '300.0000000000000000', minAmount is 100, maxAmount is 500 ``` ### Ordering and limiting a grouped query @@ -683,13 +685,16 @@ const plan = db.sql.public.order .orderBy('totalAmount', { direction: 'desc' }) .limit(1) .build(); -const rows = await runtime.query(plan); -// the single highest-spending customer +const rows = await runtime.query(plan); // the single highest-spending customer ``` ## Mutations -Mutations start from a table with `insert()`, `update()`, or `delete()`, and end with [`build()`](#build). Add [`returning()`](#returning) to get the affected rows back. Run a mutation with no `returning()` using `runtime.execute(plan)`, which resolves to `{ affectedRows }`. With `returning()`, run it with `runtime.query(plan)`. The examples below go back to this page's main schema, so they use `db.sql.public.tag` and `db.sql.public.user` again. +:::warning[An update or delete with no `where()` hits every row] +`where()` is optional on `update()` and `delete()`. Leave it out and every row in the table is changed, with no warning. +::: + +Mutations start from a table with `insert()`, `update()`, or `delete()`, and end with [`build()`](#build). Add [`returning()`](#returning) to get the affected rows back. Run a mutation with no `returning()` using `runtime.execute(plan)`, which resolves to `{ affectedRows }`. With `returning()`, run it with `runtime.query(plan)`. The examples below go back to this page's example models, so they use `db.sql.public.tag` and `db.sql.public.user` again. ### `insert()` @@ -698,17 +703,16 @@ Insert one or more rows in a single statement. #### Remarks - `insert()` always takes an **array**. A single-row insert is a one-element array. There is no separate single-row overload. An empty array throws an error whose `code` is `ORM.MUTATION_DATA_MISSING`. -- Leave a column out and the `@default(...)` in `contract.prisma` is applied, such as `@default(uuid())` or `@default(now())`. `build()` generates default values once. Running the same built insert twice writes the same id twice, which fails on a unique column. Call `.build()` again for each run. -- Leave out a required column that has no default and PostgreSQL rejects the insert when the query runs. +- Leave a column out and the `@default(...)` in `contract.prisma` is applied. Prisma ORM generates `@default(uuid())` and `@default(now())` values when you call `build()`, not when the query runs. The built query then holds fixed values, so running it twice inserts the same id twice, which fails on a unique column. Call `.build()` again for each insert. - There is no `ON CONFLICT` and no upsert here. For "insert or update", use the ORM client's [`upsert()`](/orm/reference/orm-client#upsert) or write the statement with [`db.raw.sql`](/orm/reference/raw-queries). #### Options | Name | Type | Required | Description | |---|---|---|---| -| `rows` | Array of row objects | Yes | The rows to insert. Every column is optional. | +| `rows` | Array of row objects | Yes | The rows to insert. Every column is optional in TypeScript. | -Each value is sent as a query parameter, never pasted into the SQL. The column it goes into decides how it is stored and read, so you never call `.returns()` or [`param()`](#param) on an insert value. +PostgreSQL still rejects a missing required column that has no default. Never call `.returns()` or [`param()`](#param) on an insert value. #### Return type @@ -722,8 +726,7 @@ Each value is sent as a query parameter, never pasted into the SQL. The column i ```ts const plan = db.sql.public.tag.insert([{ label: 'single-row-tag' }]).build(); -await runtime.execute(plan); -// execute runs a write that returns no rows. id is filled in from @default(uuid()) +await runtime.execute(plan); // execute runs a write that returns no rows. id is filled in from @default(uuid()) ``` ##### Insert multiple rows in one statement @@ -760,24 +763,23 @@ Return columns from the rows affected by an `insert()`, `update()`, or `delete() ```ts const plan = db.sql.public.tag.insert([{ label: 'returned-tag' }]).returning('id', 'label').build(); -const rows = await runtime.query(plan); -// rows === [{ id: '', label: 'returned-tag' }] +const rows = await runtime.query(plan); // rows === [{ id: '', label: 'returned-tag' }] ``` ### `update()` -Update matched rows. Set columns with a values object, or derive new values from existing columns with an expression callback. Choose the rows with `where()`, and use [`returning()`](#returning) to get the updated rows. `where()` is optional. An `update()` with no `where()` updates every row in the table, and nothing warns you. +Update matched rows. Set columns with a values object, or derive new values from existing columns with an expression callback. Choose the rows with `where()`, and use [`returning()`](#returning) to get the updated rows. #### Options -`update()` accepts a values object or an expression callback. +`update()` accepts a values object or an expression callback. You cannot mix the two forms in one call. | Form | Signature | Description | |---|---|---| | Values object | `update({ col: value, ... })` | Set columns to fixed values. | | Expression callback | `update((f, fns) => ({ col: expr }))` | Set columns to expressions computed from existing columns. | -Every value the callback returns must be an expression. To set one column to a fixed value in the callback form, wrap that value in a `fns.raw` fragment, as the second example below does. +Every value the callback returns must be an expression. To set a column to a fixed value in the callback form, wrap that value in a `fns.raw` fragment, as the second example below does. #### Return type @@ -795,8 +797,7 @@ const plan = db.sql.public.user .where((f, fns) => fns.eq(f.email, 'bob@example.com')) .returning('id', 'displayName') .build(); -const rows = await runtime.query(plan); -// one row, with displayName now 'Bobby' +const rows = await runtime.query(plan); // one row, with displayName now 'Bobby' ``` ##### Derive a value from an existing column @@ -810,13 +811,12 @@ const plan = db.sql.public.user .where((f, fns) => fns.eq(f.email, 'carol@example.com')) .returning('id', 'displayName') .build(); -const rows = await runtime.query(plan); -// one row, with displayName now 'CAROL' and a fixed new email +const rows = await runtime.query(plan); // one row, with displayName now 'CAROL' and a fixed new email ``` ### `delete()` -Delete matched rows. `delete()` takes no arguments. Choose the rows with `where()`, and use [`returning()`](#returning) to get the deleted rows back. `where()` is optional. A `delete()` with no `where()` deletes every row in the table, and nothing warns you. +Delete matched rows. `delete()` takes no arguments. Choose the rows with `where()`, and use [`returning()`](#returning) to get the deleted rows back. #### Return type @@ -834,8 +834,7 @@ const plan = db.sql.public.tag .where((f, fns) => fns.eq(f.label, 'to-delete')) .returning('id', 'label') .build(); -const rows = await runtime.query(plan); -// one row, the tag that was deleted +const rows = await runtime.query(plan); // one row, the tag that was deleted ``` ### `param()` @@ -844,8 +843,8 @@ Give a value its type by hand, for a value with no column to take the type from, #### Remarks -- Import `param` from `@prisma/orm-postgres/relational-core/expression`. `param` and `ResultType` are in different subpaths of the same package, so use the paths shown. -- You rarely need `param()`. Values passed to `insert()`, `update()`, and comparison functions such as `fns.eq(f.col, value)` each take their type from the column they are used with. `param()` is for the values that have no such column. +- Import `param` from `@prisma/orm-postgres/relational-core/expression`. +- Values passed to `insert()`, `update()`, and comparison functions such as `fns.eq(f.col, value)` each take their type from the column they are used with, so you rarely need `param()`. Reach for it when the type the builder would pick is not the one the column needs. A bare string becomes `pg/text@1`, so a value going into a `uuid` column needs `param(id, { codecId: 'pg/uuid@1' })`. A `Temporal.Instant` cannot be interpolated bare at all. - The option is called `codecId`, but it takes the same type id used everywhere else on this page. [Binding a bare value with `param()`](/orm/reference/raw-queries#binding-a-bare-value-with-param) lists the type id each kind of JavaScript value gets, which is where to look for a date, a uuid, a boolean, or an array. #### Options @@ -868,10 +867,10 @@ Give a value its type by hand, for a value with no column to take the type from, ```ts import { param } from '@prisma/orm-postgres/relational-core/expression'; -const targetEmailDomain = param('%@example.com', { codecId: 'pg/text@1' }); +const targetId = param('11111111-1111-1111-1111-111111111111', { codecId: 'pg/uuid@1' }); const plan = db.sql.public.user .select('id', 'email') - .where((f, fns) => fns.raw`${f.email} LIKE ${targetEmailDomain}`.returns('pg/bool@1')) + .where((f, fns) => fns.raw`${f.id} = ${targetId}`.returns('pg/bool@1')) .build(); const rows = await runtime.query(plan); ``` @@ -911,36 +910,36 @@ const authors = db.sql.public.user .build(); ``` -Which aggregate functions exist depends on your database. PostgreSQL gives you these eight: +Which aggregate functions exist depends on your database. PostgreSQL gives you these eight. The contract type is the one you write in `contract.prisma`, and the PostgreSQL type is in brackets: | Function | Returns | |---|---| -| `count()` / `count(field)` | `number`. With no argument it counts rows; with a field it counts that field's non-null values | -| `countBigInt()` / `countBigInt(field)` | `bigint`. The version of `count` that does not overflow | -| `sum(field)` | `number` over an integer column. Over a `numeric` column you get a string, and over a `float8` column a `number` | -| `sumBigInt(field)` | `bigint`. The version of `sum` over an integer column that does not overflow | -| `avg(field)` | `number` over an integer or float column | +| `count()` / `count(field)` | `number`. With no argument it counts rows. With a field it counts that field's non-null values | +| `countBigInt()` / `countBigInt(field)` | `bigint`. Use it when the count can pass [2^53 - 1](#group-by-a-column-with-a-count) | +| `sum(field)` | `number` over an `Int` (`int4`) or `BigInt` (`int8`) column. A string over `Decimal` or `Numeric` (`numeric`), a `number` over `Float` (`float8`) | +| `sumBigInt(field)` | `bigint`. The sum over an integer column that can pass [2^53 - 1](#group-by-a-column-with-a-count) | +| `avg(field)` | `number` over an `Int` (`int4`) or `Float` (`float8`) column | | `avgDecimal(field)` | An exact decimal string. The version of `avg` that does not round | -| `min(field)` | The input column's own type. A `varchar` column is typed as `text` | -| `max(field)` | The input column's own type. A `varchar` column is typed as `text` | +| `min(field)` | The input column's own type. A `VarChar` column gives a `String` (`text`) | +| `max(field)` | The input column's own type. A `VarChar` column gives a `String` (`text`) | -On PostgreSQL you also get `ilike(expr, pattern)` for text columns, as in `fns.ilike(f.displayName, '%alice%')`. `cosineDistance(a, b)` compares two vector values and returns a `number`. It comes with pgvector, the PostgreSQL extension for vector columns, so it is there only if your contract uses pgvector. +On PostgreSQL you also get `ilike(expr, pattern)` for text columns, as in `fns.ilike(f.displayName, '%alice%')`. `cosineDistance(a, b)` compares two vector values and returns a `number`. It comes with pgvector, the PostgreSQL extension for vector columns, so it is there only if your contract composes pgvector. See [Extension types](/orm/contract-authoring/psl-syntax#extension-types). ### `fns.raw` and `.returns()` -Write a raw SQL fragment as a tagged template. Interpolate columns, typed expressions, and bare JavaScript values with `${...}`. For a bare value, the builder picks the type id from its JavaScript type. [Binding a bare value with `param()`](/orm/reference/raw-queries#binding-a-bare-value-with-param) lists which JavaScript type becomes which type id, and shows how to choose a different one with `param(...)`. Call `.returns(codecId)` to declare the fragment's result type. +Write a raw SQL fragment as a tagged template. Interpolate columns, typed expressions, and bare JavaScript values with `${...}`. For a bare value, the builder picks the type id from its JavaScript type. [Binding a bare value with `param()`](/orm/reference/raw-queries#binding-a-bare-value-with-param) lists which JavaScript type becomes which type id, and shows how to choose a different one with `param(...)`. Call `.returns(typeId)` to declare the fragment's result type. #### Options | Name | Type | Required | Description | |---|---|---|---| | SQL fragment | Tagged template | Yes | The raw SQL, with `${...}` interpolations for columns and values. | -| `.returns(codecId)` | `string` | Yes, always | Declares the result type, for example `'pg/int4@1'`, `'pg/text@1'`. For a fragment used as a `where()` predicate, write `.returns('pg/bool@1')`. | +| `.returns(typeId)` | `string` | Yes, always | Declares the result type, for example `'pg/int4@1'`, `'pg/text@1'`. For a fragment used as a `where()` predicate, write `.returns('pg/bool@1')`. | #### Remarks - **Always call `.returns()`.** Nothing in the builder accepts a fragment without it. -- **`.returns()` only tells TypeScript what type to expect.** It does not convert the value. PostgreSQL computes `EXTRACT(...)` as `numeric`, which arrives as a string, so `yearPlaced` is `'2024'`. Convert it in JavaScript with `Number(...)`, or run the statement with [`db.raw.sql` and `.returnsRow()`](/orm/reference/raw-queries), which does decode. +- **`.returns()` only tells TypeScript what type to expect.** It does not convert the value. Convert the value in JavaScript, or run the statement with [`db.raw.sql` and `.returnsRow()`](/orm/reference/raw-queries), which does decode. - Use `fns.raw` for any SQL feature without a dedicated helper: `COALESCE`, `CAST`, `LENGTH`, `UPPER`, `EXTRACT`, and so on. There is no `fns.coalesce` and no `fns.cast`. #### Examples @@ -1004,6 +1003,6 @@ const rows = await runtime.query(plan); // Row[] ### Streaming vs. collecting -`await runtime.query(plan)` collects every row into an array. This is the common case and what every example on this page uses. You can also loop the same result with `for await` to handle one row at a time. Use `for await` when you want to start handling rows before the array is assembled. It does not reduce memory on PostgreSQL, because both `await` and `for await` read the same result the client has already loaded. +`await runtime.query(plan)` collects every row into an array. This is the common case and what every example on this page uses. You can also loop the same result with `for await` to handle one row at a time, which lets you start work before the array is assembled. On PostgreSQL, use `await`. -You can `await` a result as many times as you like. You cannot mix `await` and `for await` on the same result, and you cannot `for await` it twice. Either one throws an error whose `code` is `RUNTIME.ITERATOR_CONSUMED`. See [`AsyncIterableResult`](/orm/reference/orm-client#asynciterableresult). +Keep the result in a variable, as in `const result = runtime.query(plan)`, and you can `await result` as many times as you like. You cannot mix `await` and `for await` on the same result, and you cannot `for await` it twice. Either one throws an error whose `code` is `RUNTIME.ITERATOR_CONSUMED`. See [`AsyncIterableResult`](/orm/reference/orm-client#asynciterableresult). diff --git a/apps/docs/content/docs/orm/reference/transactions-and-runtime.mdx b/apps/docs/content/docs/orm/reference/transactions-and-runtime.mdx index 25c424a0aa0..c46d06653f6 100644 --- a/apps/docs/content/docs/orm/reference/transactions-and-runtime.mdx +++ b/apps/docs/content/docs/orm/reference/transactions-and-runtime.mdx @@ -8,9 +8,13 @@ metaDescription: 'Reference for the Prisma ORM client lifecycle, transactions, p Prisma ORM 8 renames `schema.prisma` to `contract.prisma`. Run `npx prisma contract emit`. It writes two files next to it: `contract.json`, which your app imports, and `contract.d.ts`, which gives you the TypeScript types. A new project keeps all three files in `src/prisma/`. Every client you create needs `contract.json`. The query you came for, `prisma.user.findMany({ where })`, is now `db.orm.public.User.where(...).all()`. [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7) lists the packages to install, the commands to run, and the rest of the renamed calls. -`db` is the client `postgres(...)` or `mongo(...)` returns. The runtime is the connected object you run queries on, and `db.runtime()` returns it. The import path `@prisma/orm-postgres/runtime` is a package name and is not that object. +`db` is the client that `postgres(...)` or `mongo(...)` returns. `db.runtime()` gives you the object that actually runs queries. That object is called the runtime. -This page documents the client lifecycle (create, connect, close), transactions, prepared statements, and per-query execution options across PostgreSQL and MongoDB. Each method below has a Remarks list. If a method behaves differently on PostgreSQL and MongoDB, or exists on only one of them, that list says so. Transactions are fully supported on PostgreSQL. They are not available on MongoDB. To run one there, pass your own `MongoClient` with the `mongoClient` option and use the driver's `session.withTransaction(...)`. See [Transactions on MongoDB](/orm/fundamentals/transactions#transactions-on-mongodb) for the code. For a task-oriented walkthrough, see the Fundamentals guide to [Transactions](/orm/fundamentals/transactions). For the ways to query inside a transaction, see the [ORM client reference](/orm/reference/orm-client), the [SQL query builder reference](/orm/reference/sql-query-builder), and the [raw queries reference](/orm/reference/raw-queries). +This page documents the client lifecycle (create, connect, close), transactions, prepared statements, and per-query execution options across PostgreSQL and MongoDB. For a task-oriented walkthrough, see the Fundamentals guide to [Transactions](/orm/fundamentals/transactions). For the ways to query inside a transaction, see the [ORM client reference](/orm/reference/orm-client), the [SQL query builder reference](/orm/reference/sql-query-builder), and the [raw queries reference](/orm/reference/raw-queries). + +Each method below has a Remarks list. If a method behaves differently on PostgreSQL and MongoDB, or exists on only one of them, that list says so. + +Transactions are PostgreSQL only. To run one on MongoDB, pass your own `MongoClient` with the `mongoClient` option and use the driver's `session.withTransaction(...)`, which [Transactions on MongoDB](/orm/fundamentals/transactions#transactions-on-mongodb) shows in code. ## Client lifecycle on PostgreSQL @@ -23,12 +27,12 @@ Create a PostgreSQL client. #### Remarks - Import `contract.json` and pass it as `contractJson`, as every example below does. Pass `contract` only if you already hold a contract object; most code passes `contractJson`. Supply exactly one. -- The `` type argument is what types `db.orm` and `db.sql`. Pass it yourself whenever you pass `contractJson`, because TypeScript cannot read a type out of a JSON file. It is inferred only on the `contract` path. -- Tell the client which database to use in one of three ways: `url`, `pg`, or `binding`. A `binding` is one object that names the source and carries it: `{ kind: 'url', url }`, `{ kind: 'pgPool', pool }`, or `{ kind: 'pgClient', client }`. Use it when your code picks the source at run time. Pass exactly one of `url`, `pg`, and `binding`, or leave all three out and pass one to [`connect()`](#connect). -- When you pass a `pg` pool or client, you own it and you close it. When you pass a `url`, the client creates the pool and closes it. Set that pool's timeouts with `poolOptions`. `poolOptions` has no effect on a pool you supply yourself. +- The `` type argument is what types `db.orm` and `db.sql`. `contract.d.ts` exports it under the name `Contract`. Pass it yourself whenever you pass `contractJson`, because TypeScript cannot read a type out of a JSON file. It is inferred only when you use the `contract` option. +- Tell the client which database to use in one of three ways: `url`, `pg`, or `binding`. A `binding` wraps any of the three choices in one object, with a `kind` field saying which it is: `{ kind: 'url', url }`, `{ kind: 'pgPool', pool }`, or `{ kind: 'pgClient', client }`. Use it when your code picks the source at run time. Pass exactly one of `url`, `pg`, and `binding`, or leave all three out and pass one to [`connect()`](#connect). +- When you pass a `pg` pool or client, you own it: call `db.close()` first, then close it yourself with `pool.end()`. When you pass a `url`, the client creates the pool and closes it. Set that pool's timeouts with `poolOptions`, which has no effect on a pool you supply yourself. - Some types come from a PostgreSQL extension, such as a `pgvector.Vector(1536)` type in your `contract.prisma`. Run `npm install @prisma/orm-extension-pgvector` first. Then `import pgvector from '@prisma/orm-extension-pgvector/runtime'` and pass `extensions: [pgvector]`. - A middleware is an object with a name and one or more hooks, such as `{ name: 'no-big-deletes', beforeQuery: (query) => { if (query.sql.includes('DELETE')) throw new Error('blocked') } }`. Throw from a hook to block the query. The hooks are `beforeCompile`, `beforeQuery`, `beforeExecute`, `interceptQuery`, `interceptExecute`, `onRow`, `afterQuery`, and `afterExecute`. See [How middleware works](/orm/middleware/how-middleware-works). -- `verifyMarker: 'onFirstUse'`, the default, makes the first query check that the database was set up for your contract. If it was not, Prisma ORM logs a warning and runs the query anyway. The usual fix is to apply your migrations with `npx prisma db update`. `verifyMarker: false` skips the check. See [How migrations work](/orm/migrations/how-migrations-work). +- Migrations record in your database which contract it matches. With `verifyMarker: 'onFirstUse'`, the default, the first query checks that record and logs a warning on a mismatch, then runs anyway. If you see the warning, run `npx prisma db update`. `verifyMarker: false` skips the check. - `postgres(options)` does not open a connection. The client opens one on first use, so [`connect()`](#connect) is optional: call it to open the connection up front and fail early. #### Options @@ -42,7 +46,7 @@ Pass exactly one of `url`, `pg`, and `binding`. | `pg` | `Pool` or `Client` from the `pg` package | See note | An existing `pg` instance to use. You close it yourself. | | `binding` | `{ kind: 'url' \| 'pgPool' \| 'pgClient', ... }` | See note | The same three choices in one object. | | `poolOptions` | `{ connectionTimeoutMillis?: number; idleTimeoutMillis?: number }` | No | Timeouts for the pool the client creates from `url` (defaults `20000` / `30000`). | -| `extensions` | Array of extension packages | No | The npm packages for the PostgreSQL extensions your contract's types need. Install each one, then import it and put it in this array. | +| `extensions` | Array of extension runtimes, the default export of each package's `/runtime` subpath | No | The PostgreSQL extensions your contract's types need. Install each package, then import its `/runtime` subpath and put the default export in this array. | | `middleware` | Array of middleware | No | Code that runs around every query. See [How middleware works](/orm/middleware/how-middleware-works). | | `verifyMarker` | `'onFirstUse'` or `false` | No | Whether the first query checks that the database was set up for this contract. Defaults to `'onFirstUse'`. | @@ -54,8 +58,8 @@ Pass exactly one of `url`, `pg`, and `binding`. - `.orm` holds your models by model name. See the [ORM client reference](/orm/reference/orm-client). - `.sql` is the SQL query builder, keyed by schema, then table name: `db.sql.public.tag`. The table name is the model's `@@map` value, or the model name with a lowercase first letter when the model sets no `@@map`. See the [SQL query builder reference](/orm/reference/sql-query-builder). `.raw` writes raw SQL as a template string. See the [raw queries reference](/orm/reference/raw-queries). -- `.enums` holds the enum values from your contract, by PostgreSQL schema: `db.enums.public.Role`. `.nativeEnums` holds the values of enum types that exist in PostgreSQL itself, created with `CREATE TYPE`, also by schema. Look the type up by name in brackets: `db.nativeEnums.public['AalLevel'].values` is `['aal1', 'aal2']`. A PostgreSQL enum type has values and no separate names, so each value is its own name: `db.nativeEnums.public['AalLevel'].nameOf('aal1')` is `'aal1'`. -- `.contract` is the contract as an object, and `.context` is internal. [`connect()`](#connect), [`runtime()`](#runtime), [`transaction()`](#dbtransactioncallback), [`prepare()`](#dbpreparedeclaration-callback), and [`close()`](#close) are documented on this page. +- `.enums` holds the enum values from your contract, by PostgreSQL schema: `db.enums.public.Role`. `.nativeEnums` holds the values of enum types that exist in PostgreSQL itself, created with `CREATE TYPE`, also by schema. Look the type up by name in brackets: `db.nativeEnums.public['AalLevel'].values` is `['aal1', 'aal2']`. +- `.contract` is the contract as an object. [`connect()`](#connect), [`runtime()`](#runtime), [`transaction()`](#dbtransactioncallback), [`prepare()`](#dbpreparedeclaration-callback), and [`close()`](#close) are documented on this page. #### Examples @@ -69,7 +73,7 @@ import contractJson from './contract.json' with { type: 'json' }; const db = postgres({ contractJson, url: process.env.DATABASE_URL! }); ``` -These examples assume the file is beside `contract.json` in `src/prisma/`. From anywhere else, change the relative paths. Write `./contract.d` exactly like that, with no extension. The line `import contractJson from './contract.json' with { type: 'json' }` is what `prisma orm init` writes, and it needs Node.js 22.18 or newer and TypeScript set up as `prisma orm init` sets it up. TypeScript types every `process.env` value as possibly undefined, so the examples write `!` after it, the same way `prisma orm init` does. +These examples assume the file is beside `contract.json` in `src/prisma/`. From anywhere else, change the relative paths. Write `./contract.d` exactly like that, with no extension. The line `import contractJson from './contract.json' with { type: 'json' }` is what `prisma orm init` writes. It needs Node.js 22.18 or newer, and these `tsconfig.json` settings: `resolveJsonModule: true`, `module: "preserve"`, and `moduleResolution: "bundler"`. TypeScript types every `process.env` value as possibly undefined, so the examples write `!` after it, the same way `prisma orm init` does. ##### Bind to an existing `pg` pool @@ -81,7 +85,6 @@ import contractJson from './contract.json' with { type: 'json' }; const pool = new Pool({ connectionString: process.env.DATABASE_URL! }); const db = postgres({ contractJson, pg: pool }); -// you own `pool`; close it yourself when the client is done ``` ### `connect()` @@ -93,13 +96,12 @@ Open a connection and return a runtime. - Returns a `Promise`. The runtime is the connected object that runs queries. It has five calls: - `runtime.query(built)` runs a built query and returns its rows. A built query is the object you get from `.build()` at the end of a builder or raw chain. See [Running a built query](/orm/reference/sql-query-builder#executing-a-plan). - `runtime.execute(built)` runs a built query that returns no rows, such as an insert, and returns `{ affectedRows }`. See [Running a built query](/orm/reference/sql-query-builder#executing-a-plan). - - `runtime.connection()` takes one connection out of the pool so that several queries run on the same connection. Call `connection.release()` when you are done, to put it back. See [Manual connection and transaction control](#manual-connection-and-transaction-control). - - `runtime.prepare(...)` prepares a statement you run many times with different values. The client has a `prepare()` too, and the two do the same thing. See [Prepared statements (PostgreSQL)](#prepared-statements-postgresql). + - `runtime.connection()` takes one connection out of the pool so that several queries run on the same connection. It returns a promise, and you put the connection back with `connection.release()`: `const connection = await runtime.connection(); try { await connection.query(built) } finally { await connection.release() }`. See [Manual connection and transaction control](#manual-connection-and-transaction-control). + - `runtime.prepare(declaration, callback)` prepares a statement you run many times with different values. The client has a `prepare()` too, and the two do the same thing. See [Prepared statements (PostgreSQL)](#prepared-statements-postgresql). - `runtime.telemetry()` reports how the most recent query went. See [`runtime.telemetry()`](#runtimetelemetry). - `connect()` takes the same database options as `postgres(...)`, so a client you created without one can be given its database here: `await db.connect({ url: process.env.DATABASE_URL! })`, or `await db.connect({ binding: { kind: 'url', url } })`. - Every error has a `code` property, and the [error reference](/orm/reference/error-reference) lists them all. Catch the error and compare `error.code` with the value shown here, for example `if (error.code === 'DRIVER.NOT_CONNECTED')`. - Call `connect()` before any query, or not at all. Running a query connects the client, so a `connect()` after that rejects with an error whose `code` is `DRIVER.ALREADY_CONNECTED`. So does a second `connect()`. -- Calling `connect()` after the client has been closed rejects with an error whose `code` is `DRIVER.NOT_CONNECTED` (`Postgres client is closed`). #### Return type @@ -136,9 +138,8 @@ Close the client and release its pool. #### Remarks -- Returns a `Promise`. If a connection is being opened when you call it, `close()` waits for that to finish, then releases the pool it created. +- Returns a `Promise`. `close()` is safe to call at any time. - After `close()`, `connect()` rejects and `runtime()` throws an error whose `code` is `DRIVER.NOT_CONNECTED` (`Postgres client is closed`). -- The client only owns pools it created from a `url`. When you passed your own `pg` pool or client, close that instance yourself. #### Return type @@ -152,7 +153,7 @@ Close the client and release its pool. ```typescript const db = postgres({ contractJson, url: process.env.DATABASE_URL! }); -const tags = await db.runtime().query(db.sql.public.tag.select('id', 'label').build()); +const admins = await db.orm.public.User.where((u) => u.kind.eq('admin')).all(); await db.close(); ``` @@ -163,7 +164,7 @@ The client implements `Symbol.asyncDispose`, so `await using` closes it automati #### Remarks - Disposal fires at the end of the **block** the `await using` declaration lives in, not on the next line. Scope the client to the block where you need it. After the block exits, the client is closed the same way `close()` closes it: a later `connect()` rejects with an error whose `code` is `DRIVER.NOT_CONNECTED`. -- `await using` needs `@types/node` installed, or `"lib": ["esnext.disposable"]` in your `tsconfig.json`. +- `await using` needs `@types/node` installed, or `"esnext.disposable"` added to the `lib` array in your `tsconfig.json`. #### Examples @@ -187,11 +188,10 @@ Create a MongoDB client. #### Remarks - Pass the contract with `contractJson` or `contract` (supply exactly one), the same way as on PostgreSQL. -- Tell the client which database to use in one of four ways: `url`, `uri`, `mongoClient`, or `binding`. A `binding` is one object that names the source and carries it, in two kinds: `{ kind: 'url', url, dbName }` and `{ kind: 'mongoClient', client, dbName }`. Use it when your code picks the source at run time. The `uri` path uses the `url` kind. -- Pass exactly one of `url`, `uri`, `mongoClient`, and `binding`, or leave all four out and pass one to [`connect()`](#connect-1). `dbName` is a separate option and does not count as one of the four, so `{ url, dbName }` is allowed. -- With `url`, the database name comes from the path of the connection string, as in `mongodb://host:27017/app`. Add `dbName` to override that name, or when the string has no name in its path. With `uri`, Prisma ORM never reads the database name from the string, so `dbName` is required. That is the only difference between `uri` and `url`. +- Tell the client which database to use in one of four ways: `url`, `uri`, `mongoClient`, or `binding`. A `binding` wraps the choices in one object, with a `kind` field saying which it is: `{ kind: 'url', url, dbName }` or `{ kind: 'mongoClient', client, dbName }`. Use it when your code picks the source at run time, and use the `url` kind for a `uri`. Pass exactly one of the four, or leave all four out and pass one to [MongoDB `connect()`](#connect-1). `dbName` is separate and does not count as one of the four, so `{ url, dbName }` is allowed. +- With `url`, the database name comes from the path of the connection string, as in `mongodb://host:27017/app`. Add `dbName` to override that name, or when the string has no name in its path. Use `uri` only when you must pass `dbName` separately. - **Client ownership**: with `url` or `uri`, Prisma ORM creates the underlying `MongoClient` and closes it on `close()`. With `mongoClient`, you supplied the client, so Prisma ORM does **not** close it. Because `close()` leaves your client open, you can use one `MongoClient` both through Prisma ORM and in code you write against the `mongodb` package directly. You close that client yourself. -- `mongo(options)` does not open a connection. The runtime is built on first use, or explicitly via [`connect()`](#connect-1). +- `mongo(options)` does not open a connection. The runtime is built on first use, or explicitly via [MongoDB `connect()`](#connect-1). #### Options @@ -205,7 +205,7 @@ Pass exactly one of `url`, `uri`, `mongoClient`, and `binding`. `dbName` is sepa | `mongoClient` + `dbName` | `MongoClient` + `string` | See note | An existing `MongoClient` you own, plus the database name. | | `binding` | `{ kind: 'url' \| 'mongoClient', ... }` | See note | The same choices in one object. | | `middleware` | Array of middleware | No | Code that runs around every query. See [How middleware works](/orm/middleware/how-middleware-works). | -| `mode` | `'strict'` or `'permissive'` | No | Defaults to `'strict'`. Only your own middleware reads it. | +| `mode` | `'strict'` or `'permissive'` | No | Defaults to `'strict'`. Prisma ORM ignores this value. It is passed through to your middleware so your own code can branch on it. | #### Return type @@ -215,7 +215,7 @@ Pass exactly one of `url`, `uri`, `mongoClient`, and `binding`. `dbName` is sepa - `.orm` holds your models by collection name, with no schema segment: `db.orm.users`. The collection name is the model's `@@map` value, or the model name with a lowercase first letter when the model sets no `@@map`. The [example schema](/orm/reference/orm-client#example-schema) sets `@@map("users")` on `User`, which is why the key here is `users`. See the [ORM client reference](/orm/reference/orm-client). - `.query` is the pipeline builder, which builds MongoDB aggregation pipelines. See the [pipeline builder reference](/orm/reference/pipeline-builder). `.raw` sends raw MongoDB commands. See the [raw queries reference](/orm/reference/raw-queries). -- `.enums` holds the enum values from your contract, `.contract` is the contract as an object, and `.context` is internal. [`connect()`](#connect-1), [`runtime()`](#runtime-1), and [`close()`](#close-1) are documented below. +- `.enums` holds the enum values from your contract, and `.contract` is the contract as an object. [MongoDB `connect()`](#connect-1), [MongoDB `runtime()`](#runtime-1), and [MongoDB `close()`](#close-1) are documented below. #### Examples @@ -240,7 +240,6 @@ import contractJson from './contract.json' with { type: 'json' }; // `MongoClient` here is the driver's class; the `db` below is Prisma ORM's own MongoClient type const client = new MongoClient(process.env.MONGODB_URL!); const db = mongo({ contractJson, mongoClient: client, dbName: 'app' }); -// you own `client`; db.close() will not close it for you ``` ### `connect()` @@ -251,7 +250,6 @@ Open a connection and return a runtime. - Returns a `Promise`. Call `connect()` before any query, or not at all. Running a query connects the client, so a `connect()` after that rejects with an error whose `code` is `DRIVER.ALREADY_CONNECTED`. So does a second `connect()`. - `connect()` takes the same database options as `mongo(...)`, so a client you created without one can be given its database here: `await db.connect({ url: process.env.MONGODB_URL!, dbName: 'app' })`. -- After the client has been closed, `connect()` rejects with an error whose `code` is `DRIVER.NOT_CONNECTED` (`Mongo client is closed`). #### Return type @@ -298,6 +296,7 @@ Run a built query through the runtime. // 'posts' is the collection name, the same key you use on `db.orm` const built = db.query.from('posts').build(); const posts = await (await db.runtime()).query(built); +const { affectedRows } = await (await db.runtime()).execute(built); // for a write that returns no rows ``` ### `close()` @@ -306,7 +305,7 @@ Close the client. #### Remarks -- Call `db.close()`, not `runtime.close()`. `db.close()` marks the client closed and then closes the `MongoClient` Prisma ORM created. After `runtime.close()` the client still accepts calls and then fails. +- Always call `db.close()`. `runtime.close()` leaves the client looking open, and later calls fail. - Returns a `Promise`. After `close()`, any further use rejects with an error whose `code` is `DRIVER.NOT_CONNECTED` (`Mongo client is closed`). That covers `db.runtime()` and ORM access such as `db.orm.users.first()`, since both go through the same runtime. When you supplied a `mongoClient`, `close()` does not close your client (see [`mongo(options)`](#mongooptions)). #### Return type @@ -345,7 +344,7 @@ The MongoDB client implements `Symbol.asyncDispose` too, so `await using` closes ``` ## Transactions (PostgreSQL) -A transaction groups several writes into one unit: they all commit together, or they all roll back. Use `db.transaction(...)` in application code. If you are writing a function that takes a runtime as a parameter instead of the client, use `withTransaction(...)`. Open a connection and drive the transaction yourself when you need statements on one connection outside a transaction, such as a `SET` before you begin. +A transaction groups several writes into one unit: they all commit together, or they all roll back. Use `db.transaction(...)` in application code. If you are writing a function that takes a runtime as a parameter instead of the client, use `withTransaction(...)`. If you need to run several statements on one connection, get a connection with `runtime.connection()` and call `commit()` or `rollback()` yourself. ### `db.transaction(callback)` @@ -353,15 +352,15 @@ Run a callback inside a transaction. The transaction commits when the callback r #### Remarks -- Query through `tx`, not `db`. `tx.orm` holds your models. Build queries with `tx.sql`, then run them with `tx.query(...)` to get rows back, or `tx.execute(...)` when the write returns no rows. Every call on `tx` uses the same transaction connection, and queries on `db` run outside the transaction. -- `db.transaction(...)` takes the callback and nothing else. There are no `isolationLevel`, `timeout`, or `maxWait` options, and Prisma ORM offers no way to set the isolation level. Nothing retries a failed transaction for you. A transaction has no time limit by default. To set one, set PostgreSQL's `idle_in_transaction_session_timeout` on the connection. +- Query through `tx`, not `db`. `tx.orm` holds your models. Build queries with `tx.sql`, then run them with `tx.query(...)` to get rows back, or `tx.execute(...)` when the write returns no rows. `tx.execute(...)` resolves to `{ affectedRows }`, the number of rows the statement changed. Every call on `tx` uses the same transaction connection, and queries on `db` run outside the transaction. +- `db.transaction(...)` takes the callback and nothing else. There are no `isolationLevel`, `timeout`, or `maxWait` options, and Prisma ORM offers no way to set the isolation level. Nothing retries a failed transaction for you. Write the retry loop yourself. A transaction has no time limit by default. To set one, set PostgreSQL's `idle_in_transaction_session_timeout` in your connection string or server configuration. See the [fundamentals transactions guide](/orm/fundamentals/transactions#options-and-isolation-level). - To put a time limit on the queries inside a transaction, pass a `signal` to each `tx.query(...)` and `tx.execute(...)` call, as the example below does. See [`RuntimeExecuteOptions`](#runtimeexecuteoptions). -- `tx` has no `transaction()` method, so transactions do not nest. If a helper you call starts its own transaction, change it to take `tx` as a parameter instead. `TransactionContext`, imported from `@prisma/orm-postgres/family-runtime`, has `query` and `execute` only, so a helper typed with it cannot use `tx.orm` or `tx.sql`: `async function addTag(tx: TransactionContext) { ... }`. To pass the full `tx`, take its type from the client: `type Tx = Parameters[0]>[0]`. +- `tx` has no `transaction()` method, so transactions do not nest. If a helper you call starts its own transaction, change it to take `tx` as a parameter instead. `TransactionContext`, imported from `@prisma/orm-postgres/family-runtime`, has `query` and `execute` only, so a helper typed with it cannot use `tx.orm` or `tx.sql`: `async function addTag(tx: TransactionContext) { ... }`. There is no exported type for the full `tx`, so derive it: `type Tx = Parameters[0]>[0]`. - `tx.sql` is a full SQL builder, keyed the same way as `db.sql`: use `tx.sql.public.
`, where `public` is the PostgreSQL schema. - Reads inside the transaction see the transaction's own uncommitted writes. - `tx.enums` and `tx.nativeEnums` are the same enum accessors the client has, and `.values` is the list of an enum's members. - The callback's return value passes through as the result of `db.transaction(...)`. -- Do not let an [`AsyncIterableResult`](#asynciterableresult) escape the callback unread. A result you return and then read after the transaction has ended rejects with an error whose `code` is `RUNTIME.TRANSACTION_CLOSED` (see the warning after the examples). To act on a code, catch the error and compare `error.code` with the string. Every code is listed in the [error reference](/orm/reference/error-reference). +- `tx.query(...)` returns an [`AsyncIterableResult`](#asynciterableresult) that works only while the transaction is open. Read it inside the callback. A result you return and then read after the transaction has ended rejects with an error whose `code` is `RUNTIME.TRANSACTION_CLOSED`. To act on a code, catch the error and compare `error.code` with the string. Every code is listed in the [error reference](/orm/reference/error-reference). #### Options @@ -458,8 +457,8 @@ A transaction helper you import directly. Use it when you are writing a function #### Remarks -- Imported from `@prisma/orm-postgres/family-runtime`, along with the `Runtime` type. That path holds the SQL runtime API, the same one for every SQL database. -- The callback receives a transaction handle whose only methods are `query` and `execute`. Unlike `db.transaction(...)`'s `tx`, it has no `.orm` or `.sql`, so build your queries with a SQL builder you already hold, such as the client's `db.sql`. +- Imported from `@prisma/orm-postgres/family-runtime`, along with the `Runtime` type. +- The callback receives a transaction handle whose only methods are `query` and `execute`. Unlike `db.transaction(...)`'s `tx`, it has no `.orm` or `.sql`. Pass a SQL builder into your function as well, such as the client's `db.sql`, and build your queries with that. - Commits on return, rolls back on throw, the same as `db.transaction(...)`. #### Options @@ -482,29 +481,27 @@ A transaction helper you import directly. Use it when you are writing a function ```typescript import { withTransaction, type Runtime } from '@prisma/orm-postgres/family-runtime'; -async function addTags(runtime: Runtime) { +async function addTags(runtime: Runtime, sql: typeof db.sql) { return withTransaction(runtime, async (tx) => { - await tx.execute(db.sql.public.tag.insert([{ id: crypto.randomUUID(), label: 'wt-commit-1' }]).build()); - await tx.execute(db.sql.public.tag.insert([{ id: crypto.randomUUID(), label: 'wt-commit-2' }]).build()); + await tx.execute(sql.public.tag.insert([{ id: crypto.randomUUID(), label: 'wt-commit-1' }]).build()); + await tx.execute(sql.public.tag.insert([{ id: crypto.randomUUID(), label: 'wt-commit-2' }]).build()); }); } -await addTags(db.runtime()); +await addTags(db.runtime(), db.sql); ``` ### Manual connection and transaction control -The lowest level: acquire a connection, open a transaction on it, commit or roll back yourself, and return the connection to the pool. Use it when you need statements on one connection outside a transaction, such as a `SET` you run with `connection.execute(...)` before you open the transaction. +The lowest level: acquire a connection, open a transaction on it, commit or roll back yourself, and return the connection to the pool. Use it when you need several statements on one connection, such as a read and then a write with no transaction around them. #### Remarks - `runtime.connection()` returns a dedicated connection. `connection.transaction()` opens a transaction on it. Run built queries with `transaction.query(...)` for rows or `transaction.execute(...)` for writes that return no rows, then call `transaction.commit()` or `transaction.rollback()`. Always `release()` the connection when done, to return it to the pool. Commit or roll back first: release it in a `finally` block, as the examples do, so a statement that throws does not leak the connection. -- `connection.destroy()` throws that connection away instead of returning it to the pool. It does not close the pool or the runtime, and the runtime opens a replacement connection on the next query. Use `destroy()` only for a connection you no longer trust. +- `connection.destroy()` throws that connection away instead of returning it to the pool. Use `destroy()` only for a connection you no longer trust. #### Options -These are the methods, not options. - | Method | Type | Description | |---|---|---| | `runtime.connection()` | none | Returns a `Promise` of a dedicated connection. | @@ -521,6 +518,7 @@ These are the methods, not options. const runtime = db.runtime(); const connection = await runtime.connection(); try { + await connection.query(db.sql.public.tag.select('id').limit(1).build()); // a read on this connection const transaction = await connection.transaction(); await transaction.execute( db.sql.public.tag.insert([{ id: crypto.randomUUID(), label: 'manual-commit' }]).build(), @@ -557,7 +555,7 @@ await connection.destroy(); ## Transactions (MongoDB) :::note[MongoDB transactions are not available in Prisma ORM yet] -There is no `db.transaction(...)` on the MongoDB client, and the MongoDB runtime has no `connection()`, `prepare()`, or `telemetry()`: it has `query`, `execute`, and `close` and nothing else. Single-document write operations are atomic on their own. Multi-document transactions through Prisma ORM are planned, not shipped. To run one today, share one `MongoClient` between Prisma ORM and your driver code and use a driver session, as the [Fundamentals transactions guide](/orm/fundamentals/transactions#transactions-on-mongodb) shows. The reading rules under [`AsyncIterableResult`](#asynciterableresult) apply to MongoDB too. +There is no `db.transaction(...)` on the MongoDB client, and the MongoDB runtime has no `connection()`, `prepare()`, or `telemetry()`: it has `query`, `execute`, and `close` and nothing else. Single-document write operations are atomic on their own. Multi-document transactions through Prisma ORM are planned, not shipped. To run one today, create your own `MongoClient`, pass it as the `mongoClient` option when you create the client, and group the writes in `session.withTransaction(...)`, as the [Fundamentals transactions guide](/orm/fundamentals/transactions#transactions-on-mongodb) shows. The reading rules under [`AsyncIterableResult`](#asynciterableresult) apply to MongoDB too. ::: ## Prepared statements (PostgreSQL) @@ -570,9 +568,9 @@ Prepare a statement from a runtime. #### Remarks -- The declaration maps each parameter name to a type id, for example `{ label: 'pg/text@1' }`. A type id is the PostgreSQL type plus a version, always `@1` today, such as `pg/int4@1`, `pg/uuid@1`, or `pg/bool@1`. A few ids carry a longer name, such as `pg/timestamptz-temporal@1` for a `DateTime`. To take one straight from a column, read `db.sql.public.user.columns.id.codecId`, which is that id as a string: `prepare({ id: db.sql.public.user.columns.id.codecId }, ...)`. For the ids the common JavaScript values get, see the [list on the raw queries page](/orm/reference/raw-queries#binding-a-bare-value-with-param). +- The declaration maps each parameter name to a type id, for example `{ label: 'pg/text@1' }`. Use `db.sql.public.
.columns..codecId` to get any column's id. The [table on the raw queries page](/orm/reference/raw-queries#binding-a-bare-value-with-param) lists the common ones. - The callback takes one argument, the declared `params`, and returns the built query, which is what `.build()` gives you. `SqlQueryPlan` in the tables below is the type of a built query. Build it with a SQL builder you already hold, such as `db.sql`. In the `where((f, fns) => ...)` calls below, `f` holds the table's columns and `fns` holds the comparison functions. -- A declared parameter that the callback never uses is rejected at prepare time with an error whose `code` is `RUNTIME.PREPARE_UNUSED_PARAM`, with `details.unused` listing the offending names. The rejection happens at `prepare()`, before any execution. +- A declared parameter that the callback never uses is rejected at prepare time with an error whose `code` is `RUNTIME.PREPARE_UNUSED_PARAM`, with `details.unused` listing the parameter names you declared but did not use. The rejection happens at `prepare()`, before any execution. - The resulting `PreparedStatement` runs via `ps.query(target, params)`; see [`PreparedStatement.query`](#preparedstatementquerytarget-params). #### Options @@ -652,7 +650,7 @@ Run a prepared statement against a target, with the parameter values. #### Remarks -- A query that returns rows prepares into a `PreparedStatement`, which you run with `query(...)`. A statement written as raw SQL with `db.raw.sql` and ending in `.affectedCount()`, which asks for a row count instead of rows, prepares into a `PreparedExecution` instead. You run that with `execute(target, params)`, and it resolves to `{ affectedRows }`, the number of rows the statement changed. See the [raw queries reference](/orm/reference/raw-queries): +- A query that returns rows prepares into a `PreparedStatement`, which you run with `query(...)`. Some queries return a row count instead of rows. End the query with `.affectedCount()`. Preparing one gives you a `PreparedExecution`, which you run with `execute(target, params)`. It resolves to `{ affectedRows }`. See the [raw queries reference](/orm/reference/raw-queries): ```typescript const archive = await runtime.prepare({ label: 'pg/text@1' }, (params) => @@ -662,9 +660,8 @@ Run a prepared statement against a target, with the parameter values. const { affectedRows } = await archive.execute(runtime, { label: 'stale' }); ``` -- You must pass `target`. It can be a runtime, a connection, or a transaction. One prepared statement runs against any of them. +- You must pass `target`. It is the runtime, connection, or transaction the statement runs on. One prepared statement runs against any of them. - A prepared statement has no `close()` method and nothing to dispose. Hold one for as long as you like and reuse it. -- A single `PreparedStatement` prepared once runs against both a runtime target and a transaction target, seeing the transaction's writes when run inside it and the committed state when run against the runtime afterward. #### Options @@ -706,14 +703,13 @@ const committed = await ps.query(runtime, { label: 'ps-both-targets' }); // runs ### `RuntimeExecuteOptions` -Per-query options for cancellation and scope. +Per-query options for cancellation. #### Remarks -- `signal` is an `AbortSignal` for per-query cancellation. A signal that is **already aborted** when you call `query(...)` rejects before any row is fetched, with an error whose `code` is `RUNTIME.ABORTED` and whose `details.phase` is `'stream'`. The error's `cause` is the signal's reason, exactly as you passed it to `controller.abort(...)`. -- An abort that lands after rows have started arriving ends the stream with the same `RUNTIME.ABORTED` error. `details.phase` says where the abort landed: `'encode'`, `'decode'`, `'stream'`, `'beforeQuery'`, `'beforeExecute'`, or `'afterQuery'`. +- `signal` is an `AbortSignal` for per-query cancellation. A signal that is **already aborted** when you call `query(...)` rejects before any row is fetched, with an error whose `code` is `RUNTIME.ABORTED`. The error's `cause` is the signal's reason, exactly as you passed it to `controller.abort(...)`. +- An abort that lands after rows have started arriving ends the stream with the same `RUNTIME.ABORTED` error. `details.phase` says where the abort landed. - Inside `db.transaction(...)` the aborted statement throws, so your callback throws and the transaction rolls back. -- `scope` is set for you. Do not pass it. #### Options @@ -732,7 +728,7 @@ controller.abort(new Error('cancelled')); await runtime.query(db.sql.public.tag.select('id').limit(1).build(), { signal: controller.signal, }); -// rejects with RUNTIME.ABORTED, details: { phase: 'stream' } +// rejects with RUNTIME.ABORTED ``` ### `runtime.telemetry()` @@ -742,7 +738,10 @@ Read telemetry about the most recent query. #### Remarks - PostgreSQL only. `telemetry()` does not exist on the MongoDB runtime. Read it after a query to log how long that query took and whether it succeeded. -- `telemetry()` returns `null` on a freshly-connected runtime, before any query has run. After a query, it returns an object of the shape `{ lane, target: 'postgres', fingerprint, outcome, durationMs? }`. `lane` is the name of the API you used, such as `orm-client` for `db.orm` or `raw` for `db.raw.sql`. Every run of the same query text shares one `fingerprint`, whatever values you pass, so you can group runs of one query together. `outcome` is `'success'` or `'runtime-error'`. `durationMs` is how long the query took, and it is present for every query the runtime ran. The object reflects only the **most recent** query, not a running history. +- `telemetry()` returns `null` on a freshly-connected runtime, before any query has run. After a query, it returns an object of the shape `{ lane, target: 'postgres', fingerprint, outcome, durationMs? }`. The object reflects only the **most recent** query, not a running history. +- `lane` says which API built the query, such as `orm-client` for `db.orm` or `raw` for `db.raw.sql`. +- Every run of the same query text shares one `fingerprint`, whatever values you pass, so you can group runs of one query together. +- `outcome` is `'success'` or `'runtime-error'`. `durationMs` is how long the query took. It is optional in the type, and it is set for every query the runtime ran. #### Return type @@ -767,6 +766,6 @@ const after = runtime.telemetry(); [`all()`](/orm/reference/orm-client#all), [`createAll()`](/orm/reference/orm-client#createall), and the runtime's `query(...)` return an `AsyncIterableResult`. `all()` and `query(...)` read rows, and `createAll()` writes rows and returns the ones it wrote. `await` the result to collect an array, or `for await` it to take rows one at a time. -Use `await` unless you want to handle rows as they arrive. `for await` does not reduce memory on PostgreSQL: every row is loaded first. +Use `await`. Use `for await` only to handle rows as they arrive. On PostgreSQL it does not reduce memory, because every row is loaded first. Pick `await` or `for await` for a given result and do not mix the two. Calling `.toArray()` does the same thing as `await`. Re-`await`ing a result you already awaited is safe and returns the same array. Switching between `await` and `for await`, or looping a second time with `for await`, throws an error whose `code` is `RUNTIME.ITERATOR_CONSUMED`. For the full rules on reading a result, shared identically by PostgreSQL and MongoDB, see [`AsyncIterableResult`](/orm/reference/orm-client#asynciterableresult) in the ORM client reference. diff --git a/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part1.round4.md b/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part1.round4.md new file mode 100644 index 00000000000..05dba115389 --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part1.round4.md @@ -0,0 +1,133 @@ +I read the file once, top to bottom, as a Prisma ORM 7 user seeing this API for the first time. Findings below. + +## Sentences I could not restate after one reading + +**1. Line 11, the "use the pipeline builder when..." sentence.** +> "Use the ORM client (`db.orm`) for everyday reads and writes across models and relations, and the pipeline builder when you need a stage the ORM client doesn't expose: grouping, `$lookup` joins with post-processing, computed projections, multi-stage transformations, or stages that write the results into a collection (`$out` and `$merge`)." + +Too many ideas in one sentence: two tools, five use cases, three new terms. "Post-processing" of what, by what? "Computed projections" means nothing to me yet. Plainer: split it into two sentences and drop the list to three concrete items. + +**2. Line 19.** +> "`Article` and `Tutorial` are variants of `Post`. They are stored in the same `posts` collection and told apart by the value of the `kind` field." + +"Variant" is a new word the page uses as if I know it. The schema then shows `@@discriminator(kind)` and `@@base(Post, "article")`, neither of which is explained anywhere on this page. I can guess, but I had to. + +**3. Line 95.** +> "Passing an unknown collection name throws right away, with an error whose `code` is `ORM.MODEL_UNKNOWN` and whose message is `Unknown root: "". Valid roots: ...`." + +"Root" appears here for the first and only time and is never defined. The page has spent a paragraph telling me the argument is a *collection name*, then shows an error that calls it a *root*. I could not tell whether "root" is a third concept. + +**4. Line 136.** +> "**After `lookup()` or `group()`, compare ids with `String(a._id) === String(b._id)`**, where `a` is a looked-up sub-document and `b` is the parent document it came from." + +This sits in "Building and executing a pipeline", before `lookup()` or `group()` have been introduced. I do not yet know what a "looked-up sub-document" is. The letters `a` and `b` are not in any example on the page, so I had to invent the situation the rule applies to. + +**5. Line 137.** +> "**Read results are decoded.** If every stage in your pipeline is one of `match()`, `sort()`, `limit()`, `skip()`, `sample()`, `project()`, `addFields()`, and `vectorSearch()`, results are decoded: `_id` is a hex string and dates are your contract's date type. If any stage is not in that list, including `lookup()` and `group()`, every document comes back raw." + +I understood the rule, but not "your contract's date type" — my contract does not declare a date type anywhere I can see, and `createdAt DateTime` in the schema does not tell me what I get back. This is also the most consequential rule on the page and it is a bullet in the middle of a list. + +**6. Line 159.** +> "Neither a hex string nor a driver `ObjectId` matches in `match()`. The builder sends the value as a plain object, so an `ObjectId` arrives as its bytes." + +Two problems. "The driver" is never introduced — I inferred the MongoDB Node driver. And the second sentence explains the tool's insides to justify a rule; I only need the rule. + +**7. Line 161.** +> "To compare computed values rather than a field against a constant, wrap the comparison in `expr(...)`... `fn` holds the helpers that build expressions, and `expr()` wraps one so `match()` accepts it. The `fn.*` helpers take expressions, not plain values, so wrap every constant in `fn.literal()`." + +Three new things at once (`fn`, `expr`, `fn.literal`) inside a remark about `match()`. I could copy the example but I could not have written a second one. I still cannot state the rule for when I need `expr()` versus when I do not. + +**8. Lines 642–646, the "Call site / What you pass" table.** + +This table is dropped inside the `redact()` section, under `redact()`'s remarks, with no heading, no introductory sentence, and no subject. It is plainly about `fn.*` values in general, not about `redact()`. I did not know what it was describing until I had read all three rows. Worse, it is the first mention of `.node`, and `.node` is never defined on this page — the table tells me when to type it but not what it is. + +**9. Line 683.** +> "`granularity` picks the bucket size from one of MongoDB's preferred-number series." + +"Preferred-number series" is a term I do not know, and the page gives no values. I have no idea what to type. + +**10. Line 686.** +> "`setWindowFields({ partitionBy?, sortBy?, output })` computes a value for each document from the documents around it, such as a running total. ... `output` is an object mapping each new field name to `{ operator, window? }`, where `operator` is the accumulator or expression to compute." + +I understand the purpose sentence. I cannot picture the `output` object. `{ operator }` — is `operator` a string, an `acc.*` value, an `fn.*` value, a `.node`? No example. + +**11. Line 692.** +> "Each stage class takes the same arguments as the MongoDB stage it builds." + +Twenty-eight class names are listed, then this sentence tells me to go work out the arguments myself from MongoDB's docs. Only three are then documented. This is a list, not an explanation. + +**12. Line 750.** +> "These stages need MongoDB Atlas. The query builds, and MongoDB rejects it when it runs." + +I had to read twice. Plainer: "Nothing stops you from building these against a non-Atlas database. The error only appears when you run the query." + +## Words and phrases I had to guess + +- **"variants"** (line 19) — guessed: subtypes of one model stored in one collection. +- **"root"** (line 95) — guessed: the same thing as the collection you pass to `from()`. +- **"the driver"** (lines 136, 137, 159) — guessed: the official MongoDB Node.js driver, underneath Prisma. +- **"decoded" / "raw"** (line 137) — guessed: converted to friendly JS types versus handed back as the driver produced them. +- **"your contract's date type"** (line 137) — guessed: whatever `DateTime` maps to, probably `Date`. +- **"plan"** (line 123 onwards) — guessed: just the variable name for what `build()` returns. The page says as much at line 129, but the name appears three sections earlier. +- **"raw expression"** (lines 682, 683, 684, 685, 686, 688) — used six times before line 691 tells me how to make one. Guessed: a MongoDB expression object, not a typed builder value. +- **`.node`** (lines 646, 670, 691) — guessed: the plain MongoDB object inside an `fn.*` wrapper. +- **"document-shaped expression"** (line 539) — guessed: an expression that evaluates to an object. +- **"option-object stages"** (line 646) — used in the table before the section of that name exists, at line 676. +- **`@@type("mongo/string@1")`** (line 27) — guessed: how the enum is stored. No idea what `@1` means. +- **"post-processing"** (line 11) — guessed: more stages after the join. + +## Places I asked "so what do I actually type?" + +1. **Line 97, using the builder without a client.** + > "You can build the same pipelines without a client. `import { mongoQuery } from '@prisma/orm-mongo/query-builder'`, then `mongoQuery({ contractJson }).from('posts')`." + + It tells me how to *build* a pipeline with no client, and never tells me how to *run* one. If there is no client there is no `runtime.query`. So what do I do with the result? No example, no code block, one line in a bullet. + +2. **`build()` itself.** Line 9 says I "finish the chain with the call that builds the query". Every table says "then finish with `build()`". But `build()` has no entry of its own, no options table, no return-type table, unlike every other method on the page. What does it return? Can I inspect it? Can I log the pipeline it produced? + +3. **Line 686, `setWindowFields`.** No example anywhere. I cannot type this. + +4. **Lines 687–688, `densify` and `fill`.** `{ method }` — which methods? No values given. `bounds` gets its three values spelled out; `method` gets nothing. + +5. **Line 683, `granularity`.** No values. + +6. **`search()` and `vectorSearch()`.** Lines 751–753 describe the arguments in prose; there is no code example for any of the three Atlas stages. The `search()` call only appears squeezed into a table cell at line 759. + +7. **Line 691 contradicts the `bucket()` example.** The bullet says "Build a raw expression with the `fn.*` helpers and pass its `.node` property, or name a field with `MongoAggFieldRef.of('duration')`." The example at line 735 passes `MongoAggFieldRef.of('duration')` with no `.node`. So does `MongoAggFieldRef` need `.node` or not? I would have guessed wrong. + +8. **Line 459, `unwind()`.** + > "This example needs a `tags String[]` field on `Post`, which the example schema does not have." + + The one stage where I most wanted to copy a working example is the one I cannot run. Add `tags` to the schema. + +9. **Example output comments assume data I never saw.** Line 629 says "two buckets, each `{ _id: , count: 1 }`" and line 722 says "facets.totalCount is `[{ count: 2 }]`". The page shows a schema but never shows the documents. I cannot check my results against these. + +10. **Three different import paths** — `@prisma/orm-mongo/runtime`, `@prisma/orm-mongo/query-builder`, `@prisma/orm-mongo/query-ast/execution` — with no statement of which things come from which. I had to scan every code block to work out that `fn`, `acc`, `expr` are in one and the `Mongo*Stage` classes in another. + +11. **`count()` collides with `acc.count()`.** The page documents a `count()` stage at line 574 and an `acc.count()` at line 505 without ever saying they are different things. I would have typed the wrong one. + +12. **`lookup()` with no match.** Line 360 says the joined documents arrive in an array. It does not say what happens when nothing matches — empty array, or missing field? + +## Places that explain the insides when I only wanted the instruction + +- **Line 159.** "The builder sends the value as a plain object, so an `ObjectId` arrives as its bytes." I do not need to know how the value is serialized. The rule I need is in the warning at line 200. +- **Line 639.** "It prefixes `$` to the string you give it, so `f.rawPath('$KEEP')` reaches MongoDB as `$$KEEP`." Here the internal detail is unavoidable, because the string I type does not look like the string MongoDB sees. But the surrounding sentences — "There is no expression helper for a `$$` variable, and `fn.literal(...)` cannot stand in. The server rejects both `fn.literal('KEEP')` and `fn.literal('$$KEEP')`" — tell me about two things that do not work before telling me the one that does. +- **Line 692.** The full list of 28 internal class names is the implementation surfacing into a reference page. I need the ones I can use in `facet` and `unionWith`, with their arguments. +- **Line 137.** The decode rule is stated as a fact about the library's behaviour with no instruction attached. What do I do about it? Change my comparison code? Convert manually? The page tells me the mechanism and leaves the action to me. + +## Could I do what the page is for, after one reading? + +Partly. I could write and run a pipeline that filters, sorts, limits, projects, groups, and joins — those five have clear examples I could adapt, and the `groupBy` diff at line 526 is the single most useful thing on the page for someone coming from version 7. + +What I would still not know: + +- What `.node` is, and therefore when to type it. The table at line 642 gives me three rules for a thing the page never names. +- How to run a pipeline built with `mongoQuery` and no client. +- How to write `setWindowFields`, `densify`, `fill`, `graphLookup`, `geoNear`, `search`, `searchMeta`, or `vectorSearch`. Eight stages are described only in prose bullets with no code. +- What arguments any of the 28 stage classes take, beyond the three used in one example. +- Whether `MongoAggFieldRef.of(...)` needs `.node`. +- What "your contract's date type" is, so I do not know what my dates will actually be. +- What to do about the decode rule beyond being warned it exists. +- What `@@discriminator`, `@@base`, and `@@type("mongo/string@1")` mean in the schema I am told all examples run against. + +The biggest single defect is the table at lines 642–646. It is general guidance about `fn.*` values, filed under `redact()`, introducing `.node` with no definition and referring forward to a section that does not exist yet. I would have missed it entirely if I had jumped to the stage I needed, which is how people read reference pages. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part2.round4.md b/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part2.round4.md new file mode 100644 index 00000000000..6417be71541 --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part2.round4.md @@ -0,0 +1,112 @@ +Read once, top to bottom, as a Prisma 7 user who has never seen this builder. + +## Sentences I could not restate after one reading + +**1. Line 108, the rename table's last row.** +> `| $first` and `$last` | `fn.firstElem` and `fn.lastElem` | none | + +The column is headed "Why". The answer is "none". So two helpers were given different names for no reason? That reads as a mistake or a joke. The other four rows all give a real reason. I'd guess the real reason is a collision with `acc.first`/`acc.last`, but the page will not say. Plainest wording: say the actual reason, or drop the row's "Why" cell rather than writing "none". + +**2. Line 95, the definition of an expression.** +> "An expression is an `f.field` value or an `fn.*` value, never a plain JavaScript value. Stage callbacks and these helpers take expressions. The update operators on the [write methods](#write-methods) and the `MongoFieldFilter` methods take plain JavaScript values instead." + +Three rules in three clauses, and the third names two things (`MongoFieldFilter`, write-method update operators) that have not appeared yet. I could not hold it. Plainest wording: "Inside a stage, write `f.title` or `fn.toUpper(...)` — never `'hello'` or `3` on their own; wrap constants in `fn.literal('hello')`. Update operators and `MongoFieldFilter` are the opposite: they take ordinary JavaScript values, so write `f.bio.set('hello')`, not `f.bio.set(fn.literal('hello'))`." + +**3. Line 125, the `.node` paragraph.** +> "Every `fn.*` helper returns a typed wrapper, and `.node` is the raw MongoDB expression inside that wrapper. Arguments typed as an expression take the wrapper. Arguments typed as a raw MongoDB expression take `.node`, and those are the condition of `fn.cond()`, `redact()`, and the option-object stages..." + +I cannot apply "Arguments typed as an expression take the wrapper" to code I am writing, because nothing on the page tells me how to look at an argument and see which type it is. I only ever learn it from the list that follows. The general rule is unusable; the list is the whole answer. Cut the general rule and keep the table. + +**4. Line 130, the `.node` table's second row.** +> "The condition argument of `fn.cond()`, and `f.stage.set()`, `f.stage.replaceRoot()`, `f.stage.replaceWith()`" + +`f.stage.*` is not defined until line 378, roughly 250 lines later. On first read this row is unresolvable. Also the row is ambiguous: does "the condition argument of" apply to all four items, or only to `fn.cond()`? (It turns out to be only `fn.cond()`, but I had to reach line 416 to be sure.) + +**5. Line 161, the named-arguments remark.** +> "Some helpers take one object of named arguments rather than positional ones: `fn.dateDiff({...})`, `fn.dateTrunc({...})`, `fn.dateAdd({...})`, ... and `fn.convert({ input, to, onError?, onNull? })`." + +Eleven signatures inside one sentence. I skimmed it and retained nothing. This is a table, not a sentence. + +**6. Line 162.** +> "So does every other helper not named above, in the order given for that operator in [MongoDB's aggregation operator reference]." + +I can restate this, but what it actually says is: for about sixty of the helpers in your table, this reference page will not tell me the arguments. I have to leave and read MongoDB's docs. That is worth saying plainly rather than burying it in a remark. + +**7. Line 163.** +> "To turn a string id from your own code into an `ObjectId`, build the `ObjectId` yourself and put it in a raw pipeline with [`rawCommand()`](#rawcommand). The last example on this page does exactly that." + +The last example does not do that. It does `new ObjectId(postId)` in a `$match`. It never converts an id inside a pipeline, which is what `fn.toObjectId` is for. So I could not tell whether the cross-reference was wrong or whether I had misunderstood `fn.toObjectId`. + +**8. Line 236.** +> "Write methods are available at three points: on the root collection, after a `match()` filter, and as the last call in a pipeline." + +"As the last call in a pipeline" meant nothing until I reached `out()` and `merge()` 180 lines later. The first two points name the position; the third does not name the methods. Plainest wording: "...and at the end of any pipeline, where `out()` and `merge()` write the output into a collection." + +**9. Line 242.** +> "Pipeline form calls `f.stage.*`, and can read other fields of the same document." + +"Can read other fields of the same document" is the entire reason to choose pipeline form, and it is one trailing clause. I did not understand what it bought me until I saw `f.stage.set({ bio: f.name.node })` at line 416. + +## Words and phrases I had to guess + +- **"option-object stages"** (line 125). Guessed: stages whose argument is a single options object rather than a callback. The page lists five of them (`bucket()`, `bucketAuto()`, `graphLookup()`, `setWindowFields()`, `fill()`), so I could work out the membership, but never the definition. +- **"typed wrapper"** (line 125). Guessed: an object the builder makes, whose real contents sit in `.node`. This is the tool's internals, not something I need. +- **"combinators"** (line 159). Guessed: the `eq`/`gt`/`in` filter methods. The word is used as if I already know it; the link is to a different page. +- **"`Mongo*Stage` classes, one per MongoDB stage"** (line 202). Guessed: `$unwind` becomes `MongoUnwindStage`. The page never lists them or states the naming rule outright, so I am guessing a class name and hoping the import resolves. +- **"read terminals"** / **"pipeline-write-terminals"** (anchors at lines 222 and 421). Guessed: "terminal" means the call that ends the chain. The word appears only in the anchors, never in the prose, but it is what shows up in the URL I share with a colleague. +- **"decoded"** applied to `pipe()** (line 204): "the rows come back exactly as MongoDB returns them, whatever the TypeScript type claims." Guessed: adding `pipe()` means the pipeline is no longer all-eight-approved-stages, so decoding stops for the whole query. The page does not connect this back to the eight-stage rule, and does not say whether the loss applies to the whole result or only to fields the raw stage touched. +- **"the driver"** (lines 204, 311, 312). Guessed: the `mongodb` npm package. Never introduced. +- **`f.name.node`** (line 416). The `.node` rules at lines 125-131 are all about `fn.*` values. Here `.node` is on a plain field reference. I guessed that field references also carry `.node`, but the rule as written does not cover this case. + +## Places where I asked "so what do I actually type?" and got no answer + +1. **Argument order for most helpers** (line 162). The table at 112-123 lists roughly a hundred helpers. Apart from the eleven object-argument ones and `slice`/`range`, the page tells me to go read MongoDB's docs. So this reference does not let me write `fn.dateDiff` — sorry, `fn.substr` — without leaving it. + +2. **OR/AND/NOT inside `project()` or `addFields()`** (line 160): "Write that stage yourself with `pipe()`, or write the whole command with `rawCommand()`." No example. I have a concrete need (`$or` inside a computed field) and two pointers, neither of which shows the code. + +3. **`MongoAndExpr`** (line 158) is named but never imported or shown. Only `MongoOrExpr` has an example. I assume the import path is the same, but I am assuming. + +4. **Which `Mongo*Stage` class do I want?** (line 202). Two are shown (`MongoMatchStage`, `MongoCountStage`). There is no list and no stated naming rule, so for any third stage I am typing a name and hoping. + +5. **`whenMatched` as an array of update stages** (line 429): "`MongoAddFieldsStage`, `MongoProjectStage`, and `MongoReplaceRootStage` objects". No constructor signatures, no example. I cannot type `new MongoAddFieldsStage(...)` from this. + +6. **Can I put `sort()` between `match()` and a write?** Line 311 tells me to "use `findOneAndUpdate()` after a `sort()` when the choice matters", but the section is titled "Writes after `match()`", every example is `match()` then write, and line 313 says `skip()` blocks these methods. I do not know whether the chain is `.match(...).sort(...).findOneAndUpdate(...)` or something else, and no example shows it. + +7. **What fields must an insert document have?** Line 253 says "TypeScript requires no particular field, and the builder does not check the record against your contract", yet the example at 265 passes `bio: null` and `address: null` explicitly. Can I leave them out? Should I? The page says the builder won't stop me and then models the opposite. + +8. **The two examples I cannot run.** Line 44 ("on a collection with a numeric `views` field") and line 409 (`f.views.inc(1)`, `f.tags.push('mongodb')`) both invent fields the example schema does not have. Every other example uses `users` and `posts`. For `inc`, `push`, `pop`, `pull`, `pullAll`, and `addToSet` — six of the fourteen operators — there is no runnable example at all. + +9. **Mixed argument conventions inside one accumulator call** (lines 35-36, 85-86). In `acc.top({ output: f.title, sortBy: { createdAt: -1 } })` I use the `f` argument for `output` and a bare string key for `sortBy`. In `acc.firstN({ input: f.title, n: fn.literal(2) })` I use `f` for `input` and `fn.literal` for a number. Three conventions in two lines. Line 36 flags the `n` rule but nothing explains why `sortBy` keys are bare strings when everything else goes through `f`. + +10. **`MongoFieldFilter.eq('kind', 'tutorial')`** (line 148) takes the field name as a string. Everywhere else I write `f.kind`. The page never says that building a filter by hand costs me the typed field reference and the spell-checking that comes with it. I would want that said out loud, because it is the trade I am making. + +11. **`neq` vs `ne`.** `MongoFieldFilter` has `neq` (line 159). `fn.*` has `ne` (line 118). Two spellings of the same idea, on the same page, uncommented. I will get this wrong. + +12. **`setOnInsert(value)`** (line 398): "A value written only when an upsert inserts a new document." So it only does anything inside `upsertOne`. The page does not say that, and the `upsertOne` examples do not use it. + +13. **`fn.toObjectId` throws** (line 163) but, unlike every other failure on the page, no error `code` is given. Elsewhere I get `ORM.MUTATION_DATA_MISSING`, `ORM.ARGUMENT_INVALID`, `ORM.OPERATION_UNSUPPORTED`. Here I just get "throws", so I cannot recognise it in a log. + +14. **Line 256** points at "the `match()` warning" for the `_id` rule, and line 457 does the same. Both links go to `#match`, which is in the half I do not have. Inside this page the rule appears twice as a pointer and once as an example, but never as a statement of what actually goes wrong if I try `f._id.eq(someString)`. + +## Places that explain the machinery when I only wanted the instruction + +- **Line 125**: "Every `fn.*` helper returns a typed wrapper, and `.node` is the raw MongoDB expression inside that wrapper." I do not need to know there is a wrapper object. I need to know: type `.node` in these four situations, and nowhere else. The table right below already does that job. +- **Line 125 again**: "Arguments typed as an expression take the wrapper. Arguments typed as a raw MongoDB expression take `.node`." This describes the type signatures the library authors wrote. It is not actionable, because I cannot see those signatures while typing. +- **Line 204**: "After a `pipe()` stage the rows come back exactly as MongoDB returns them, whatever the TypeScript type claims." The useful half is the second clause — the types lie to you. The framing is about how the builder's decoding step is implemented. +- **Line 311**: "Which document `updateOne` / `deleteOne` picks is up to the driver." Correct but unhelpful. What I need is: do not use `updateOne` when it matters which one; use `findOneAndUpdate` after a `sort()`. The sentence after does say that, so the driver sentence earns nothing. + +## Could I do what the page is for, after one reading? + +Partly. I could write a `group()` with the common accumulators, do a simple `project()` with `fn.concat`/`fn.toUpper`, and do all the basic writes — `insertOne`, `insertMany`, `updateMany`, `deleteMany`, `upsertOne`, `findOneAndUpdate`. Those have real, runnable, complete examples, and the "return an array" rule at line 238 is stated firmly enough that I would not get it wrong. + +What I still would not know: + +- The arguments to most of the ~100 expression helpers. The page hands that back to MongoDB's docs. +- Whether to write `.node`, in any situation not literally listed at lines 129-132. The rule is a list, not a principle, so the first unlisted case stops me. +- How to write an `$or` inside a `project()`. I know I must use `pipe()` or `rawCommand()`, and that is all I know. +- Which `Mongo*Stage` class to import for any stage other than match and count. +- How to build the update-stage objects `merge({ whenMatched: [...] })` accepts. +- Whether `sort()` is allowed between `match()` and `findOneAndUpdate()` — which line 311 tells me to do and no example shows. +- How to use `inc`, `push`, `pop`, `pull`, `pullAll`, or `addToSet` against the schema the page uses, since the only examples for them reference fields that do not exist. + +The two structural problems, if you only fix two things: line 161's eleven signatures should be a table, and line 162 should not outsource argument order for most of the helper list to another site. A reference page that cannot tell me the arguments is not yet a reference page. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/raw-queries-and-index.round4.md b/docs/orm-docs-audit/c21-orm-reference/raw-queries-and-index.round4.md new file mode 100644 index 00000000000..ec9aef8ead1 --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-reference/raw-queries-and-index.round4.md @@ -0,0 +1,146 @@ +I read both pages once, top to bottom, as a Prisma ORM 7 user who has never seen version 8. + +# Page 1 — `index.mdx` (reference landing page) + +## Sentences I could not restate after one reading + +**1. The whole of line 15.** This is one paragraph carrying eight separate ideas: + +> "Plus raw SQL and raw commands when none of them fits. See [Raw queries]... For connecting and disconnecting the client, transactions, and prepared statements, see [Transactions and runtime]... In the example below, `db.runtime()` returns the connection that runs a built query. Create it once and reuse it. `.all()` runs the query and returns every row. It is what replaced `findMany`. The ORM client runs when you `await` it. The SQL query builder and the pipeline builder do not run on their own. Each produces a built query with `.build()`, which you then run with `runtime.query(...)`. Watch the casing: `User` is the model name and `user` is the table name." + +What stopped me: two link paragraphs, a definition of `runtime`, a definition of `.all()`, a migration note, an execution-model rule, and a naming warning are all in one block with no headings. I had to re-read it three times to find the one rule that matters, which is "the ORM client runs on `await`, the two builders need `.build()` then `runtime.query()`." + +Plainer: split it. One short paragraph for "when to use raw queries and transactions, with links." Then a small section titled "How a query runs" containing only: the ORM client runs when you `await` it; the SQL query builder and pipeline builder return a built query from `.build()`, which you run with `runtime.query(...)`; get `runtime` from `db.runtime()` once and reuse it. Then a one-line callout for the casing rule. + +**2.** > "it returns results that are not model rows, such as an aggregate over a join." + +"Model rows" is not defined anywhere on this page. I guessed it means "objects shaped like your model, the kind `db.orm` gives back." Plainer: "it returns whatever shape your `select()` asks for, not objects shaped like your models." + +**3.** > "`db.query` builds MongoDB pipelines. The separate `runtime.query()` method is what runs any built query." + +The word "separate" told me these two are being confused on purpose, but it did not tell me why I should care. I could not restate what problem this sentence is solving. Plainer: "Two different things are both named `query`. `db.query` builds a MongoDB pipeline. `runtime.query(...)` runs a query that has already been built." + +## Words and phrases I had to guess + +- **"built query"** — used three times before anything defines it. I guessed it means a plain object describing the query that has not been sent to the database yet. +- **`plan`** — the code names the variable `plan`, but the prose only ever says "built query." I guessed they are the same thing. The page never says so. +- **"the connection that runs a built query"** — is `runtime` a database connection, a connection pool, or a wrapper? "Create it once and reuse it" implies it is expensive, which implies a pool. I guessed pool. +- **`(u) => u.kind.eq('admin')` versus `(f, fns) => fns.eq(f.kind, 'admin')`** — two callback styles appear on consecutive lines in the same code block, and the page does not mention that they differ. I guessed the ORM client puts the comparison on the column and the SQL builder puts it in a helper object. I would not have been able to write either one from memory. +- **"typed"** in "three typed ways to write a query" — I guessed it means TypeScript checks the column names and result types. + +## Places I asked "so what do I actually type?" + +- **MongoDB.** The page says the SQL query builder is "PostgreSQL only" and that `db.query` "builds MongoDB aggregation pipelines," but the only code block is PostgreSQL. `const runtime = db.runtime();` is shown with no `await`. On MongoDB that line is wrong — I only found that out on the raw-queries page, at line 25. If I were a MongoDB user, I would have copied the broken line from the landing page. +- **Is `db.query` available on PostgreSQL?** The page says what it builds. It never says it does not exist on PostgreSQL, the way it does for `db.sql`. I do not know what happens if I type it. +- **`runtime.execute`** is never mentioned. The landing page implies `runtime.query(...)` runs everything. The raw-queries page then reveals a second method for statements that return no rows. The landing page's model of "how a query runs" is incomplete, and I did not know that until the next page. +- **`.all()`.** It is introduced as what replaced `findMany`. Do the SQL query builder and pipeline builder have `.all()` too? The example uses `.build()` for those instead, so I guessed no, but the page does not say. + +## Places it explains internals when I wanted instructions + +Only one, and it is mild: "The SQL query builder and the pipeline builder do not run on their own." That is a fact about the design. The instruction I needed is "call `.build()`, then `runtime.query(...)`," which arrives in the same sentence but second. + +## Could I do what the page is for, after one reading? + +Mostly. Its job is to send me to the right sub-page, and I can do that: everyday reads and writes go to `db.orm`, PostgreSQL joins and aggregates to `db.sql`, MongoDB pipelines to `db.query`. What I would still not know: how to get a `runtime` on MongoDB, that `runtime.execute` exists, and how to write either callback style without copying the example. + +# Page 2 — `raw-queries.mdx` + +## Sentences I could not restate after one reading + +**1.** > "A type id such as `pg/text@1` tells Prisma ORM what JavaScript type a column should become. In code, the property is called `codecId`." + +Two names for one thing, introduced in consecutive sentences, with no reason given. I still do not know what a "codec" is here or why the property is not called `typeId`. Plainer: pick one name and use it everywhere; if the property really is `codecId`, say "the property is named `codecId` for historical reasons" or just call the thing a codec id throughout. + +**2.** > "A date column arrives as whatever the `pg` package produces for it, with no conversion by Prisma ORM." + +This is the sentence I most needed an answer from, and it refuses to give one. I do not know what I get. A `Date`? A string? The page tells me to go read another project's documentation to find out. Plainer: say the actual type, for example "a `timestamptz` column arrives as a JavaScript `Date` in local time." + +**3.** > "Never put user input into the SQL text itself, because a column or table name cannot be a parameter." + +The reason does not support the rule. User input should never go into SQL text because of SQL injection. The fact that identifiers cannot be parameters is a *different* fact, and it is the reason you might be tempted to. Reading this once, I thought the page was saying user input is fine as long as it is not a column name. Plainer: "Interpolated values are sent as query parameters, so they are safe. Column and table names cannot be parameters, so they would be pasted into the SQL text. Never build an identifier from user input." + +**4.** > "A type id is the PostgreSQL type name plus a version, always `@1` today." + +This contradicts the table directly above it. `pg/int8number@1` and `pg/timestamptz-temporal@1` are not PostgreSQL type names. I could not restate the rule because the rule is wrong. Plainer: "A type id is `pg/`, a name, and a version. The name is usually the PostgreSQL type, sometimes with a suffix saying which JavaScript type you get back." + +**5.** > "You can nest a `.returnsRow()` query inside another. That is how you write a subquery or a CTE. Interpolate it before `.build()`, and call `.build()` on the outer one only. Its `.returns` property holds its declared columns, so `outer.returnsRow({ postCount: inner.returns.postCount })` reuses a declaration. Three names look alike. `.returns(typeId)` is a method on a fragment, `.returnsRow(spec)` is a method on a statement, and `.returns` is a property on the object `.returnsRow()` gives you. You cannot nest a statement that ends with `.affectedCount()`, because it produces no rows. You can nest an `UPDATE ... RETURNING`, because it does." + +Six unrelated rules in one paragraph. The "three names look alike" list is genuinely useful and is buried in the middle of it. "CTE" is never expanded. Plainer: make the three-names list its own callout, and give nesting its own short paragraph. + +**6.** > "Each of these nine methods builds a command. Call `.build()` on it, then run it with `runtime.query(built)`, the inserts, updates, and deletes included." + +The trailing clause reads like an afterthought and carries the surprise. On PostgreSQL the page just told me that statements returning no rows go through `runtime.execute(...)`. Here, deletes go through `runtime.query(...)`. Nothing explains why the rule flips. Plainer: "Run every one of these with `runtime.query(...)`, including the writes. `runtime.execute(...)` is PostgreSQL only." + +**7.** > "The write methods give you its own result objects, `{ insertedId }`, ... and you read those keys directly." + +"its" points back to "the `mongodb` package" two sentences earlier. On one reading I read "its" as pointing to "the write methods" and stalled. + +**8.** > "The **first** call to an upsert counter returns an **empty** result, because there is no earlier version. Read the counter value on the second call and later ones, each of which returns the version before its own increment." + +I had to read this three times. The behavior is simple but the wording is not. Plainer: "`findOneAndUpdate` returns the document as it was *before* your update. The first call creates the document, so there is nothing to return and you get `[]`. The second call returns `count: 1`, which is the value before that call added one." + +Also, the example undercuts itself: > "The example schema has no counter collection, so `users` stands in for one here, with the plain string `'pageViews'` as its `_id`." The warning two sections earlier told me to build ids with `ObjectId`. Now the example uses a plain string as `_id` with no comment. I could not tell whether that was allowed on purpose or an artifact of the stand-in. + +## Words and phrases I had to guess + +- **"spec"** in `.returnsRow(spec).build()` (line 19). Introduced with no definition. It is defined at line 136, over 100 lines later. On first reading I guessed it was a schema object. +- **"projection"** in the heading "`fns.raw` in a projection". Never used again on the page. I guessed it means the `select()` list. +- **`Contract`, `contract.d`, `contract.json`** — the code imports all three. I know what a contract is, but not that it compiles into two generated files, nor where they come from. I guessed a code-generation step I have not been shown. +- **`Temporal.Instant`** — described as "the standard JavaScript object for a point in time." It is used at line 99 with no import and no note about runtime support. I guessed my Node version has it. That guess may well be wrong, and the page gives me nothing to check against. +- **"codec"** — see above. Guessed: the thing that converts between a PostgreSQL wire value and a JavaScript value. +- **"CTE"** — I know it, but it is never expanded, and it is the only unexpanded acronym on the page. +- **"native BSON values"** — guessed: the raw objects the MongoDB driver produces, without any Prisma conversion. +- **`pg/int8number@1` versus `pg/int8@1`** — one reads back as `number`, the other as `bigint`. I guessed I choose based on whether I want to risk precision loss. The page never says when to pick which. + +## Places I asked "so what do I actually type?" + +1. **Finding a type id for a column that has no contract match.** > "The ids your contract uses are in `contract.json`." Where in it? What key? There is no path, no example fragment, no shape. This is the single most likely thing I will need, and the page points at a file and walks away. `db.sql.public.
.columns..codecId` is given for columns I *do* have, which is the easier case. + +2. **`Temporal`.** Do I import it? Enable a flag? Install a polyfill? The `param` and `ObjectId` imports are both spelled out. `Temporal` is not. + +3. **Reading documents raw on MongoDB.** There are nine methods and none of them is `find` or `findOne`. I wanted a plain raw read. I eventually worked out that `aggregate()` with a `$match` is the answer, but the page never says "there is no raw `find`; use `aggregate`." + +4. **What happens when a raw query fails.** The page documents exactly one error, `RUNTIME.RAW_SQL_UNSUPPORTED_INTERPOLATION`. Nothing about a SQL syntax error, a constraint violation, or a `.returnsRow()` spec that does not match the real columns. If my spec is wrong, do I get a run-time error or silently wrong values? I do not know. + +5. **Transactions on MongoDB.** PostgreSQL gets `tx.query(...)` and `tx.execute(...)` at line 157. The MongoDB half says nothing. I do not know whether raw MongoDB commands can run inside a transaction. + +6. **`.returnsRow()` when a column can be null.** Fragments get `{ codecId, nullable }` with its own section. For a whole statement's spec, entries are "either a column from your contract or a type id." A bare type id has no nullability. What do I type for a nullable column with no contract match, such as a `LEFT JOIN` column? Not covered. + +## Places it explains internals when I only wanted to know what to do + +1. > "If you get past the type check, say by casting the object to `any`, those keys are ignored at run time with no error." (line 289) + +This tells me how the implementation behaves when I defeat its own type checking. I did not want to know. The instruction I wanted was the next sentence: use `RawFindOneAndUpdateCommand`. + +2. > "runs a raw MongoDB aggregate command through the [pipeline builder], with no translation of the command you pass." (line 335) + +"Through the pipeline builder, with no translation" is about plumbing. What I need is the three practical facts, which are in the *rest* of the paragraph: it is already built so do not call `.build()`, rows come back as `unknown`, and you cast them. + +3. **`RawFindOneAndUpdateCommand` in general.** Being sent to `@prisma/orm-mongo/query-ast/execution` to hand-build a command object with six positional arguments, because the friendly method deliberately refuses `sort` and `returnDocument`, is the tool's internal shape showing through. The page even needs a `// collection, filter, update, upsert, sort, returnDocument` comment above the call so I can read my own code. I do not know whether that import path is supported or will break on the next release. + +4. > "`.returns(...)` only tells TypeScript what to expect. It does not cast the value in SQL and it does not change how the `pg` package reads it." + +Three sentences about what a method does *not* do, and no sentence about what to type when I actually want the conversion. The answer, use `.returnsRow()` instead, is somewhere else. + +5. > "`rawCommand()` is the way out when the typed pipeline builder can't express a query, such as filtering by `_id` equality." + +A significant limitation of a different API is disclosed in a subordinate clause on this page. If the pipeline builder cannot filter by `_id` equality, I want to know that in bold, on the pipeline builder page, not as an example here. + +## Could I do what the page is for, after one reading? + +**PostgreSQL: yes, for the common cases.** I could write a `fns.raw` fragment in a `select()`, use one as a `where()` predicate, and run a whole `db.raw.sql` statement with `.returnsRow()`. The examples are complete and I could copy them. + +**What I would still not know on PostgreSQL:** +- What JavaScript value I actually get for a date, a `numeric`, or a `json` column from a fragment's `.returns()`. The page tells me it does not convert and names no types. +- How to find a type id for a column my contract has no match for. I was pointed at `contract.json` with no instructions. +- Whether `Temporal` works in my runtime or needs anything installed. +- What happens when a `.returnsRow()` spec does not match the real result columns. +- How to declare a nullable column inside a `.returnsRow()` spec. + +**MongoDB: partly.** I could copy the insert, update, and delete examples and they would probably work. But I would not know: +- That there is no raw `find`, and that `aggregate` is the substitute. +- Why writes go through `runtime.query` here when they go through `runtime.execute` on PostgreSQL. +- Whether raw commands work inside a transaction. +- Whether a plain string `_id` is genuinely allowed, or just an artifact of the counter example borrowing the `users` collection. + +The two biggest structural problems, in order: the type-id system is introduced through its restrictions and its two names rather than through a straight answer to "what type do I get back," and the landing page's line 15 buries the one execution rule a reader needs under seven other ideas. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part1.round4.md b/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part1.round4.md new file mode 100644 index 00000000000..2ea9e3314a4 --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part1.round4.md @@ -0,0 +1,75 @@ +Read once, top to bottom, as a Prisma 7 user. + +## Sentences I could not restate after one reading + +**Line 19** — "No example uses `Address`, `Task`, `Bug`, or `Feature`. No example on this page uses `types`, `@@type`, `@@discriminator`, or `@@base`." +What stopped me: not a word, but the point. You gave me a schema containing five things, then told me none of them are used. So why are they there? I spent time reading `types { Embedding1536 = pgvector.Vector(1536) }`, `@@discriminator(type)`, and `@@base(Task, "bug")` — all new to me — before being told to ignore them. Plainest version: cut the unused models from the example schema. If they must stay, say "the schema below is the full example schema used across the reference pages; this page only uses `user`, `post`, `post_tag`, and `tag`." + +**Line 137** — "`db.runtime()` returns the connection that runs a built query. `db.runtime()` is synchronous and returns immediately. Only `runtime.query()` is awaited. Hold the connection in a variable and reuse it. Write the type import path as `./contract.d`, exactly like that." +Five unrelated facts in five sentences, under a heading that says "The `db.sql` builder" but which is mostly about `db.runtime()`. "Write the type import path as `./contract.d`, exactly like that" told me *what* to type but gave me no reason, so I could not tell whether it is a real rule or a typo I should fix. Plainest version: split this into a short "connect and run" paragraph and a separate "build a query" paragraph. + +**Line 137** — "Finish it with [`build()`](#build), which returns the built query." +Circular. `build()` returns the built query. I still do not know what a built query *is* — an object? SQL text? Is it sent to the database at `build()` time or at `query()` time? Plainest version: "`build()` turns the chain into a query object. Nothing touches the database until you pass it to `runtime.query()`." + +**Line 172** — "Chain the methods in any order, except that a join must come before any call that names the joined table's columns, and `distinctOn()` needs its columns first in `orderBy()`." +Three rules in one sentence, and the third one I could not parse. "needs its columns first in `orderBy()`" — first in what sense? Does `distinctOn('userId')` have to be called before `orderBy`, or does `'userId'` have to be the leading `orderBy` key? I only worked it out from the example at line 465, where `distinctOn()` is called *last*. Plainest version: "The columns you pass to `distinctOn()` must be the first sort keys in `orderBy()`." + +**Line 176** — "After a join, a column name that both tables have is on `f` only under its table name, such as `f.post.id`." +I believed this, then the `innerJoin` example at line 305 writes `f.user.email`, and `email` exists only on `user`. Line 283 says a name only one table has "stays on `f` directly." So which is it — is `f.user.email` also legal, or is the example wrong? The page never says both forms work. This is the single thing I was least sure about, and it affects every joined query I would write. + +**Line 176** — "For an enum column you pass the value stored in the database, the string after `=` in the enum, so `High = \"high\"` is matched by `fns.eq(f.priority, 'high')`." +Works for `Priority`. But the schema's other enum, `user_type`, has no `=` at all — its members are bare `admin` and `user`. So what do I pass for `f.kind`? The rule as written has no answer. + +**Line 340** — "On SQLite TypeScript rejects the call. If you get past the types, it throws an error whose `code` is `ORM.CAPABILITY_MISSING`." +"If you get past the types" — I had to guess this means "if you cast to `any` or ignore the error." Say that. + +**Line 392** — "TypeScript also accepts a `nulls` option, but the builder ignores it and no `NULLS FIRST` or `NULLS LAST` reaches the SQL. Do not use it. To put nulls last, sort first by a computed value: an `fns.raw` fragment of the form `CASE WHEN IS NULL THEN 1 ELSE 0 END` that returns `pg/int4@1`. Then call `orderBy()` again for the column itself." +I understood that `nulls` is broken. I could not reliably reconstruct the workaround from prose. This is the one place on the page that describes a multi-step code change with no code block, and it is also the one place where I am most likely to get it wrong. + +## Words and phrases I had to guess + +- **"facet"** (line 135, in the anchor `#the-dbsql-facet`) — appears nowhere in the prose. I guessed it means "the `db.sql` part of the client." If the word is not in the text, it should not be in the anchor. +- **"caveat that shows up only when the query runs"** (line 11) — guessed: "fails at runtime, not at compile time." +- **"SQL-family builder"** (line 13) — guessed: "the builder for SQL databases, as opposed to MongoDB." +- **"finished `SELECT` query"** (line 164) — guessed: a chain you have stopped adding to but have *not* called `build()` on. The page uses "finished" and "built" for different things and never says so. Line 520 ("You cannot call `build()` on it") is what finally told me. +- **"contract"** (line 131) — guessed: the new name for the schema. The page says the file is renamed but never says what the word means or what `contract.json` and `contract.d.ts` each contain. +- **"`Uuid`"** as a field type (line 51) — guessed: a new built-in scalar replacing v7's `String @db.Uuid`. +- **"`@1`"** in `pg/int4@1` (line 194) — guessed: a version number on the type id. Never stated. +- **"codec"**, in `.codecId` (line 194) — guessed: the thing that converts between the database type and the JS value. Never defined. +- **"plan"** (line 137) — I guessed this is just the examples' variable name, not a Prisma concept. The page does say "The examples name the built query `plan`," which helped, but `plan` also reads like a query plan, which it is not. +- **"top-N-per-group"** (line 336) — I know this one, but it is SQL-blog jargon, not plain English. +- **"scope"** (line 174) — this one you actually defined, and I used the definition throughout. Keep it. + +## Where I asked "so what do I actually type?" + +1. **Line 131, "Install `@prisma/orm-postgres`."** No command. Every other instruction on this page is a literal command (`npx prisma contract emit`). Give me `npm install @prisma/orm-postgres`. +2. **Line 142, `import contractJson from './contract.json' with { type: 'json' };`** Import attributes need specific `tsconfig.json` and Node settings. The page shows the line and says nothing about what has to be true for it to compile. I have never typed `with { type: 'json' }` in a v7 project. +3. **Line 131, where do these files live?** "beside it" — beside `contract.prisma`. But my source is in `src/`. The import is `'./contract.d'`, so the emitted files must sit next to my TypeScript. The page never reconciles these. +4. **Line 392, the nulls-last workaround.** Described in words only. I wanted the five-line code block. +5. **Line 192, "To return fewer columns, start a new query instead."** No example of what that looks like. I understand the concept; I wanted to see the two-query version so I know there is no `reselect()` I am missing. +6. **Line 194, type ids.** "A few use a longer name, such as `pg/timestamptz-temporal@1` for `DateTime`." Which few? Where is the full list? If my `fns.raw` returns a `numeric` or a `jsonb`, I have to guess the id, and the page's rule ("`pg/` plus the PostgreSQL type name plus `@1`") has just told me the rule has exceptions it will not enumerate. +7. **Closing the connection.** "Hold the connection in a variable and reuse it." Do I ever close it? In v7 I called `$disconnect()`. The page does not say, and this is the entry-points section. +8. **`outerLateralJoin()`** is named three times (lines 151, 340) and has no section, no signature, and no example. Line 340 says "same arguments, same availability," which is something, but it is buried in another method's Remarks. + +## Where the page explains the internals when I only wanted to know what to do + +1. **Line 133** — "`public` is the PostgreSQL schema your tables are in, and Prisma ORM 8 puts every table in `public` today." If every table is always in `public`, telling me why does not help me type anything. I only need "type `db.sql.public.
`." +2. **Line 137** — "`db.runtime()` is synchronous and returns immediately. Only `runtime.query()` is awaited." This is a fact about the implementation. The example at line 145 already shows me `const runtime = db.runtime()` with no `await`, which is all I needed. +3. **Line 392** — explaining that TypeScript accepts `nulls` but the builder drops it, and that "no `NULLS FIRST` or `NULLS LAST` reaches the SQL," is an explanation of a bug's mechanism. "The `nulls` option does nothing. Do this instead: [code]" is what I needed. +4. **Line 343** — "because a name both tables have is not in the query's scope on its own" is the reason. The rule in the sentence before it was enough. + +## Could I do what the page is for, after one reading? + +Partly. I could write a single-table `select().where().orderBy().limit().build()` and run it. The chain shape is clear, the examples are runnable, and the `f`/`fns` split made sense on the first pass. + +What I still would not know: + +- **Whether my join code compiles.** The `f.user.email` contradiction means I do not know if I must qualify every column after a join or only the ambiguous ones. This blocks the main reason I would reach for this builder at all. +- **How to get from my working v7 project to the first line of this page's code.** The Entry points section assumes `contract.prisma` already exists, points at a migration page I have not read, and does not say where the emitted files go relative to my source. +- **What to pass for an enum with no `=` values**, such as `kind`. +- **What type id to use** for any Postgres type not in the seven-item list. +- **How to sort nulls last**, concretely. +- **What `outerLateralJoin()` looks like when typed out.** +- **Whether I ever close the connection.** + +One more thing that cost me confidence rather than comprehension: the schema declares `posts Post[]` on `Tag` and `tags Tag[]` on `Post`, *and* a separate explicit `PostTag` model. In v7 those are two different ways to model many-to-many and you pick one. The page then calls `post_tag` "the model for the `Post`/`Tag` join table" without explaining why both exist. I could not tell whether this is new v8 behaviour I need to learn or an error in the example schema. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part2.round4.md b/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part2.round4.md new file mode 100644 index 00000000000..b0223e8cbe2 --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part2.round4.md @@ -0,0 +1,66 @@ +Read once, top to bottom, as a Prisma 7 user. Findings below. + +## Sentences I could not restate after one reading + +1. Line 5: *"The examples below use a separate `order` schema, where `order.amount` is an integer column"* — "schema" collides with the meaning I was given in part 1, where `db.sql.public.user` is "schema then table" and `public` is the schema. So I read this as a PostgreSQL schema named `order`, then hit `db.sql.public.order` and had to reread. The word is used in three more senses on this page: "the aggregate example schema" (line 9, a `.prisma` model file) and "this page's main schema" (line 154, the example models). Plainest wording: "The examples below use an extra table, `order`, whose `amount` column is an integer." + +2. Line 3: *"A grouped query supports `having()` for group-level filtering, the aggregate functions, and the same `orderBy()`, `limit()`, `offset()`, and `distinct()` methods as a `SelectQuery`."* — "supports the aggregate functions" does not match the example. In the example the aggregate is written inside `.select()` **before** `.groupBy()`, and the same line tells me to "Call `groupBy()` after all your `select()` calls". So the grouped query does not support aggregates; the selects that precede it do. Plainer: "Write your aggregates in `select()` first, then call `groupBy()`. After that you can add `having()`, `orderBy()`, `limit()`, `offset()`, and `distinct()`." + +3. Line 75 / line 405, the `EXTRACT` comment: *"`.returns()` only tells TypeScript what type to expect. It does not convert the value, so yearPlaced is the string '2024'."* — I declared `'pg/int4@1'` and got a string. I could not work out what TypeScript now believes `row.yearPlaced` is. If `.returns('pg/int4@1')` types it as `number`, then the page's own advice, `Number(row.yearPlaced)`, is code TypeScript would flag, and the row is typed wrong at runtime. The page never says. This is the single sentence that cost me the most. + +4. Line 87: *"TypeScript does let you name a selected alias on the `f` argument here, but PostgreSQL does not accept an alias in `HAVING`, and the query then fails when it runs."* — three ideas in one sentence, and the practical upshot (the types offer you something that is guaranteed to break) arrives last. Plainer: "Do not use a selected alias in `having()`. TypeScript will let you, but PostgreSQL rejects it and the query fails at run time. Write the aggregate again instead." + +5. Line 163: *"`build()` generates default values once. Running the same built insert twice writes the same id twice, which fails on a unique column."* — This contradicts the sentence just before it, which says "the `@default(...)` in `contract.prisma` is applied". I could not tell who applies the default: PostgreSQL, or the builder on my machine. The answer matters and is only implied. Plainer: "Prisma generates `@default(uuid())` and `@default(now())` values when you call `build()`, not when the query runs. So the built query holds fixed values — reusing it inserts the same id twice. Call `.build()` again for each insert." + +6. Line 171 vs line 164: *"The rows to insert. Every column is optional."* against *"Leave out a required column that has no default and PostgreSQL rejects the insert when the query runs."* — Read together, these say the opposite things. I assume the first means "optional to TypeScript", but the table does not say so. + +7. Line 310 vs line 393: *"`param()` is for the values that have no such column"* against *"For a bare value, the builder picks the type id from its JavaScript type."* — If the builder already picks a type for a bare value in `fns.raw`, then the bare value *does* get a type without a column, and `param()`'s stated reason for existing disappears. After one reading I did not know when I actually need `param()`. + +8. Line 469: *"It does not reduce memory on PostgreSQL, because both `await` and `for await` read the same result the client has already loaded."* — I can restate the words, but not the point. The section exists to offer me `for await`, then tells me it buys nothing on my database. + +9. Line 471: *"You can `await` a result as many times as you like. You cannot mix `await` and `for await` on the same result."* — "a result" is a thing I never have. Every example on the page writes `await runtime.query(plan)` in one expression. Nothing shows the object you would hold and await twice. + +## Words and phrases I had to guess + +- **"plan"** — I guessed it is just the variable name the page chose for whatever `build()` returns, not a type. But line 434's anchor is `#executing-a-plan` while the heading reads "Running a built query", and line 456's anchor is `#recover-the-row-type-from-a-plan`. I guessed these are the same thing. +- **"contract"/"contract.prisma"** — carried over from part 1, fine. But line 389, *"so it is there only if your contract uses pgvector"*, made me guess that a contract can declare an extension. I do not know how. +- **`codecId`** (lines 311, 318, 400) — I guessed "codec" is Prisma's internal word for the thing that converts a database value to JavaScript. The page never says, it only apologises for the name. +- **`numeric`, `float8`, `varchar`, `int4`, `text`** (lines 382, 386, 405) — PostgreSQL type names. In my contract I write `Decimal`, `Float`, `String`, `Int`. I guessed at the mapping. The page never gives it, so I cannot tell which of *my* columns makes `sum()` return a string. +- **"relational-core"** in `@prisma/orm-postgres/relational-core/expression` (line 309) — I guessed it is an internal layer name I am not meant to think about. +- **"the version of `count` that does not overflow"** (line 381) — I guessed "overflow" means the 2^53 limit described at line 61, but those two places never point at each other. +- **`AsyncIterableResult`** (line 471) — guessed this is the type of the object `runtime.query()` returns, which would explain awaiting it twice. Only findable by following the link. + +## Places I asked "so what do I actually type?" and got no answer + +1. **Line 61**: *"To check for a code, catch the error and compare `error.code`."* No snippet. No `try { … } catch (e) { … }`. No word on whether `error.code` is typed, or whether I must narrow `unknown` first. This is the only error-handling instruction on the page and it has no code. +2. **Line 78**: *"You can also repeat the same `fns.raw` fragment inside `groupBy()`."* The Options table gives the signature `groupBy((f, fns) => expr)`, but there is no example. Do I have to call `.returns()` again on the repeated fragment? Do the two fragments have to match textually? Unanswered. +3. **Line 86**: `having()` rejects a `bigint` literal such as `1n`. But if my counts are big enough that I used `countBigInt()`, how do I filter on that count? No answer. +4. **Line 69**: nothing tells me which type id `EXTRACT(YEAR FROM …)` *should* have. The example passes `'pg/int4@1'` and then explains that the value is a string. Should I have written a `numeric` type id instead? I do not know what I am meant to type. +5. **Line 389**: *"it is there only if your contract uses pgvector"*. How do I make my contract use pgvector? No link, no line of schema. +6. **Line 204**: *"**Availability:** PostgreSQL and SQLite."* So on MySQL, what do I write instead of `returning()`? Not said, and not linked. +7. **Line 240**: can I mix the two `update()` forms — fixed values for some columns, expressions for others? Line 242 tells me to wrap a fixed value in `fns.raw` inside the callback, which implies no. It never says so plainly. +8. **Line 471**: what expression produces the "result" I can await repeatedly? `const result = runtime.query(plan)` with no `await`? Never shown. +9. **Line 333**: the one `param()` example binds a string with `'pg/text@1'` — exactly what line 393 says the builder infers on its own. So the example does not show a case where `param()` is needed. I finished the section without one. + +## Places that explain the inside when I only wanted the instruction + +- Line 173: *"Each value is sent as a query parameter, never pasted into the SQL. The column it goes into decides how it is stored and read."* I only needed the rule: "Never call `.returns()` or `param()` on an insert value." The mechanism is the whole first sentence. +- Line 469: *"because both `await` and `for await` read the same result the client has already loaded."* Driver-level detail. The instruction I wanted is "On PostgreSQL, use `await`." +- Line 309: *"`param` and `ResultType` are in different subpaths of the same package, so use the paths shown."* This explains the package layout to excuse itself. "Import each from the path shown" would do. +- Line 386–387: *"A `varchar` column is typed as `text`."* Both are `string` in JavaScript. I could not work out what this changes for me. +- Line 78: *"That failure comes back as PostgreSQL's own error, not a Prisma ORM code."* Useful only if the page then tells me what I *can* match on. It does not. + +## Could I do what the page is for, after one reading? + +Partly. I could write a grouped query with a count and a `having()` filter, an insert with `returning()`, an update with a values object, and a delete with a `where()`. Those four have clean, complete, copyable examples and I trust them. + +What I still would not know: + +- Whether `.returns()` makes TypeScript agree with the value I actually receive, and therefore whether my `Number(row.yearPlaced)` compiles. +- Which of my contract's column types (`Decimal`, `Float`, `Int`) make `sum()` and `avg()` hand back a string instead of a number. The table is written in PostgreSQL type names I do not use. +- How to catch and identify any of the six error codes the page names (`RUNTIME.DECODE_FAILED`, `ORM.COLUMN_UNKNOWN`, `RUNTIME.ENCODE_FAILED`, `ORM.MUTATION_DATA_MISSING`, `RUNTIME.ITERATOR_CONSUMED`), because there is no `try`/`catch` anywhere on the page. +- When `param()` is genuinely required, as opposed to redundant with the builder's own inference. +- How to group by a computed expression rather than by its alias, which the Options table offers and no example shows. +- How to insert repeatedly without rebuilding, or whether rebuilding is the only answer. + +One more thing that would bite me the day after reading: line 231 and line 281 both say an `update()` or `delete()` with no `where()` hits every row and "nothing warns you". That is the most dangerous fact on the page and it sits mid-paragraph in the prose, not in a `:::warning`, while the far less costly aggregate-overflow note gets its own callout box. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part1.round4.md b/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part1.round4.md new file mode 100644 index 00000000000..428013c01d4 --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part1.round4.md @@ -0,0 +1,83 @@ +I read it once, top to bottom, as a Prisma 7 user. + +## Sentences I could not restate after one reading + +**Line 11:** "`db` is the client `postgres(...)` or `mongo(...)` returns. The runtime is the connected object you run queries on, and `db.runtime()` returns it. The import path `@prisma/orm-postgres/runtime` is a package name and is not that object." + +Three different things called "runtime" in three sentences, and the third one is a warning about a confusion I did not yet have. It made me think I had missed something. Plainer: "`db` is the client. `db.runtime()` gives you the object that actually runs queries. (The `/runtime` in the import path is unrelated — it is just where the package lives.)" + +**Line 13:** The whole paragraph. It is one block containing: what the page covers, that each method has a Remarks list, that Remarks note database differences, that transactions work on Postgres, that they do not work on Mongo, the workaround for Mongo, and four outbound links. I cannot hold that. Split it: a sentence on scope, a sentence on how to read the Remarks lists, then a short "Transactions are PostgreSQL-only" note. + +**Line 26:** "The `` type argument is what types `db.orm` and `db.sql`. Pass it yourself whenever you pass `contractJson`, because TypeScript cannot read a type out of a JSON file. It is inferred only on the `contract` path." + +"the `contract` path" — I had to reread twice to work out this means "when you use the `contract` option instead of `contractJson`". I first read "path" as a file path, which is exactly the wrong reading on a line about JSON files. Say "It is inferred only when you use the `contract` option." + +**Line 27:** "A `binding` is one object that names the source and carries it: `{ kind: 'url', url }`, `{ kind: 'pgPool', pool }`, or `{ kind: 'pgClient', client }`." + +"names the source and carries it" is a riddle. I understood it only from the code. Plainer: "A `binding` wraps any of the three choices in one object, with a `kind` field saying which it is." + +**Line 31:** "`verifyMarker: 'onFirstUse'`, the default, makes the first query check that the database was set up for your contract." + +"marker" is never explained. I do not know what a marker is, where it lives, or how it got there. I can guess it is a row or table Prisma writes during migration, but the page never says. Also the option name says "verify marker" while the description says "check that the database was set up" — those do not obviously connect. + +**Line 57:** "A PostgreSQL enum type has values and no separate names, so each value is its own name: `db.nativeEnums.public['AalLevel'].nameOf('aal1')` is `'aal1'`." + +This documents a method whose entire purpose is to return its own argument. After one reading I could not tell why it exists or when I would call it. If it is only there for API symmetry with `.enums`, say that. + +**Line 72:** "The line `import contractJson from './contract.json' with { type: 'json' }` is what `prisma orm init` writes, and it needs Node.js 22.18 or newer and TypeScript set up as `prisma orm init` sets it up." + +"TypeScript set up as `prisma orm init` sets it up" tells me nothing actionable. I have an existing project; `prisma orm init` already ran, or did not. I do not know which `tsconfig.json` settings are required. Name the settings. + +**Line 208:** "`mode` | `'strict'` or `'permissive'` | No | Defaults to `'strict'`. Only your own middleware reads it." + +I stopped dead. If only my own middleware reads it, what does Prisma do with it, and what do the two words mean? This reads like an option that does nothing, which cannot be right. Plainer: "Prisma ignores this value. It is passed through to your middleware so your own code can branch on it." + +**Line 309:** "Call `db.close()`, not `runtime.close()`. `db.close()` marks the client closed and then closes the `MongoClient` Prisma ORM created. After `runtime.close()` the client still accepts calls and then fails." + +Two ideas jammed together: an instruction, and an explanation of internal bookkeeping. The last sentence describes a bug-shaped behaviour without telling me what to do about it. "Always call `db.close()`. `runtime.close()` leaves the client looking open, and later calls fail." would be enough. + +## Words and phrases I had to guess + +- **"contract"** — I guessed it is what `schema.prisma` used to be, renamed. The page tells me the file was renamed but never says what a contract *is*. +- **"marker"** (line 31) — guessed: something Prisma writes into the database during migration to record which schema version is applied. +- **"binding"** — guessed: a discriminated union wrapping the connection-source options. +- **"middleware"** — line 30 gives an example, so I got the shape. I guessed the eight hook names from their words alone. I do not know when `beforeCompile` fires versus `beforeQuery`, what `interceptQuery` intercepts, or whether `onRow` runs per row (I assume yes). +- **"built query"** (line 94) — guessed: the object `.build()` returns. The page does say this, but the link text next to it says "Running a built query" while the anchor is `#executing-a-plan`, and the Mongo heading anchor is `[#run-a-pipeline-builder-plan]`. So "built query" and "plan" appear to be the same thing under two names, and I had to guess that. +- **"the `contract` path"** — guessed: "the `contract` option". +- **"`.context` is internal"** (lines 58, 218) — guessed: do not touch. Then why list it? +- **"`AsyncIterableResult`"** (line 284) — guessed from the name plus the sentence. The link goes to part 2, which I do not have. + +## Places I asked "so what do I actually type?" + +1. **`runtime.connection()`** (line 96). "Call `connection.release()` when you are done" — I have no code shape. Do I `const connection = await runtime.connection()`? Does it return a promise? Do I run queries on `connection.query(...)`? The link points to a section in the half I do not have, but this is the only lifecycle call on the page with no example and an unknown signature. +2. **`runtime.prepare(...)`** (line 97). Literally written with an ellipsis. I do not know what goes inside. +3. **A plain read.** Line 9 promises `db.orm.public.User.where(...).all()`, and then every single example on the page uses `db.sql.public.tag.select(...).build()`. Not one example uses `.orm`. After reading the whole client-lifecycle section I still cannot write the Prisma 7 `findMany` I came for. +4. **The `Contract` type.** Examples write `import type { Contract } from './contract.d'`. The page never says `contract.d.ts` exports a type named `Contract`, so I do not know whether that name is fixed, configurable, or my own alias. +5. **Closing a pool you supplied.** Lines 28, 84, 141 all tell me I close it myself. No example shows the order — do I call `db.close()` first, then `pool.end()`? Does `db.close()` on a supplied pool even do anything? Same gap on Mongo (lines 193, 243). +6. **Running a Mongo write.** Line 285 says use `runtime.execute(built)` and that there is no `execute()` on the client. No example. +7. **The `tsconfig.json` for `await using`** (line 166). "`@types/node` installed, **or** `"lib": ["esnext.disposable"]`" — I cannot tell if the second replaces my existing `lib` array or adds to it. +8. **Extensions** (line 29, 45). The remark says import from `@prisma/orm-extension-pgvector/runtime` and pass `extensions: [pgvector]`. The table calls the type "Array of extension packages". So do I pass the package, or the thing its `/runtime` subpath default-exports? Those are different, and the table contradicts the example. + +## Places that explain the internals when I only wanted to know what to do + +- **Line 11**, the whole "the import path is a package name and is not that object" aside. +- **Line 31**, "Prisma ORM logs a warning and runs the query anyway" — plus the concept of a marker at all, when what I need is "run `npx prisma db update`". +- **Line 57**, `nameOf` returning its own argument, and the reasoning about PostgreSQL enums having no separate names. +- **Lines 58 and 218**, "`.context` is internal." Do not document it, then. +- **Line 139**, "If a connection is being opened when you call it, `close()` waits for that to finish, then releases the pool it created." I only needed "`close()` is safe to call at any time." +- **Line 309**, "marks the client closed and then closes the `MongoClient` Prisma ORM created." +- **Line 192**, "That is the only difference between `uri` and `url`." The page spends a long remark on a distinction that reads like a historical accident. What I want is a one-line rule: "Use `url`. Use `uri` only if you must pass `dbName` separately." + +## Could I do what the page is for, after one reading? + +Partly. I could create a Postgres client from a connection string, connect it, close it, and use `await using`. The Postgres examples are concrete and I could copy them. + +What I still could not do: + +- **Write an actual query.** Every example builds a SQL builder query and calls `.build()`. The page never shows a completed read through `db.orm`, which is the thing line 9 told me I came for. +- **Use a pooled connection or a prepared statement.** Both are listed as runtime calls with no signature I can type. +- **Run a Mongo write**, or a Mongo transaction. Line 13 tells me to "pass your own `MongoClient` with the `mongoClient` option and use the driver's `session.withTransaction(...)`" and sends me elsewhere for the code. That is the single most likely reason a Mongo user opens a page called "Transactions and runtime", and it is a link. +- **Decide about `verifyMarker` or `mode`,** because I do not know what either one controls. +- **Set up a new project.** The page assumes `prisma orm init` already ran and refers to its output three times ("what `prisma orm init` writes", "TypeScript set up as `prisma orm init` sets it up", "the same way `prisma orm init` does") without saying what any of it is. That is fair for a reference page, but then it should link to the setup page from line 72, not just name the command. + +One more thing. The heading structure repeats — `connect()`, `runtime()`, `close()`, and `await using` all appear twice, once per database — and the only way I can tell which one a link points at is a trailing `-1` in the anchor (`#connect-1`, `#runtime-1`, `#close-1`). Clicking `[Transactions (MongoDB)](#transactions-mongodb)` from line 269 is fine, but `#connect-1` gives me no clue it means the MongoDB one. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part2.round4.md b/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part2.round4.md new file mode 100644 index 00000000000..b707c39a2e2 --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part2.round4.md @@ -0,0 +1,81 @@ +I read `/Users/wmadden/Projects/prisma/web/.claude/worktrees/error-reference-follow-ups-81a8e7/wip/c21/ref/r4/transactions-and-runtime.part2.mdx` top to bottom, once. + +# Sentences I could not restate after one reading + +**Line 3.** "Open a connection and drive the transaction yourself when you need statements on one connection outside a transaction, such as a `SET` before you begin." +Stopped by: "drive the transaction yourself" names no method, and "statements on one connection outside a transaction" is three ideas stacked. I did not know what "open a connection" meant in this API until line 156 told me it is `runtime.connection()`. Plainer: "If you need to run a statement like `SET` on the same connection before the transaction starts, get a connection with `runtime.connection()` and call `commit()` or `rollback()` yourself." + +**Line 14.** "To pass the full `tx`, take its type from the client: `type Tx = Parameters[0]>[0]`." +Stopped by: nested `Parameters<...>` with two index accesses. I can decode it given a minute, but not in one reading, and the page does not say why no exported type exists. Plainer: add "There is no exported type for the full `tx`, so derive it:" before the code. + +**Line 19.** "Do not let an `AsyncIterableResult` escape the callback unread." +Stopped by: `AsyncIterableResult` is not defined until line 423, the last section of the page. On first reading it is a word I have never seen. "escape ... unread" is a metaphor. Plainer: "`tx.query(...)` returns a result object that only works while the transaction is open. Read it inside the callback." + +**Line 116.** "That path holds the SQL runtime API, the same one for every SQL database." +Stopped by: I do not know what "the SQL runtime API" is as a thing, and "the same one for every SQL database" tells me about Prisma's internal layering, not about what I do. Also the import path contains `family-runtime` and the page never says what a "family" is. + +**Line 310.** "A statement written as raw SQL with `db.raw.sql` and ending in `.affectedCount()`, which asks for a row count instead of rows, prepares into a `PreparedExecution` instead." +Stopped by: four ideas in one sentence (raw SQL, `.affectedCount()`, what it means, the resulting type). Plainer: "Some queries return a row count instead of rows. End the query with `.affectedCount()`. Preparing one gives you a `PreparedExecution`, which you run with `execute(target, params)`. It resolves to `{ affectedRows }`." + +**Line 368.** "A signal that is already aborted when you call `query(...)` rejects before any row is fetched, with an error whose `code` is `RUNTIME.ABORTED` and whose `details.phase` is `'stream'`." +Stopped by: the phase is `'stream'` for a query that never streamed anything. That reads like a contradiction, and nothing explains it. + +**Line 400.** "`durationMs` is how long the query took, and it is present for every query the runtime ran." +Stopped by: the type is written `durationMs?` twice (line 400 and the example comment on line 418). Optional in the type, always present in the prose. I could not tell which to trust or whether I need a null check. + +**Line 400.** "`lane` is the name of the API you used, such as `orm-client` for `db.orm` or `raw` for `db.raw.sql`." +Stopped by: "lane". I only understood it because of the examples. The word carries no meaning on its own. + +**Line 425.** "`for await` does not reduce memory on PostgreSQL: every row is loaded first." +Stopped by: this removes the only reason I would have used `for await`, and the page does not say whether it is different on MongoDB or why I would ever pick it. Line 423 offers it as an equal option and line 425 says it is pointless. I could not tell what the page wants me to do. + +# Words and phrases I had to guess + +- **"family-runtime"** (lines 14, 116, 138, 342) — I guessed a "family" is a group of databases that share a runtime, so PostgreSQL/MySQL/SQLite. The page never says. +- **"SqlQueryPlan"** (lines 229, 238, 284) — I guessed this is just the type of what `.build()` returns. Line 229 says so, but the name "plan" made me expect something I had to inspect or execute differently. +- **"codecId"** (line 228) — I guessed "codec" means the encoder/decoder for a column's type, and `codecId` gives me the same string as `'pg/text@1'`. +- **"type id"** / **"`pg/text@1`"** (line 228) — I guessed these are Prisma's own names for PostgreSQL types. I could not guess which id to use for a `String` column vs a `DateTime` beyond the two examples given. +- **"`pg/timestamptz-temporal@1`"** (line 228) — I guessed "temporal" refers to the JS Temporal API. Not stated. +- **"fingerprint"** (line 400) — I guessed a hash of the query text. The page says what it is for, which was enough. +- **"scope"** (line 371) — I guessed it is an internal tracing field. Guessing was forced: the page names it, tells me not to pass it, and never says what it is. +- **"lane"** (line 400) — guessed above. +- **"target"** (line 320) — I guessed "the thing that owns the connection the statement runs on." The word alone told me nothing. +- **"the offending names"** (line 230) — I guessed this means the parameter names I declared but never used. + +# "So what do I actually type?" — unanswered + +1. **Line 12: the transaction timeout.** "To set one, set PostgreSQL's `idle_in_transaction_session_timeout` on the connection." This is the one concrete way to bound a transaction and there is no code. I do not know whether that is a connection-string parameter, a `SET` I run via `connection.execute`, or server config. This is the first thing a Prisma 7 user will look for, because `$transaction` had `timeout` and `maxWait`. +2. **Lines 3 and 152: the `SET` before the transaction.** Both sentences say the entire reason to use manual control is running a `SET` first — and neither of the two manual examples (lines 175 and 191) contains a `SET`. `connection.execute(...)` appears to take a built query, and I cannot build `SET` with `db.sql.public.
`, so I would have to guess `db.raw.sql`. The page names the use case three times and never shows it. +3. **Line 117: `withTransaction` without a client.** The section exists for "a function that takes a runtime as a parameter instead of the client", then tells me to build queries "with a SQL builder you already hold, such as the client's `db.sql`" — and the example on line 142 reaches out to a module-scope `db`. If my function only has a runtime, I still do not know where a SQL builder comes from. The example does not demonstrate the situation the section is written for. +4. **Line 215: MongoDB transactions.** "share one `MongoClient` between Prisma ORM and your driver code" — no code, no indication of how a `MongoClient` is handed to the Prisma client. The link may cover it, but the reference gives me nothing. +5. **What `tx.execute(...)` returns.** Line 11 says use it "when the write returns no rows". Every example throws the result away. `{ affectedRows }` is only ever mentioned for `PreparedExecution` (line 310). I do not know if `tx.execute` gives me a row count. +6. **Line 228: which type id for my column.** Four ids are listed. My contract has `String`, `Int`, `DateTime`, `Boolean`, `Json`, enums. I would have to follow a link for the mapping, or use `codecId`, and the page does not say "use `codecId` and skip the table", which is what I actually want. +7. **Line 369: the phase list.** Six values, no meaning for any of them. If I catch `RUNTIME.ABORTED`, I cannot tell what I should do differently for `'encode'` vs `'afterQuery'`. +8. **Retrying.** Line 12 says "Nothing retries a failed transaction for you." A serialization or deadlock failure is the normal reason to retry, and the page never says which `code` to catch or that I should write the loop myself. + +# Places that explain the internals when I only wanted to know what to do + +- **Line 116:** "That path holds the SQL runtime API, the same one for every SQL database." Package layout, not my problem. +- **Line 371:** "`scope` is set for you. Do not pass it." Telling me a field exists purely so I can avoid it. If I must not pass it, do not name it. +- **Line 228:** "A type id is the PostgreSQL type plus a version, always `@1` today." The versioning scheme is Prisma's business. I need the id string. +- **Line 157:** "It does not close the pool or the runtime, and the runtime opens a replacement connection on the next query." Reassuring, but it is pool mechanics; the actionable half is the last sentence, "Use `destroy()` only for a connection you no longer trust." +- **Line 400:** "Every run of the same query text shares one `fingerprint`, whatever values you pass" — this one earns its place, but it sits in a paragraph that also defines `lane`, `outcome`, `durationMs`, and the "most recent query only" rule. Five facts in one bullet. + +# Could I do what the page is for after one reading? + +Partly. + +I could write a normal transaction. Lines 3–108 are clear, the `tx` vs `db` warning on line 11 is exactly what a Prisma 7 user needs, and the diff on line 96 answered my first question — where did `$transaction([...])` go — better than anything else on the page. + +What I would still not know: + +- How to put a time limit on a transaction. The page names `idle_in_transaction_session_timeout` and stops. +- How to run the `SET` that is the page's own stated reason for manual connection control. +- How to use `withTransaction` in the situation it was written for, since the example uses an outer-scope `db`. +- What to do when a transaction fails on a conflict — which code, and that I write the retry loop myself. +- Which type id my columns need, beyond `codecId`. +- Whether `durationMs` can be missing. +- Whether I should ever use `for await`. +- What `tx.execute(...)` returns. + +The prepared statements section is the strongest part: line 252 and line 297 gave me two working shapes and the difference between them is stated plainly on line 276. \ No newline at end of file From 9eb8ae0f2e4b4cf2f936e2320c78e8d72b57c498 Mon Sep 17 00:00:00 2001 From: willbot Date: Sun, 13 Sep 2026 09:44:58 +0200 Subject: [PATCH 09/14] docs(orm8): orm/reference pages, final fact re-check against rc.10 Twenty-six corrections across four pages, each backed by a source file in prisma/orm at rc.10. Among them: the pipeline builder's callback project() has no 0 form, redact() takes the fn value itself, findOneAndUpdate() is offered only straight after match(), and update results carry upsertedCount and upsertedId only after an upsert insert; the SQL builder's limit() and offset() also take a numeric expression, .returns() takes { codecId, nullable }, and tables can sit in a namespace block; transactions and prepared statements are unavailable on MongoDB rather than PostgreSQL only, MongoDB's runtime.execute() serves update and delete commands only, and the RuntimeExecuteOptions table lists scope again. Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- .../docs/orm/reference/pipeline-builder.mdx | 36 ++++++++++--------- .../docs/orm/reference/raw-queries.mdx | 10 +++--- .../docs/orm/reference/sql-query-builder.mdx | 14 ++++---- .../reference/transactions-and-runtime.mdx | 16 ++++----- 4 files changed, 39 insertions(+), 37 deletions(-) diff --git a/apps/docs/content/docs/orm/reference/pipeline-builder.mdx b/apps/docs/content/docs/orm/reference/pipeline-builder.mdx index bbd46a62260..33bf70c3cf0 100644 --- a/apps/docs/content/docs/orm/reference/pipeline-builder.mdx +++ b/apps/docs/content/docs/orm/reference/pipeline-builder.mdx @@ -81,7 +81,7 @@ model Tutorial { Every pipeline starts with `db.query.from('posts')`, naming the collection to aggregate over. It ends with a call that turns the chain into a query you can run. Between them you chain stages. -`db` is the client you create with `mongo(...)`, whose `dbName` option is the name of the MongoDB database to use. See [Transactions and runtime](/orm/reference/transactions-and-runtime#mongooptions) for every option and for how to create the client. Three import paths appear on this page: `@prisma/orm-mongo/runtime` holds the `mongo()` client, `@prisma/orm-mongo/query-builder` holds `mongoQuery`, `fn`, `acc`, and `expr`, and `@prisma/orm-mongo/query-ast/execution` holds the `Mongo*Stage` classes, `MongoAggFieldRef`, and `MongoFieldFilter`. Every example holds the built query in a variable named `plan`. +`db` is the client you create with `mongo(...)`, whose `dbName` option is the name of the MongoDB database to use. See [Transactions and runtime](/orm/reference/transactions-and-runtime#mongooptions) for every option and for how to create the client. Three Prisma ORM import paths appear on this page: `@prisma/orm-mongo/runtime` holds the `mongo()` client, `@prisma/orm-mongo/query-builder` holds `mongoQuery`, `fn`, `acc`, and `expr`, and `@prisma/orm-mongo/query-ast/execution` holds the `Mongo*Stage` classes, `MongoAggFieldRef`, and `MongoFieldFilter`. Every example holds the built query in a variable named `plan`. Prisma ORM 8 renames `schema.prisma` to `contract.prisma`. Run `npx prisma contract emit` before any code on this page compiles: it writes `contract.json`, used at run time, and `contract.d.ts`, used for types. The example below imports `contract.json` with `with { type: 'json' }`, which works with the TypeScript settings `prisma orm init` writes for you. [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7) has the setup steps. @@ -162,8 +162,8 @@ Where each form goes. `.node` is the raw MongoDB expression inside an `fn.*` val | Call site | What you pass | |---|---| -| `project()`, `addFields()`, and the arguments of an accumulator | The `fn.*` value itself. | -| The condition argument of `fn.cond()`, `redact()`, and the stages that take an options object | The `.node` property of the `fn.*` value. | +| `project()`, `addFields()`, `redact()`, and the arguments of an accumulator | The `fn.*` value itself. | +| The condition argument of `fn.cond()`, and the stages that take an options object | The `.node` property of the `fn.*` value. | | `match()` | The `fn.*` value wrapped in `expr(...)`. | #### Options @@ -402,14 +402,14 @@ Reshape each document. #### Remarks - `project()` has two forms. A **key-list** form narrows to the named fields (`project('title', 'kind')`); `_id` is retained implicitly even when not listed. -- A **callback** form computes a projection spec (`project((f) => ({ title: 1, shout: fn.toUpper(f.title) }))`). Write `1` to keep a field, `0` to drop it, or an expression to compute it. MongoDB rejects a mix of `1` and `0` in one projection, except for `_id: 0`. +- A **callback** form computes a projection spec (`project((f) => ({ title: 1, shout: fn.toUpper(f.title) }))`). Write `1` to keep a field, or an expression to compute it. There is no `0` form here: the callback form cannot drop a field. #### Options | Name | Type | Required | Description | |---|---|---|---| | `...fields` | Field names (`string`) | Key-list form | The fields to keep. | -| `spec` | Callback `(f) => ({ [field]: 1 \| 0 \| Expression })` | Callback form | The projection specification. | +| `spec` | Callback `(f) => ({ [field]: 1 \| Expression })` | Callback form | The projection specification. | #### Return type @@ -433,7 +433,7 @@ import { fn } from '@prisma/orm-mongo/query-builder'; const plan = db.query .from('posts') - .project((f) => ({ _id: 0, title: 1, shout: fn.toUpper(f.title) })) + .project((f) => ({ title: 1, shout: fn.toUpper(f.title) })) .build(); const projected = await runtime.query(plan); ``` @@ -444,7 +444,7 @@ Unroll an array field into one document per element. #### Remarks -- `unwind(field, { preserveNullAndEmptyArrays? })` maps to MongoDB's `$unwind`. The named field must hold an array. +- `unwind(field, { preserveNullAndEmptyArrays? })` maps to MongoDB's `$unwind`. Name the field holding the array. TypeScript accepts any field of the document; nothing checks that it holds an array. #### Options @@ -484,7 +484,7 @@ Group documents by a key and compute per-group aggregates. - `group()` takes a callback that receives **only** the `f` argument, one property per field (`(f) => ...`), and returns a spec object. The spec's `_id` sets the grouping key; every other key must be an **accumulator**. - `acc` holds the accumulators `group()` accepts, such as a count or a sum, documented under [Accumulators](#accumulators). Import it and use it inside the callback, **not** as a second callback argument: `import { acc } from '@prisma/orm-mongo/query-builder'`, then `acc.count()`, `acc.push(f.title)`. The callback takes one argument; there is no `(f, acc) => ...` form. - A `_id: null` key groups the whole collection into a single bucket. -- A non-accumulator value for a non-`_id` key throws as soon as you call `group()`, with an error whose `code` is `ORM.ARGUMENT_INVALID`: `group() field "" must use an accumulator (e.g. acc.sum(), acc.count()). Got "" expression.` A `null` value for a key other than `_id` throws the same way. +- A non-accumulator value for a non-`_id` key throws as soon as you call `group()`, with an error whose `code` is `ORM.ARGUMENT_INVALID`: `group() field "" must use an accumulator (e.g. acc.sum(), acc.count()). Got "" expression.` A `null` value for a key other than `_id` throws with the same code and the message `group() field "" must not be null. Only _id can be null.` #### Options @@ -681,7 +681,7 @@ Several stages take a single MongoDB stage options object rather than a typed `( - `bucket({ groupBy, boundaries, default_?, output? })` sorts documents into the ranges you name. `default` is a reserved word in JavaScript, so the key is `default_`. `groupBy` is a raw expression. - `bucketAuto({ groupBy, buckets, output?, granularity? })` sorts them into a given number of ranges, chosen so each holds about the same number of documents. `groupBy` is a raw expression. `granularity` is a string that MongoDB reads as the series it rounds the bucket boundaries to. Prisma ORM passes it through unchanged, accepts any string, and MongoDB rejects the ones it does not know. -- `geoNear({ near, distanceField, spherical?, maxDistance?, minDistance?, query?, key?, distanceMultiplier?, includeLocs? })` sorts documents by their distance from a point and writes that distance into `distanceField`. `near` is a raw expression. +- `geoNear({ near, distanceField, spherical?, maxDistance?, minDistance?, query?, key?, distanceMultiplier?, includeLocs? })` sorts documents by their distance from a point and writes that distance into `distanceField`. `near` is the point to measure from, and Prisma ORM passes it through to MongoDB unchanged. - `graphLookup({ from, startWith, connectFromField, connectToField, as, maxDepth?, depthField?, restrictSearchWithMatch? })` follows links from document to document through one collection and collects what it reaches into `as`. `startWith` is a raw expression. - `setWindowFields({ partitionBy?, sortBy?, output })` computes a value for each document from the documents around it, such as a running total. `partitionBy` is a raw expression. `output` is an object mapping each new field name to `{ operator, window? }`, where `operator` is a raw expression, usually an accumulator: `{ runningTotal: { operator: MongoAggAccumulator.sum(MongoAggFieldRef.of('duration')), window: { documents: ['unbounded', 0] } } }`. - `densify({ field, partitionByFields?, range })` adds documents to fill the gaps in a sequence of numbers or dates. `range` is `{ step, unit?, bounds }`, where `bounds` is `'full'`, `'partition'`, or a pair of start and end values. @@ -887,10 +887,10 @@ Here is every helper. Anything missing from this list has no helper, including ` | Call site | What you pass | |---|---| -| `project()`, `addFields()`, and the arguments of an accumulator | The `fn.*` value itself. | +| `project()`, `addFields()`, `redact()`, and the arguments of an accumulator | The `fn.*` value itself. | | The condition argument of `fn.cond()` | Its `.node` property. | | `f.stage.set()`, `f.stage.replaceRoot()`, and `f.stage.replaceWith()`, documented under [Update operation forms](#update-operation-forms) | The `.node` property. | -| `redact()`, and the `groupBy`, `startWith`, and `partitionBy` options of `bucket()`, `bucketAuto()`, `graphLookup()`, `setWindowFields()`, and `fill()` | The `.node` property. | +| The `groupBy`, `startWith`, and `partitionBy` options of `bucket()`, `bucketAuto()`, `graphLookup()`, `setWindowFields()`, and `fill()` | The `.node` property. | | `match()` | The `fn.*` value wrapped in `expr(...)`. | `expr()` is what lets a `match()` filter compare computed values. Import it beside `fn`: @@ -932,9 +932,11 @@ These helpers take one object of named arguments instead of positional ones: | `fn.dateTrunc` | `{ date, unit, binSize?, timezone?, startOfWeek? }` | | `fn.dateAdd` | `{ startDate, unit, amount, timezone? }` | | `fn.dateSubtract` | The same keys as `fn.dateAdd`. | -| `fn.dateToString` | `{ date, format }` | -| `fn.trim` | `{ input }` | -| `fn.regexMatch` | `{ input, regex }` | +| `fn.dateToString` | `{ date, format?, timezone?, onNull? }` | +| `fn.dateFromString` | `{ dateString, format?, timezone?, onError?, onNull? }` | +| `fn.trim`, `fn.ltrim`, and `fn.rtrim` | `{ input, chars? }` | +| `fn.regexMatch`, `fn.regexFind`, and `fn.regexFindAll` | `{ input, regex, options? }` | +| `fn.replaceOne` and `fn.replaceAll` | `{ input, find, replacement }` | | `fn.zip` | `{ inputs, useLongestLength?, defaults? }` | | `fn.getField` | `{ field, input? }` | | `fn.setField` | `{ field, input, value }` | @@ -1026,7 +1028,7 @@ Updater callbacks (the `(f) => [...]` argument to the update methods) must retur #### Remarks - These return a built query. Run it with `runtime.query(...)`. -- Write results are result objects, not documents: `insertOne` gives `{ insertedId }`, `insertMany` gives `{ insertedIds, insertedCount }`, the update methods and `upsertOne` give `{ matchedCount, modifiedCount, upsertedCount, upsertedId }`, and the delete methods give `{ deletedCount }`. A write returns one result object, as the single row of the result, which is why every example here reads it with `const [result] = ...`. +- Write results are result objects, not documents: `insertOne` gives `{ insertedId }`, `insertMany` gives `{ insertedIds, insertedCount }`, the update methods and `upsertOne` give `{ matchedCount, modifiedCount }`, plus `upsertedCount` and `upsertedId` when an upsert inserted a document, and the delete methods give `{ deletedCount }`. A write returns one result object, as the single row of the result, which is why every example here reads it with `const [result] = ...`. - The document you pass to `insertOne` / `insertMany` is a plain record. TypeScript requires no particular field, and the builder does not check the record against your contract. You can leave nullable fields out. The examples below pass `null` for them to make the document shape explicit. - You can leave `_id` out and let MongoDB assign one. - **On the way in, values are stored exactly as you pass them, so pass a `Date` for a date field and an `ObjectId` from the `mongodb` package for an id field.** Nothing turns a string into either. This is the write direction only. Reads are decoded as the first half of this page describes. @@ -1078,9 +1080,9 @@ After a `match()` filter, `updateMany`, `updateOne`, `deleteMany`, `deleteOne`, - The `match()` filter supplies the write's filter, so you do not repeat it. - After `match()`, `upsertOne()` takes the updater callback only (the filter comes from `match()`). -- `updateOne` / `deleteOne` affect at most one matching document. `updateMany` / `deleteMany` affect all matches. When it matters which document you change, put a `sort()` between the `match()` and the write and use `findOneAndUpdate()`, as in `.match(...).sort(...).findOneAndUpdate(...)`. +- `updateOne` / `deleteOne` affect at most one matching document. `updateMany` / `deleteMany` affect all matches. Which document that is, this builder gives you no way to choose. - `findOneAndUpdate()` and `findOneAndDelete()` return the document exactly as the `mongodb` driver gives it, so its `_id` is an `ObjectId` from the `mongodb` package even though the TypeScript type says `string`. To get the id as a string, write `String(result._id)`. -- You cannot call `findOneAndUpdate()` or `findOneAndDelete()` after `skip()`, and TypeScript does not offer them there. If a cast gets one past TypeScript, it throws an error whose `code` is `ORM.OPERATION_UNSUPPORTED`. +- `findOneAndUpdate()` and `findOneAndDelete()` are offered straight after `match()` and nowhere else. TypeScript withdraws them after every other stage, `sort()` and `skip()` included. If a cast gets one past TypeScript after a `skip()`, it throws an error whose `code` is `ORM.OPERATION_UNSUPPORTED`. #### Examples diff --git a/apps/docs/content/docs/orm/reference/raw-queries.mdx b/apps/docs/content/docs/orm/reference/raw-queries.mdx index 9128971dde2..1e94aaba8bc 100644 --- a/apps/docs/content/docs/orm/reference/raw-queries.mdx +++ b/apps/docs/content/docs/orm/reference/raw-queries.mdx @@ -47,7 +47,7 @@ const rows = await runtime.query(plan); // rows === [{ id: aliceId, upperEmail: 'ALICE@EXAMPLE.COM' }] ``` -`.returns(...)` only tells TypeScript what to expect. It does not cast the value in SQL and it does not change how the `pg` package reads it. It takes a type id string, or an object of the form `{ codecId, nullable }`. It does not take a column, so `.returns(db.sql.public.user.columns.id)` is a type error. Only `.returnsRow(spec)` takes columns. See [Binding a bare value with `param()`](#binding-a-bare-value-with-param) for the type ids you are most likely to write, and the SQL query builder's [`fns.raw` and `.returns()`](/orm/reference/sql-query-builder#fnsraw-and-returns) for the full treatment. +`.returns(...)` only tells TypeScript what to expect. It does not cast the value in SQL and it does not change how the `pg` package reads it. It takes a type id string, or an object of the form `{ codecId, nullable }`. See [Binding a bare value with `param()`](#binding-a-bare-value-with-param) for the type ids you are most likely to write, and the SQL query builder's [`fns.raw` and `.returns()`](/orm/reference/sql-query-builder#fnsraw-and-returns) for the full treatment. ### `fns.raw` as a `where()` predicate @@ -64,7 +64,7 @@ const rows = await runtime.query(plan); ### Interpolating a typed expression -You can interpolate another expression, such as a comparison, a column reference, or another raw fragment. Interpolated values are sent as query parameters, so they are safe. Column and table names cannot be parameters, so they would be pasted into the SQL text. Never build an identifier from user input. When the column is chosen at run time, pick it in your own code from a fixed set of columns you control, and choose the matching `fns.raw` fragment or `select()` call for each one. +You can interpolate another expression, such as a comparison, a column reference, or another raw fragment. The expression itself goes into the fragment, not its rendered SQL, so it stays type-checked. An interpolated value is sent as a query parameter, never pasted into the SQL. A column or table name cannot be interpolated as a string, because a string becomes a parameter. When the column is chosen at run time, pick it in your own code from a fixed set of columns you control, and choose the matching `fns.raw` fragment or `select()` call for each one. ```ts const plan = db.sql.public.user @@ -91,7 +91,7 @@ A bare value that you do interpolate directly gets the type id Prisma ORM picks | a `boolean` | `pg/bool@1` | | a `Uint8Array` | `pg/bytea@1` | -A type id is `pg/`, a name, and `@1`. The name is usually the PostgreSQL type, and a few carry a suffix that says which JavaScript type you get, such as `pg/int8number@1` for a `number` and `pg/timestamptz-temporal@1` for a `Temporal.Instant`. Between those two, pick `pg/int8@1` when the value can exceed 2^53. Three more you are likely to write are `pg/uuid@1`, `pg/numeric@1`, and `pg/jsonb@1`. Those are the common ones, not the whole set. For any column, `db.sql.public.
.columns..codecId` is its id, and the same ids are in `contract.json` under `storage.namespaces..entries.table.
.columns..codecId`. A point in time is the case `param()` exists for, and `pg/timestamptz-temporal@1` takes a `Temporal.Instant`, the standard JavaScript object for a point in time, not a `Date`. `Temporal` is a global in Node.js 26 and later, and on earlier versions you install `temporal-polyfill` and add `import 'temporal-polyfill/full/global'`. +A type id is `pg/`, a name, and `@1`. The name is usually the PostgreSQL type, and a few carry a suffix that says which JavaScript type you get, such as `pg/int8number@1` for a `number` and `pg/timestamptz-temporal@1` for a `Temporal.Instant`. Between those two, pick `pg/int8@1` when the value can exceed 2^53. Three more you are likely to write are `pg/uuid@1`, `pg/numeric@1`, and `pg/jsonb@1`. Those are the common ones, not the whole set. For any column, `db.sql.public.
.columns..codecId` is its id, and the same ids are in `contract.json` under `storage.namespaces..entries.table.
.columns..codecId`. A point in time is the case `param()` exists for, and `pg/timestamptz-temporal@1` takes a `Temporal.Instant`, the standard JavaScript object for a point in time, not a `Date`. `Temporal` is a global in Node.js 26.8.2 and later, and on earlier versions you install `temporal-polyfill` and add `import 'temporal-polyfill/full/global'`. ```ts import { param } from '@prisma/orm-postgres/relational-core/expression'; @@ -207,7 +207,7 @@ To bind any value whose type is not in the list, wrap it in [`param(value, { cod Each of these nine methods builds a command. Call `.build()` on it, then run every one of them with `runtime.query(...)`, including the writes. The raw collection methods all run through `query()`. A write returns a one-element array, so destructure it with `const [result] = ...`. :::warning[MongoDB raw results are native BSON values] -Nothing on the MongoDB side of this page is converted for you. An `_id` comes back as an `ObjectId` instance from the `mongodb` package, the MongoDB driver underneath, not a hex string. Compare one with `String(row._id) === aliceId`. Raw filters take native BSON values too, so build ids with that package's `ObjectId` class. The write methods give you the `mongodb` package's own result objects, `{ insertedId }`, `{ matchedCount, modifiedCount, upsertedCount, upsertedId }`, and `{ deletedCount }`, and you read those keys directly. +Nothing on the MongoDB side of this page is converted for you. An `_id` comes back as an `ObjectId` instance from the `mongodb` package, the MongoDB driver underneath, not a hex string. Compare one with `String(row._id) === aliceId`. Raw filters take native BSON values too, so build ids with that package's `ObjectId` class. The write methods give you the counts and ids under the `mongodb` package's own key names, `{ insertedId }`, `{ matchedCount, modifiedCount, upsertedCount, upsertedId }`, and `{ deletedCount }`, and you read those keys directly. ::: The examples in this section run against the `users` / `posts` schema from the pipeline builder page. See its [example schema](/orm/reference/pipeline-builder#example-schema) for the full model definitions. Create the client with `mongo(...)`, get the connection that runs a built query from `await db.runtime()`, and declare the two example ids once: @@ -336,7 +336,7 @@ const isAlice = String(deleted._id) === aliceId; // true ## `db.query.rawCommand()` -`db.query.rawCommand(command)` runs a raw MongoDB aggregate command, sent exactly as you wrote it. See the [pipeline builder](/orm/reference/pipeline-builder). It returns a built query already, so do not call `.build()` on it. Unlike `aggregate()` it takes no type parameter, so its rows come back as `unknown` and you cast them yourself. +`db.query.rawCommand(command)` runs a raw MongoDB command, sent exactly as you wrote it. See the [pipeline builder](/orm/reference/pipeline-builder). It returns a built query already, so do not call `.build()` on it. Unlike `aggregate()` it has no row type parameter, so its rows come back as `unknown` and you cast them yourself. ```ts import { RawAggregateCommand } from '@prisma/orm-mongo/query-ast/execution'; 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 4c895e87322..2ce9718a1b3 100644 --- a/apps/docs/content/docs/orm/reference/sql-query-builder.mdx +++ b/apps/docs/content/docs/orm/reference/sql-query-builder.mdx @@ -130,11 +130,11 @@ model Feature { In Prisma ORM 8 your `schema.prisma` is called `contract.prisma`. [Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7) gets you from a Prisma ORM 7 project to that file, and has the rest of the setup steps. Install the client with `npm install @prisma/orm-postgres`. Run `npx prisma contract emit` to write `contract.json` and `contract.d.ts` beside `contract.prisma`. The examples below assume your own file is in `src/prisma/`, next to those two files, and they write the type import path as `./contract.d`, exactly like that. -You build queries from the client's `sql` property. Create a Postgres client with `postgres(...)`, then reach tables through `db.sql.public`. `public` is the PostgreSQL schema your tables are in. Prisma ORM 8 puts every table in `public`. Table accessors are keyed by table name, not model name. A table name is the model name with a lowercase first letter unless the model sets `@@map`, so the `User` model mapped to `user` is reached as `db.sql.public.user`, and the model for the `Post`/`Tag` join table, mapped to `post_tag`, is reached as `db.sql.public.post_tag`. Column names are the field names unless a field sets `@map`. +You build queries from the client's `sql` property. Create a Postgres client with `postgres(...)`, then reach tables through `db.sql.public`. `public` is the PostgreSQL schema your tables are in. A model goes in `public` unless your contract puts it in a `namespace` block. Table accessors are keyed by table name, not model name. A table name is the model name with a lowercase first letter unless the model sets `@@map`, so the `User` model mapped to `user` is reached as `db.sql.public.user`, and the model for the `Post`/`Tag` join table, mapped to `post_tag`, is reached as `db.sql.public.post_tag`. Column names are the field names unless a field sets `@map`. ### The `db.sql` builder [#the-dbsql-facet] -`db.runtime()` returns the connection that runs a built query. Hold it in a variable and reuse it. When your app shuts down, close the client with [`db.close()`](/orm/reference/transactions-and-runtime#close). +`db.runtime()` returns the connection that runs a built query. On PostgreSQL it is synchronous, so no `await` is needed. Hold it in a variable and reuse it. When your app shuts down, close the client with [`db.close()`](/orm/reference/transactions-and-runtime#close). A `select()`, `insert()`, `update()`, or `delete()` call starts a query. [`build()`](#build) turns the chain into a query object, which the examples name `plan`. Nothing touches the database until you pass that object to `runtime.query(...)`. You can run the same built query more than once. @@ -479,8 +479,8 @@ Cap the number of returned rows and skip rows in the ordered result set. | Method | Argument | Description | |---|---|---| -| `limit(n)` | `number` | Return at most `n` rows. | -| `offset(n)` | `number` | Skip the first `n` rows. | +| `limit(n)` | `number`, or a numeric expression | Return at most `n` rows. | +| `offset(n)` | `number`, or a numeric expression | Skip the first `n` rows. | #### Return type @@ -671,7 +671,7 @@ const rows = await runtime.query(plan); // avgAmount is 300, exactAvg is the exa ### Ordering and limiting a grouped query -A `GroupedQuery` supports the same `orderBy()`, `limit()`, `offset()`, `distinct()`, and [`distinctOn()`](#distincton) methods as a `SelectQuery`. Sort by an aggregate alias to order groups. The alias must be one you selected in this query. Any other name throws an error whose `code` is `ORM.COLUMN_UNKNOWN`, at the moment you call `orderBy()`. +A `GroupedQuery` supports the same `orderBy()`, `limit()`, `offset()`, `distinct()`, and [`distinctOn()`](#distincton) methods as a `SelectQuery`. Sort by an aggregate alias to order groups. A name that is neither a column of the table you started from nor an alias you selected throws an error whose `code` is `ORM.COLUMN_UNKNOWN`, at the moment you call `orderBy()`. #### Examples @@ -844,7 +844,7 @@ Give a value its type by hand, for a value with no column to take the type from, #### Remarks - Import `param` from `@prisma/orm-postgres/relational-core/expression`. -- Values passed to `insert()`, `update()`, and comparison functions such as `fns.eq(f.col, value)` each take their type from the column they are used with, so you rarely need `param()`. Reach for it when the type the builder would pick is not the one the column needs. A bare string becomes `pg/text@1`, so a value going into a `uuid` column needs `param(id, { codecId: 'pg/uuid@1' })`. A `Temporal.Instant` cannot be interpolated bare at all. +- Values passed to `insert()`, `update()`, and comparison functions such as `fns.eq(f.col, value)` each take their type from the column they are used with, so you rarely need `param()`. Reach for it when the type the builder would pick is not the one the column needs. Inside `fns.raw` no column lends its type, so a bare string becomes `pg/text@1` and a value compared there with a `uuid` column needs `param(id, { codecId: 'pg/uuid@1' })`. A `Temporal.Instant` cannot be interpolated bare at all. - The option is called `codecId`, but it takes the same type id used everywhere else on this page. [Binding a bare value with `param()`](/orm/reference/raw-queries#binding-a-bare-value-with-param) lists the type id each kind of JavaScript value gets, which is where to look for a date, a uuid, a boolean, or an array. #### Options @@ -934,7 +934,7 @@ Write a raw SQL fragment as a tagged template. Interpolate columns, typed expres | Name | Type | Required | Description | |---|---|---|---| | SQL fragment | Tagged template | Yes | The raw SQL, with `${...}` interpolations for columns and values. | -| `.returns(typeId)` | `string` | Yes, always | Declares the result type, for example `'pg/int4@1'`, `'pg/text@1'`. For a fragment used as a `where()` predicate, write `.returns('pg/bool@1')`. | +| `.returns(typeId)` | `string`, or `{ codecId, nullable }` | Yes, always | Declares the result type, for example `'pg/int4@1'`, `'pg/text@1'`. For a fragment used as a `where()` predicate, write `.returns('pg/bool@1')`. | #### Remarks diff --git a/apps/docs/content/docs/orm/reference/transactions-and-runtime.mdx b/apps/docs/content/docs/orm/reference/transactions-and-runtime.mdx index c46d06653f6..a86f8aa01c1 100644 --- a/apps/docs/content/docs/orm/reference/transactions-and-runtime.mdx +++ b/apps/docs/content/docs/orm/reference/transactions-and-runtime.mdx @@ -14,7 +14,7 @@ This page documents the client lifecycle (create, connect, close), transactions, Each method below has a Remarks list. If a method behaves differently on PostgreSQL and MongoDB, or exists on only one of them, that list says so. -Transactions are PostgreSQL only. To run one on MongoDB, pass your own `MongoClient` with the `mongoClient` option and use the driver's `session.withTransaction(...)`, which [Transactions on MongoDB](/orm/fundamentals/transactions#transactions-on-mongodb) shows in code. +Transactions are not available on MongoDB. To run one there, pass your own `MongoClient` with the `mongoClient` option and use the driver's `session.withTransaction(...)`, which [Transactions on MongoDB](/orm/fundamentals/transactions#transactions-on-mongodb) shows in code. ## Client lifecycle on PostgreSQL @@ -58,7 +58,7 @@ Pass exactly one of `url`, `pg`, and `binding`. - `.orm` holds your models by model name. See the [ORM client reference](/orm/reference/orm-client). - `.sql` is the SQL query builder, keyed by schema, then table name: `db.sql.public.tag`. The table name is the model's `@@map` value, or the model name with a lowercase first letter when the model sets no `@@map`. See the [SQL query builder reference](/orm/reference/sql-query-builder). `.raw` writes raw SQL as a template string. See the [raw queries reference](/orm/reference/raw-queries). -- `.enums` holds the enum values from your contract, by PostgreSQL schema: `db.enums.public.Role`. `.nativeEnums` holds the values of enum types that exist in PostgreSQL itself, created with `CREATE TYPE`, also by schema. Look the type up by name in brackets: `db.nativeEnums.public['AalLevel'].values` is `['aal1', 'aal2']`. +- `.enums` holds the enum values from your contract, by PostgreSQL schema: `db.enums.public.Role`. `.nativeEnums` holds the values of enum types that exist in PostgreSQL itself, created with `CREATE TYPE`, also by schema. Look the type up by name in brackets: `db.nativeEnums.auth['AalLevel'].values` is `['aal1', 'aal2', 'aal3']`. - `.contract` is the contract as an object. [`connect()`](#connect), [`runtime()`](#runtime), [`transaction()`](#dbtransactioncallback), [`prepare()`](#dbpreparedeclaration-callback), and [`close()`](#close) are documented on this page. #### Examples @@ -93,7 +93,7 @@ Open a connection and return a runtime. #### Remarks -- Returns a `Promise`. The runtime is the connected object that runs queries. It has five calls: +- Returns a `Promise`. The runtime is the connected object that runs queries. It has these calls: - `runtime.query(built)` runs a built query and returns its rows. A built query is the object you get from `.build()` at the end of a builder or raw chain. See [Running a built query](/orm/reference/sql-query-builder#executing-a-plan). - `runtime.execute(built)` runs a built query that returns no rows, such as an insert, and returns `{ affectedRows }`. See [Running a built query](/orm/reference/sql-query-builder#executing-a-plan). - `runtime.connection()` takes one connection out of the pool so that several queries run on the same connection. It returns a promise, and you put the connection back with `connection.release()`: `const connection = await runtime.connection(); try { await connection.query(built) } finally { await connection.release() }`. See [Manual connection and transaction control](#manual-connection-and-transaction-control). @@ -189,7 +189,7 @@ Create a MongoDB client. - Pass the contract with `contractJson` or `contract` (supply exactly one), the same way as on PostgreSQL. - Tell the client which database to use in one of four ways: `url`, `uri`, `mongoClient`, or `binding`. A `binding` wraps the choices in one object, with a `kind` field saying which it is: `{ kind: 'url', url, dbName }` or `{ kind: 'mongoClient', client, dbName }`. Use it when your code picks the source at run time, and use the `url` kind for a `uri`. Pass exactly one of the four, or leave all four out and pass one to [MongoDB `connect()`](#connect-1). `dbName` is separate and does not count as one of the four, so `{ url, dbName }` is allowed. -- With `url`, the database name comes from the path of the connection string, as in `mongodb://host:27017/app`. Add `dbName` to override that name, or when the string has no name in its path. Use `uri` only when you must pass `dbName` separately. +- With `url`, the database name comes from the path of the connection string, as in `mongodb://host:27017/app`. Add `dbName` to override that name, or when the string has no name in its path. `uri` takes the same kind of string, and always needs `dbName`. - **Client ownership**: with `url` or `uri`, Prisma ORM creates the underlying `MongoClient` and closes it on `close()`. With `mongoClient`, you supplied the client, so Prisma ORM does **not** close it. Because `close()` leaves your client open, you can use one `MongoClient` both through Prisma ORM and in code you write against the `mongodb` package directly. You close that client yourself. - `mongo(options)` does not open a connection. The runtime is built on first use, or explicitly via [MongoDB `connect()`](#connect-1). @@ -280,7 +280,7 @@ Run a built query through the runtime. - `(await db.runtime()).query(built)` runs any built MongoDB query, including one built by the [pipeline builder](/orm/reference/pipeline-builder) (`db.query`). - Returns an [`AsyncIterableResult`](#asynciterableresult), which holds the rows the query returns: `await` it for an array, or `for await` to read the rows one at a time as they arrive. -- For a write that returns no rows, use `runtime.execute(built)`, which returns `{ affectedRows }`. There is no `execute()` on the client itself. +- For an update or delete command, use `runtime.execute(built)`, which returns `{ affectedRows }`. Other commands throw an error whose `code` is `RUNTIME.MONGO_STATISTICS_UNSUPPORTED`. There is no `execute()` on the client itself. #### Return type @@ -296,7 +296,6 @@ Run a built query through the runtime. // 'posts' is the collection name, the same key you use on `db.orm` const built = db.query.from('posts').build(); const posts = await (await db.runtime()).query(built); -const { affectedRows } = await (await db.runtime()).execute(built); // for a write that returns no rows ``` ### `close()` @@ -443,7 +442,7 @@ For Prisma ORM 7 users, an array of queries becomes a callback: - prisma.post.create({ data: { title, userId } }), - ]); + const { user, post } = await db.transaction(async (tx) => { -+ const user = await tx.orm.public.User.create({ email, displayName }); ++ const user = await tx.orm.public.User.create({ email, displayName, kind: 'user' }); + const post = await tx.orm.public.Post.create({ title, userId: user.id }); + return { user, post }; + }); @@ -560,7 +559,7 @@ There is no `db.transaction(...)` on the MongoDB client, and the MongoDB runtime ## Prepared statements (PostgreSQL) -A prepared statement compiles a query once against a declaration of its parameters, then runs it repeatedly with different values. Prepared statements are PostgreSQL only. +A prepared statement compiles a query once against a declaration of its parameters, then runs it repeatedly with different values. Prepared statements are not available on MongoDB. ### `runtime.prepare(declaration, callback)` @@ -716,6 +715,7 @@ Per-query options for cancellation. | Name | Type | Required | Description | |---|---|---|---| | `signal` | `AbortSignal` | No | Per-query cancellation signal. | +| `scope` | `'runtime' \| 'connection' \| 'transaction'` | No | A label middleware reads. The runtime sets it; setting it yourself changes only the label, not where the query runs. | #### Examples From 13f900ad284f48ecfc4e07482f36aba114b1ad92 Mon Sep 17 00:00:00 2001 From: willbot Date: Sun, 13 Sep 2026 09:45:19 +0200 Subject: [PATCH 10/14] docs: orm/reference final fact re-check reports Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- .../factcheck2.pipeline-builder.md | 115 ++++++++++++++++++ .../factcheck2.raw-queries.md | 99 +++++++++++++++ .../factcheck2.sql-query-builder.md | 79 ++++++++++++ .../factcheck2.transactions-and-runtime.md | 83 +++++++++++++ 4 files changed, 376 insertions(+) create mode 100644 docs/orm-docs-audit/c21-orm-reference/factcheck2.pipeline-builder.md create mode 100644 docs/orm-docs-audit/c21-orm-reference/factcheck2.raw-queries.md create mode 100644 docs/orm-docs-audit/c21-orm-reference/factcheck2.sql-query-builder.md create mode 100644 docs/orm-docs-audit/c21-orm-reference/factcheck2.transactions-and-runtime.md diff --git a/docs/orm-docs-audit/c21-orm-reference/factcheck2.pipeline-builder.md b/docs/orm-docs-audit/c21-orm-reference/factcheck2.pipeline-builder.md new file mode 100644 index 00000000000..7f2784ea3b3 --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-reference/factcheck2.pipeline-builder.md @@ -0,0 +1,115 @@ +Checks complete. Report below. + +## 1. Claims checked + +Line numbers are in the corrected slice (`wip/c21/ref/fc2/pipeline-builder.mdx`). Source paths are relative to `wip/prisma-src/`. `qb/` = `packages/2-mongo-family/5-query-builders/query-builder/src/`, `ast/` = `packages/2-mongo-family/4-query/query-ast/src/`. + +| Line | Claim | Source | Verdict | Now says | +|---|---|---|---|---| +| 9-11 | Aggregation only, no `find()`/`distinct()`, `limit(1)` for one document | qb/builder.ts, qb/query.ts (no such methods) | Confirmed | | +| 19 | Example schema is the same as the ORM client page | `apps/docs/.../orm-client.mdx` | Confirmed | | +| 26-75 | Schema block fields (`_id`, `kind`, `authorId`, `createdAt`, `duration`) | same | Confirmed | | +| 84 | `mongo(...)` client, `dbName` option | packages/3-extensions/mongo/src/runtime/mongo.ts:55 | Confirmed | | +| 84 | "Three import paths appear on this page" | page also imports `ObjectId` from `mongodb` (line 1233) | Corrected | "Three Prisma ORM import paths appear on this page" | +| 84 | `/runtime`, `/query-builder`, `/query-ast/execution` exist and hold those names | packages/9-public/@prisma/orm-mongo/package.json; qb/exports/*.ts; ast/exports/execution.ts | Confirmed | | +| 86 | `prisma contract emit` writes `contract.json` and `contract.d.ts` | cli/src/commands/init/hygiene-gitattributes.ts:8,35 | Confirmed | | +| 86 | `with { type: 'json' }` works with the settings `prisma orm init` writes | cli/src/commands/init/templates/tsconfig.ts:23-27 (`module: 'preserve'`, `moduleResolution: 'bundler'`, `resolveJsonModule: true`) | Confirmed | | +| 94 | `from()` takes the collection name from the roots map | qb/query.ts:29; qb/state-classes.ts:640-660 | Confirmed | | +| 95 | `ORM.MODEL_UNKNOWN`, message `Unknown root: "". Valid roots: ...` | qb/state-classes.ts:648 | Confirmed | | +| 96 | `insertOne()`/`insertMany()` on `from()` only, before any stage | qb/state-classes.ts (on `CollectionHandle`, absent from `FilteredCollection`) | Confirmed | | +| 97 | `mongoQuery({ contractJson }).from(...)` builds only, never runs | qb/query.ts:33-60 | Confirmed | | +| 116-124 | Entry example: default `mongo` import, `contract.d`, `contract.json` | cli/.../code-templates.ts:336-345 | Confirmed | | +| 129-135 | `build()` returns the built query; `db.runtime()` is a promise; no `db.execute` | qb/builder.ts `build()`; mongo.ts:38, `MongoClient` interface has no `execute` | Confirmed | | +| 136 | Decoding: the eight stages keep the shape, anything else makes every document raw; `_id` hex string vs `ObjectId` | qb/pipeline-result-shape.ts:14, 68-89 | Confirmed | | +| 136 | A `DateTime` field is a `Date` either way | `mongo/date@1` output is `Date`; raw driver returns `Date` | Confirmed | | +| 157-158 | `match()` callback; scalar operators `eq, ne, gt, gte, lt, lte, in, nin, exists, type` | qb/field-accessor.ts:51-67 | Confirmed | | +| 159 | `expr(fn.gt(...))` form for computed comparisons | qb/field-accessor.ts:222 | Confirmed | | +| 165-166 | `.node` table under `match()` listed `redact()` as needing `.node` | qb/builder.ts `redact()` takes the callback's value and reads `.node` itself | **Corrected** | `redact()` moved to the "the `fn.*` value itself" row; the `.node` row now reads "The condition argument of `fn.cond()`, and the stages that take an options object" | +| 206-213 | `_id` equality in `match()` never matches; `db.orm.posts.where({ _id }).first()` works | pipeline builder sends a bare `MongoFieldFilter` with no codec (ast/filter-expressions.ts:51); ORM client wraps values in `MongoParamRef(value, { codecId })` (orm/src/collection.ts:744) | Confirmed | | +| 222 | `sort()` spec `{ field: 1 \| -1 }` | qb/builder.ts `sort()` | Confirmed | | +| 251-322 | `limit`, `skip`, `sample` signatures | qb/builder.ts | Confirmed | | +| 330 | `addFields()` callback returns new fields, existing fields preserved | qb/builder.ts `addFields()`; qb/pipeline-result-shape.ts:56-66 | Confirmed | | +| 364-365 | `lookup()` shape; `on()` returns fixed `local`/`foreign` keys | qb/lookup-builder.ts:36-37, 195-200 | Confirmed | | +| 366 | No match gives an empty array | MongoDB `$lookup` behaviour; no source test | **Q** | | +| 367 | Both `_id`s are `ObjectId`; `String(post.author[0]._id) === String(post.authorId)` | qb/pipeline-result-shape.ts (lookup drops the shape) | Confirmed | | +| 404 | Key-list form; `_id` retained implicitly | qb/builder.ts `project()` overload 1 (`Pick`); test/integration/.../query-builder.test.ts:370 | Confirmed | | +| 405, 412, 436 | Callback form accepts `0` to drop a field; example used `_id: 0` | qb/builder.ts `project()` spec type is `Record`, and the body does `val === 1 ? 1 : val.node` | **Corrected** | "Write `1` to keep a field, or an expression to compute it. There is no `0` form here: the callback form cannot drop a field." Options row is now `1 \| Expression`; `_id: 0` removed from the example | +| 447 | "The named field must hold an array" | qb/builder.ts `unwind`; qb/types.ts:211 `UnwoundShape` has no array constraint | **Corrected** | "Name the field holding the array. TypeScript accepts any field of the document; nothing checks that it holds an array." | +| 454 | `preserveNullAndEmptyArrays` defaults to `false` | qb/builder.ts (`?? false`) | Confirmed | | +| 484-485 | `group()` takes one callback argument; `acc` is imported, not a second argument | qb/builder.ts `group()`; qb/accumulator-helpers.ts | Confirmed | | +| 487 | Non-accumulator message and `ORM.ARGUMENT_INVALID`; "a `null` value throws the same way" | qb/builder.ts (two distinct messages) | **Corrected** | The null case now quotes its own message: `group() field "" must not be null. Only _id can be null.` | +| 546 | `replaceRoot()` callback returns an object-valued expression; MongoDB's `'newRoot' expression must evaluate to an object` | qb/builder.ts `replaceRoot()` confirmed; the MongoDB message is not in the source | Confirmed / **Q** on the message text | | +| 583 | `count()` stage is not `acc.count()` | qb/builder.ts `count()`; qb/accumulator-helpers.ts `count()` | Confirmed | | +| 612 | `sortByCount()` emits `{ _id, count }` sorted descending | qb/builder.ts `sortByCount()` return type; integration test line 750 | Confirmed | | +| 644-645 | `redact()` needs `$$KEEP`/`$$DESCEND`/`$$PRUNE`; `f.rawPath` prefixes `$` | qb/field-accessor.ts `rawPath`; packages/3-mongo-target/2-mongo-adapter/src/lowering.ts:37 (`` `$${expr.path}` ``); integration test lines 620-626 | Confirmed | | +| 682 | `bucket({ groupBy, boundaries, default_?, output? })` | qb/builder.ts | Confirmed | | +| 683 | `bucketAuto({ groupBy, buckets, output?, granularity? })`, `granularity` a plain string passed through | qb/builder.ts (`granularity?: string`) | Confirmed; "MongoDB rejects the ones it does not know" is **Q** | | +| 684 | `geoNear` options; "`near` is a raw expression" | qb/builder.ts types `near: unknown`, not `MongoAggExpr` | **Corrected** | "`near` is the point to measure from, and Prisma ORM passes it through to MongoDB unchanged." | +| 685 | `graphLookup({ from, startWith, connectFromField, connectToField, as, maxDepth?, depthField?, restrictSearchWithMatch? })`, `startWith` raw | qb/builder.ts | Confirmed | | +| 686 | `setWindowFields` options and the `output` example | qb/builder.ts; ast/stages.ts:777-783; integration test lines 666-681 | Confirmed | | +| 687 | `densify({ field, partitionByFields?, range })`, `range` `{ step, unit?, bounds }` | ast/stages.ts:825-829 | Confirmed | | +| 688 | `fill` options; `output` is `{ method }` or `{ value }`; `method` a plain string | ast/stages.ts:860-863 | Confirmed | | +| 689-691 | `facet(facets)`, `unionWith(collection, pipeline?)`, `Mongo*Stage` classes from `/query-ast/execution` | qb/builder.ts; ast/exports/execution.ts | Confirmed | | +| 704-740 | `facet()` and `bucket()` examples and their result comments | integration test lines 446-490 (same call shapes; `_id` is the lower boundary, rows are `{ _id, count }`) | Confirmed | | +| 749-752 | Atlas stages build anywhere, `search(config, index?)`, `searchMeta(config, index?)`, `vectorSearch({...})` | qb/builder.ts | Confirmed; "the error only appears when you run the query" is **Q** | | +| 768-790 | Nineteen accumulators and their signatures | qb/accumulator-helpers.ts (19 exported) | Confirmed | | +| 792-795 | `{ input, n }`; `top`/`bottom` `{ output, sortBy }`; `topN`/`bottomN` add `n`; `sortBy` plain object; `n: fn.literal(2)` | qb/accumulator-helpers.ts:75-165 | Confirmed | | +| 797 | `sort()` before `group()` for `first`/`last` | integration test lines 800-810 sorts first; MongoDB `$first` is order-dependent | Confirmed | | +| 860-869 | Renames `isIn`, `typeOf`, `toString_`, `firstElem`, `lastElem` | qb/expression-helpers.ts | Confirmed | | +| 871-884 | The full helper list; no `and`/`or`/`not`/`switch`/`ifNull`/`map`/`reduce`/`filter` | qb/expression-helpers.ts (76 helpers, every group name matched one by one) | Confirmed | | +| 886 | `expr(...)` where `.node` is expected throws a `TypeError` | `MongoExprFilter.accept` calls `visitor.expr`, which the aggregation lowering visitor does not define (lowering.ts:36-56) | Confirmed from code; **no test covers it** | | +| 890-894 | The `.node` table; `redact()` listed as needing `.node` | as above | **Corrected** | `redact()` moved to the value row; the options-object row no longer names `redact()` | +| 892 | `f.stage.set()`, `replaceRoot()`, `replaceWith()` take `.node`; `unset()` is not listed | qb/field-accessor.ts:140-145 (`unset(...paths: string[])`) | Confirmed | | +| 907-921 | `MongoOrExpr.of([...])`, `MongoAndExpr.of([...])`, `.not()`; `MongoFieldFilter` has `eq, neq, gt, gte, lt, lte, in, nin, isNull, isNotNull` | ast/filter-expressions.ts:17, 51-89, 113, 140 | Confirmed | | +| 922 | `MongoFieldFilter.neq` versus `fn.ne` | ast/filter-expressions.ts:55; qb/expression-helpers.ts `ne` | Confirmed | | +| 924 | `fn.slice` and `fn.range` positional, and so is every helper not in the table | qb/expression-helpers.ts — seven more named-args helpers were missing from the table | **Corrected** via the table (below); the sentence is now true | +| 925 | `fn.toObjectId(fn.literal(id))` throws a `TypeError` | qb/expression-helpers.ts:96-101 `docUnaryExpr` reads `arg._field.codecId`, and `literal()` sets `_field: undefined as never` | Confirmed from code; **no test covers it** | | +| 931-941 | The named-argument helper table | qb/expression-helpers.ts | **Corrected** | `fn.dateToString` → `{ date, format?, timezone?, onNull? }`; `fn.trim` → `{ input, chars? }` grouped with `ltrim`/`rtrim`; `fn.regexMatch` → `{ input, regex, options? }` grouped with `regexFind`/`regexFindAll`; added `fn.dateFromString` and `fn.replaceOne`/`fn.replaceAll` | +| 943 | `fn.convert`'s `to` takes a type name as an expression | qb/expression-helpers.ts `convert` | Confirmed | | +| 951-971 | Arithmetic, string and `cond()` examples, including `.node` on the condition | qb/expression-helpers.ts `cond(condition: MongoAggExpr, ...)`; integration test lines 780-795 | Confirmed | | +| 979-982 | `pipe(stage)` / `pipe(stage)`; naming rule; rows undecoded after it | qb/builder.ts `pipe` overloads; qb/pipeline-result-shape.ts | Confirmed | | +| 1009 | `aggregate()` is another name for `build()` | qb/builder.ts `aggregate() { return this.build(); }` | Confirmed | | +| 1015-1020 | Updater array rule; bare operation throws; `ORM.MUTATION_DATA_MISSING`; `Cannot mix ...`; `Update spec collision: ...` | qb/update-ops.ts:185-225, 130-140 | Confirmed | | +| 1024 | `insertOne`, `insertMany`, `updateAll`, `deleteAll`, `upsertOne` on the root only | qb/state-classes.ts | Confirmed | | +| 1031 | Write result shapes; "`{ matchedCount, modifiedCount, upsertedCount, upsertedId }`" | ast/result-types.ts:26-31 — the last two are optional | **Corrected** | "…give `{ matchedCount, modifiedCount }`, plus `upsertedCount` and `upsertedId` when an upsert inserted a document" | +| 1032 | `insertOne` takes any record, no contract check | qb/state-classes.ts:173 (`Record`) | Confirmed | | +| 1033 | `_id` may be left out | same, plus `InsertOneResult.insertedId` | Confirmed | | +| 1034 | Values stored exactly as passed | packages/3-mongo-target/2-mongo-adapter/src/mongo-adapter.ts:94-95 passes the document through | Confirmed | | +| 1036 | `insertMany([])` throws `ORM.MUTATION_DATA_MISSING` | qb/state-classes.ts:201 | Confirmed | | +| 1066 | `upsertOne(filterCallback, updaterCallback)` on the root | qb/state-classes.ts:265 | Confirmed | | +| 1082 | After `match()`, `upsertOne()` takes the updater only | qb/state-classes.ts:516 | Confirmed | | +| 1083 | "put a `sort()` between the `match()` and the write and use `findOneAndUpdate()`" | `FilteredCollection` starts `F = 'fam-cleared'`; `sort()` keeps it cleared, so TypeScript withdraws `findOneAndUpdate`. Test asserts exactly this: qb test/state-machine-surface.test-d.ts:46-60 (`@ts-expect-error — sort-past-FilteredCollection keeps F = 'fam-cleared'`) | **Corrected** | "Which one they pick is up to the driver, and the typed builder gives you no way to order that choice." | +| 1084 | `findOneAndUpdate`/`findOneAndDelete` return the driver's document, `_id` an `ObjectId` | qb/state-classes.ts (no `resultShape` on those plans) | Confirmed | | +| 1085 | Only `skip()` withdraws them | same test file, lines 62-112: `group`, `limit`, `addFields`, `project`, `unwind`, `skip`, and `sort` all clear the marker | **Corrected** | "offered straight after `match()` and nowhere else. TypeScript withdraws them after every other stage, `sort()` and `skip()` included. If a cast gets one past TypeScript after a `skip()`, it throws an error whose `code` is `ORM.OPERATION_UNSUPPORTED`." | +| 1116 | `findOneAndUpdate()`'s second argument optional, `returnDocument` default `'after'`, `upsert` default `false` | qb/state-classes.ts:545-575 (`opts … = {}`, `?? 'after'`, `?? false`) | Confirmed | | +| 1125-1127 | `returnNewDocument` is not a valid option; unknown key ignored | the options type admits only `upsert` and `returnDocument`, and the constructor reads only those | Confirmed | | +| 1139-1140 | Operator form has fourteen operators; pipeline form has `set`, `unset`, `replaceRoot`, `replaceWith` | qb/field-accessor.ts:70-90 (14 operators); `StageEmitters` (4) | Confirmed | | +| 1145-1160 | The operator argument table, including `pop(direction)` defaulting to `1` | qb/field-accessor.ts:262 (`pop: (direction = 1)`); qb/update-ops.ts | Confirmed | | +| 1160 | `setOnInsert` does nothing outside `upsertOne()` | qb/update-ops.ts emits `$setOnInsert`; MongoDB applies it only on upsert insert | Confirmed | | +| 1189 | `out(collection, db?)` | qb/builder.ts `out(collection: string, db?: string)` | Confirmed | | +| 1190-1191 | `merge({ into, on, whenMatched, whenNotMatched })`; `whenMatched` also takes `MongoAddFieldsStage`/`MongoProjectStage`/`MongoReplaceRootStage` | qb/builder.ts `merge()`; `MongoUpdatePipelineStage` union | Confirmed | | +| 1192 | Both return an empty array | `#writeTerminal` returns `MongoQueryPlan`; `$out`/`$merge` emit no documents | Confirmed | | +| 1214 | `rawCommand()` takes `RawAggregateCommand(collection, pipeline)`, rows `unknown` | qb/query.ts:30, 41-59; ast/raw-commands.ts:4-15 | Confirmed | | + +## 2. Q list (left as written) + +| Claim | Searches run | +|---|---| +| `lookup()` with no match gives an empty array (line 366) | `grep -rn "lookup" test/integration/test/mongo/query-builder.test.ts`; qb test dir; only the matching case is tested | +| `replaceRoot()` message `'newRoot' expression must evaluate to an object` (line 546) | `grep -rn "newRoot" packages/ test/` — the string is nowhere in the tree; it is MongoDB's own | +| `bucketAuto`'s "MongoDB rejects the ones it does not know" about `granularity` (line 683) | `grep -rn "granularity" packages/` — only the pass-through `granularity?: string` | +| Atlas stages: "the error only appears when you run the query" (line 749) | `grep -rn "Atlas\|mongot\|\$search" packages/2-mongo-family packages/3-mongo-target` — nothing checks for Atlas; the builder appends the stage and sends it. Nothing in the tree runs one | +| The result comments `// { total: 2, counted: 2 }`, `// { firstTitle: 'Hello world', lastTitle: 'Tutorial one' }`, `// [{ total: 2 }]`, `// { earliest: , avgYear: 2024 }` | The integration suite uses a different fixture (Products/Orders, five documents), so no test seeds this page's `posts`. The numbers are internally consistent with a two-document `posts` collection and match the ORM client page's data, but no source file decides them | + +Two claims the code supports but **no test covers**: `fn.toObjectId(fn.literal(...))` throwing a `TypeError` (line 925) and `expr(...)` in a `.node` slot throwing a `TypeError` (line 886). Both follow directly from the code, so I left them stated as facts. + +## 3. Page-conventions bullets + +- The rc.10 bullet: the one rc.10 change named in the brief (buffered PostgreSQL queries releasing their pooled connection early) is PostgreSQL-only. This page is MongoDB-only and says nothing about connection release, so nothing to change. +- The last bullet (the generated error-reference page is out of scope): nothing in this slice touches it. The page links to `/orm/reference/raw-queries`, `/orm/reference/orm-client`, `/orm/reference/sql-query-builder`, `/orm/reference/transactions-and-runtime` and `/orm/coming-from-prisma-orm-7`, none of them the error reference. +- Anchors: no heading text, numbering, order, or pinned anchor (`[#read-terminals]`, `[#pipeline-write-terminals-out-and-merge]`) was changed. +- `bash .claude/skills/docs-reader-review/scripts/check-plain.sh wip/c21/ref/fc2/pipeline-builder.mdx` → `plain-language check: clean`. + +## 4. Line count + +Before: 1238. After: 1240. The two extra lines are rows in the named-argument helper table for helpers that were missing from it, which the sentence above the table asserted were positional. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/factcheck2.raw-queries.md b/docs/orm-docs-audit/c21-orm-reference/factcheck2.raw-queries.md new file mode 100644 index 00000000000..c1ab0f2c779 --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-reference/factcheck2.raw-queries.md @@ -0,0 +1,99 @@ +Fact re-check complete. Both slices are the same length as before (raw-queries 354 lines, index 41 lines). Five corrections, all on the raw-queries page; the index page needed none. + +## 1. Claims checked + +### `index.mdx` + +| Line | Claim | Source | Verdict | +| --- | --- | --- | --- | +| 11 | ORM client at `db.orm`, works on PostgreSQL and MongoDB | `packages/3-extensions/postgres/src/runtime/postgres.ts:51-61`, `packages/3-extensions/mongo/src/runtime/mongo.ts:30-40` | Confirmed | +| 12 | SQL query builder at `db.sql`; not on the MongoDB client | same two files (no `sql` on `MongoClient`) | Confirmed | +| 13 | Pipeline builder at `db.query`, MongoDB only | `mongo.ts:31`; no `query` on `PostgresClient` | Confirmed | +| 17 | `.build()` then `runtime.query(...)`, or `runtime.execute(...)` for a write with no rows | `packages/2-sql/5-runtime/src/sql-runtime.ts:99-113`, `packages/2-mongo-family/7-runtime/src/mongo-runtime.ts:37-47,99` | Confirmed | +| 20, 26 | `const runtime = db.runtime();` on PostgreSQL, `await db.runtime()` on MongoDB | `postgres.ts:66` (`runtime(): Runtime`), `mongo.ts:38` (`runtime(): Promise`) | Confirmed | +| 21 | `db.orm.public.User.where((u) => u.kind.eq('admin')).all()` | `packages/3-extensions/sql-orm-client/src/collection.ts:319-325`; `test/integration/test/sql-orm-client/mn-include.test.ts:257` | Confirmed | +| 22 | `db.sql.public.user.select('id','email').where((f, fns) => fns.eq(f.kind,'admin')).build()` | `packages/2-sql/4-lanes/sql-builder/src/types/select-query.ts:40`, `src/expression.ts:38-41` | Confirmed | +| 22 | `'admin'` is the stored value of the `user_type` enum | example schema in `sql-query-builder.mdx:17-40` (member with no `=`) | Confirmed | +| 26 | `.all()` replaced `findMany` | conventions.md; `collection.ts` doc comments | Confirmed | +| 28 | `User` is the model name, `user` the table name | `@@map("user")` in the shared example schema | Confirmed | +| 30 | Two things named `query` | `packages/2-mongo-family/5-query-builders/query-builder/src/query.ts:30`; `sql-runtime.ts` | Confirmed | +| 12 | SQL query builder "is PostgreSQL only" | `@prisma/orm-sqlite` exists, so the builder also runs on SQLite; page-conventions.md sanctions this wording for the docs' PostgreSQL/MongoDB scope | Left as is, noted below | + +### `raw-queries.mdx` + +| Line | Claim | Source | Verdict | +| --- | --- | --- | --- | +| 11 | Two kinds: `fns.raw` / `db.raw.sql`, and `db.raw.collection(...)` / `db.query.rawCommand(...)` | `sql-builder/src/expression.ts:116`, `types/raw-query.ts:165-172`, `orm/src/mongo-raw.ts`, `query-builder/src/query.ts:30` | Confirmed | +| 19 | `.returnsRow(spec).build()` → `runtime.query`; `.affectedCount().build()` → `runtime.execute` | `relational-core/src/expression.ts:304-316,460-482`; `test/integration/test/raw-query.integration.test.ts:117-149` | Confirmed | +| 22 | Type id names the JavaScript type; the property is `codecId` | `types/raw-query.ts:29-36`, `expression.ts:226` | Confirmed | +| 22 | `.returnsRow()` converts, `.returns()` does not | `2-sql/5-runtime/src/codecs/decoding.ts:66-100` (row-spec codecs) vs `sql-builder/src/runtime/builder-base.ts:331-332` (a raw fragment's `returnType` has no `codec`, so no codec is attached) | Confirmed. No test covers the fragment case directly; it follows from the code. | +| 22 | A `numeric` column arrives as a string | `postgres/src/core/codecs.ts:922` (wire form is a decimal string) | Confirmed | +| 22 | A date column gets no conversion by Prisma ORM | same mechanism as the fragment case above | Confirmed | +| 25 | `db.runtime()` direct on PostgreSQL, a promise on MongoDB | `postgres.ts:66`, `mongo.ts:38` | Confirmed | +| 38 | `select()` callback takes `f` and `fns`; `fns.raw` is a tagged template | `sql-builder/src/runtime/builder-base.ts:318-336`, `src/expression.ts:116` | Confirmed | +| 38 | Fragments go in `select()`, `where()`, `orderBy()`, `update()` | `types/select-query.ts:40-53`, `types/table-proxy.ts:132-137` | Confirmed | +| 50 | `.returns()` takes a type id string or `{ codecId, nullable }` | `types/raw-query.ts:126-134`, `expression.ts:430-441` | Confirmed | +| 50 | "`.returns(db.sql.public.user.columns.id)` is a type error; only `.returnsRow(spec)` takes columns" | `types/raw-query.ts:29-36,130-134`: a `ContractColumnRef` is `{ codecId, nullable, [symbol]? }`, which matches the object overload of `.returns()`; nothing excludes it | **Corrected** — both sentences removed. The paragraph now stops after the two accepted forms. | +| 54 | A `.returns('pg/bool@1')` fragment works straight in `where()` | `sql-builder/src/expression.ts:12,38-41` | Confirmed | +| 67 | "Interpolated values are sent as query parameters, so they are safe. Column and table names cannot be parameters, so they would be pasted into the SQL text. Never build an identifier from user input." | `relational-core/src/expression.ts:336-360`: an interpolated string is bound as a `ParamRef`; there is no way to interpolate an identifier at all, so nothing is pasted into the SQL | **Corrected** — now: "The expression itself goes into the fragment, not its rendered SQL, so it stays type-checked. An interpolated value is sent as a query parameter, never pasted into the SQL. A column or table name cannot be interpolated as a string, because a string becomes a parameter." The run-time-chosen-column advice is kept. | +| 84-92 | The bare-value type id table, all seven rows | `packages/3-targets/6-adapters/postgres/src/core/adapter.ts:120-141` | Confirmed | +| 94 | Type id shape `pg/` + name + `@1`, suffixes such as `int8number`, `timestamptz-temporal` | `postgres/src/core/codec-ids.ts` (full list) | Confirmed | +| 94 | `pg/uuid@1`, `pg/numeric@1`, `pg/jsonb@1` exist | `codec-ids.ts:21,37,39` | Confirmed | +| 94 | `db.sql.public.
.columns..codecId` | `sql-builder/test/types/raw-query.types.test-d.ts:113-117` | Confirmed | +| 94 | `contract.json` path `storage.namespaces..entries.table.
.columns..codecId` | `test/integration/test/fixtures/contract.json` (read the tree directly) | Confirmed | +| 94 | `pg/timestamptz-temporal@1` takes a `Temporal.Instant`, not a `Date` | `postgres/src/core/temporal-codecs.ts:139-160`; `authoring.ts:735-741` | Confirmed | +| 94 | "`Temporal` is a global in Node.js 26 and later" | `skills/prisma-8/references/contract.md:118`: Node.js **26.8.2** and later ship `globalThis.Temporal`; 26.8.1 and earlier do not | **Corrected** — "Node.js 26.8.2 and later". | +| 94, 99 | Polyfill `import 'temporal-polyfill/full/global'` | `test/integration/test/setup-temporal.ts:1` | Confirmed | +| 97 | `param` from `@prisma/orm-postgres/relational-core/expression` | `packages/9-public/@prisma/orm-postgres/package.json:127`; `relational-core/src/expression.ts:132` | Confirmed | +| 110-115 | `{ codecId, nullable: true }` object form of `.returns()` | `expression.ts:433-441` | Confirmed | +| 123 | `db.raw` holds `sql` on PostgreSQL, `collection(...)` on MongoDB | `types/raw-query.ts:165-172`, `orm/src/mongo-raw.ts:8-11` | Confirmed | +| 136 | Spec entries are a contract column or a type id; object form for a nullable column | `types/raw-query.ts:50-62`; `test/types/raw-query.types.test-d.ts:20-75` | Confirmed | +| 136 | Missing spec column throws `RUNTIME.RAW_ROW_COLUMN_MISSING` | `2-sql/5-runtime/src/codecs/decoding.ts:316-323`; `raw-query.integration.test.ts:224-233` | Confirmed | +| 136 | `SqlQueryError` with `sqlState`, `23505` for a unique violation | `packages/2-sql/1-core/errors/src/errors.ts:7-56` | Confirmed | +| 150-154 | `postCount: 'pg/int8@1'` reads back as a `bigint` | `raw-query.types.test-d.ts:40-47`; `raw-query.integration.test.ts:167-200` | Confirmed | +| 142-148 | `FROM "user"` with no schema qualifier | the statement is sent verbatim; `public` is on the default `search_path` | Confirmed (a PostgreSQL fact, not a Prisma one) | +| 157 | `.affectedCount()` + `runtime.execute` → `{ affectedRows }` | `raw-query.integration.test.ts:140-149` | Confirmed | +| 157 | `tx.query` / `tx.execute` take the same built queries | `sql-runtime.ts:132-140` (`TransactionContext extends RuntimeQueryable`) | Confirmed | +| 170 | Nesting: interpolate before `.build()`, outer `.build()` only, `.returns` property, no nesting of `.affectedCount()`, `UPDATE ... RETURNING` nests | `expression.ts:296-316,460-482`; `raw-query.types.test-d.ts:89-108` | Confirmed | +| 172-173 | The three similar names | same as above | Confirmed | +| 195-199 | Accepted interpolations; a `Date` is a TypeScript error; the exact error code and message | `relational-core/src/ast/types.ts:44`; `expression.ts:329-360` (message matches character for character) | Confirmed | +| 205 | Nine methods, no raw `find()`, no `public` segment, no MongoDB transactions | `orm/src/raw-collection.ts:22-39`; `mongo-raw.ts`; `transactions-and-runtime.mdx:555` | Confirmed | +| 207 | Every method, writes included, runs through `runtime.query(...)` and yields a one-element array | `mongo-runtime.ts:37-47` — `execute()` supports only the typed `updateOne/updateMany/deleteOne/deleteMany` kinds and throws `RUNTIME.MONGO_STATISTICS_UNSUPPORTED` for the `raw*` kinds; `7-runtime/test/raw-commands.test.ts` runs every raw write through `query()` | Confirmed | +| 210 | "The write methods give you the `mongodb` package's own result objects" | `packages/3-mongo-target/3-mongo-driver/src/mongo-driver.ts:118-175` builds new objects from the driver's fields, and reshapes `insertedIds` from a map to an array | **Corrected** — "give you the counts and ids under the `mongodb` package's own key names". The listed shapes were already right. | +| 210 | `_id` is an `ObjectId`, compare with `String(...)` | `mongo-driver.ts:176-200` (documents pass through undecoded) | Confirmed | +| 228-233 | `aggregate()` sends the pipeline unchanged, so a real `ObjectId` matches | `mongo-adapter.ts:285` (`rawAggregate` passes `draft.pipeline` through) | Confirmed | +| 238-248 | `{ insertedId }`; `{ insertedCount, insertedIds }` as an array in input order | `mongo-driver.ts:118-146` | Confirmed | +| 253-270 | `updateOne/updateMany`, document or pipeline form, `{ matchedCount, modifiedCount, upsertedCount, upsertedId }` | `raw-collection.ts:26-29`; `mongo-driver.ts:126-158`; `7-runtime/test/raw-commands.test.ts:69-150` | Confirmed | +| 275-284 | `deleteOne/deleteMany` → `{ deletedCount }` | `mongo-driver.ts:160-174` | Confirmed | +| 293 | `findOneAndUpdate()` takes only `{ upsert }`; TypeScript rejects `sort` or `returnDocument` | `raw-collection.ts:33-37,81-90` | Confirmed | +| 295-302 | `RawFindOneAndUpdateCommand` and its six arguments, the optional ones, the pre-image default | `query-ast/src/raw-commands.ts:96-127` | Confirmed | +| 305 | Import from `@prisma/orm-mongo/query-ast/execution` | `packages/9-public/@prisma/orm-mongo/package.json:96`; `query-ast/src/exports/execution.ts:41-51` | Confirmed | +| 314-323 | Upsert counter: first call gives `[]`, then `count: 1`, then `count: 2`; a plain string `_id` is allowed | `mongo-driver.ts:176-187` (`if (result) yield`); `7-runtime/test/raw-commands.test.ts:113-127` uses `_id: 'pageViews'` | Confirmed. The test pins `returnDocument: 'after'`; the `[]`-then-`1`-then-`2` sequence follows from the code, and no test covers it. | +| 328-334 | `findOneAndDelete()` yields the raw document; `String(deleted._id) === aliceId` | `mongo-driver.ts:189-200`; `7-runtime/test/raw-commands.test.ts:186-196` | Confirmed | +| 339 | "runs a raw MongoDB **aggregate** command"; "takes no type parameter" | `query-builder/src/query.ts:30,44-59`: `rawCommand` accepts any `AnyMongoCommand` (the page's own example passes a `RawFindOneAndUpdateCommand`), and it does have a type parameter, just not one for the row | **Corrected** — "runs a raw MongoDB command"; "has no row type parameter". | +| 339 | Returns a built query, so no `.build()`; rows are `unknown` | `query.ts:44-59` | Confirmed | +| 349-353 | `rawCommand()` sends the command unchanged, so a real `ObjectId` matches | `mongo-adapter.ts:285` | Confirmed | +| Cross-page links | `#example-schema` (both), `#select`, `#fnsraw-and-returns`, `#dbtransactioncallback`, `#transactions-mongodb`, `#rawcommand` | the target headings in the four sibling pages | Confirmed | + +## 2. Q list (unverifiable) + +None. Every claim resolved for or against. + +Two claims rest on code rather than a test, which I state above rather than marking Q: a raw fragment's `.returns()` not converting the value, and the first upsert-counter call returning `[]`. + +## 3. Page-conventions' last bullet + +The last bullet says the error-reference page is generated upstream and out of scope. Neither of my slices touches it. The two error codes they name, `RUNTIME.RAW_SQL_UNSUPPORTED_INTERPOLATION` and `RUNTIME.RAW_ROW_COLUMN_MISSING`, are written in the prescribed "throws an error whose `code` is `X`" form and match the source strings exactly. + +## 4. Line counts + +| File | Before | After | +| --- | --- | --- | +| `wip/c21/ref/fc2/raw-queries.mdx` | 354 | 354 | +| `wip/c21/ref/fc2/index.mdx` | 41 | 41 | + +`check-plain.sh` is clean on both. + +## 5. One item for the operator + +The index page says the SQL query builder "is PostgreSQL only". The source tree ships `@prisma/orm-sqlite`, so the builder also runs on SQLite. I left the wording alone because `page-conventions.md` names "PostgreSQL only" as the approved phrasing, and the same sentence predates these rounds. If SQLite is meant to be visible in the docs, this sentence and its twin on the SQL query builder page both need a decision. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/factcheck2.sql-query-builder.md b/docs/orm-docs-audit/c21-orm-reference/factcheck2.sql-query-builder.md new file mode 100644 index 00000000000..70cde08d3af --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-reference/factcheck2.sql-query-builder.md @@ -0,0 +1,79 @@ +Fact re-check done on `wip/c21/ref/fc2/sql-query-builder.mdx`. Five corrections, 1008 lines before and after. + +## Claims checked + +| Line | Claim | Deciding source | Verdict | +|---|---|---|---| +| 131 | `npm install @prisma/orm-postgres`; `contract emit` writes `contract.json` + `contract.d.ts`; files in `src/prisma/`; type import `./contract.d` | `packages/9-public/@prisma/orm-postgres/package.json:2`; `packages/1-framework/3-tooling/cli/test/control-api/contract-emit.test.ts:282-283`; `packages/3-extensions/postgres/README.md:33-38` | Confirmed | +| 133 | `public` is the PostgreSQL schema | `packages/3-targets/6-adapters/postgres/.../contract.ts` default namespace `public`; `packages/2-sql/2-authoring/contract-psl/src/interpreter.ts:1172` | Confirmed | +| 133 | "Prisma ORM 8 puts every table in `public`" | `packages/1-framework/2-authoring/psl-parser/src/parse.ts:519` (`namespace` blocks); `test/integration/test/authoring/parity/native-enum/schema.prisma:3,15` (`public` and `auth`); `interpreter.ts:1172` (default only) | **Corrected** → "A model goes in `public` unless your contract puts it in a `namespace` block." | +| 133 | Table accessor keyed by table name; `@@map`; `@map` columns | `sql-builder/src/runtime/sql.ts:38-55`, `table-proxy-impl.ts` | Confirmed | +| 137 | `db.runtime()`; `db.close()` | `packages/3-extensions/postgres/src/runtime/postgres.ts:66,72,374` | Confirmed | +| 139, 146-150 | `postgres({ contractJson, url })`; synchronous `runtime()`; build then `query` | `postgres.ts:86-105,266`; `builder-base.ts:buildPlan` | Confirmed | +| 139 | A built query can run more than once | plan is a frozen data object; `query()` returns a new `AsyncIterableResult` each call (`runtime-scope.ts:24`) | Confirmed | +| 153 | SQLite: `sqlite(...)` from `@prisma/orm-sqlite/runtime`; no `public` segment; `lateralJoin`/`outerLateralJoin`/`distinctOn` absent, everything else present | `packages/9-public/@prisma/orm-sqlite/README.md:24`; `packages/3-extensions/sqlite/src/static/sqlite-static.ts:33-38` (unbound namespace); `packages/3-targets/6-adapters/sqlite/src/core/adapter.ts:76-84` (`lateral: false`, no `postgres` namespace, `returning: true`) | Confirmed | +| 166, 505-519 | `.as()` before `build()`; join source; not inside `lateralJoin`; not buildable; SELECT and grouped only | `query-impl.ts:150-168` (`as()` returns only `getJoinOuterScope`/`buildAst`); `types/shared.ts` `WithAlias` on SelectQuery/GroupedQuery, absent on Insert/Update/Delete | Confirmed | +| 170 | Join must precede calls naming joined columns | scope grows only at join (`joined-tables-impl.ts:#addJoin`) | Confirmed | +| 170, 445 | `distinctOn()` columns must be the first `orderBy()` keys; later keys pick the kept row | nothing in the builder checks it (`builder-base.ts:resolveDistinctOn`) | **Q** (PostgreSQL rule, see Q list) | +| 172-174 | Scope; `f` per column; after a join columns under table name, unique names also bare | `scope.ts:MergeScopes`, `builder-base.ts:mergeScopes`, `field-proxy.ts` | Confirmed | +| 174 | Enum: pass the stored value; `High = "high"` → `'high'`; bare `admin` → `'admin'` | `contract-psl/test/interpreter.enum.test.ts:126-133` and `:624-648` (bare members store their names) | Confirmed | +| 186 | Unknown `select()` name throws `ORM.COLUMN_UNKNOWN` | `builder-base.ts:resolveSelectArgs`; `test/runtime/structured-errors.test.ts:59-68` | Confirmed | +| 187-188 | Aliased-expression and object forms | `types/shared.ts:WithSelect` | Confirmed | +| 190 | `select()` adds, never replaces | `query-impl.ts:select` (`rowFields: {...old, ...new}`, projections appended) | Confirmed | +| 192 | Type ids `pg/text@1`, `pg/int4@1`, `pg/int8@1`, `pg/float8@1`, `pg/bool@1`, `pg/uuid@1`, `pg/timestamptz-temporal@1`; `DateTime` → timestamptz-temporal; read from `.columns..codecId` | `postgres/src/core/codec-ids.ts:8-40`; `contract-psl/test/ts-psl-parity.test.ts:217`; `table-proxy-impl.ts:get columns` | Confirmed | +| 281 | `innerJoin()` scope wording | as line 172-174 | Confirmed | +| 312 | `outerLeftJoin`/`outerRightJoin`/`outerFullJoin` = LEFT/RIGHT/FULL OUTER JOIN | `joined-tables-impl.ts` join types; `postgres/src/core/sql-renderer.ts:804-809` renders `LEFT JOIN` (SQL synonym of `LEFT OUTER JOIN`) | Confirmed | +| 334, 444 | SQLite rejects `lateralJoin`/`distinctOn` in TypeScript; a cast call throws `ORM.CAPABILITY_MISSING` | `scope.ts:GatedMethod`; `builder-base.ts:assertCapability`; `test/runtime/structured-errors.test.ts:83-96`; `builders.test.ts:365-401` | Confirmed | +| 335-337, 357-371 | `lateral.from(...)`, return the chain directly, alias scoping, ambiguous bare names throw | `joined-tables-impl.ts:#buildLateral`; `builders.test.ts:373-389` | Confirmed | +| 338 | `outerLateralJoin(alias, callback)` is `LEFT JOIN LATERAL` | `joined-tables-impl.ts` (`'left'` + lateral); `sql-renderer.ts:804-809` | Confirmed | +| 385-391 | `nulls` accepted by TypeScript, never reaches the SQL; CASE-fragment workaround | `expression.ts:OrderByOptions`; `builder-base.ts:resolveOrderBy` (only `asc`/`desc`); no `NULLS` string anywhere in `packages` | Confirmed | +| 420, 433 | `distinct()` is `SELECT DISTINCT` | `sql-renderer.ts:204,286-291` | Confirmed | +| 482-483 | `limit(n)` / `offset(n)` take `number` only | `types/shared.ts:PaginationValue` = `number` \| numeric `TraitExpression`; `query-impl.ts:limit/offset` | **Corrected** → "`number`, or a numeric expression" (restores the pre-round wording) | +| 540 | Aggregates in `select()` first, then `groupBy()` | `types/grouped-query.ts` has no `select()` | Confirmed | +| 583-587 | `fns.count(f.id)` grouped example | `test/integration/test/sql-builder/group-by.test.ts:8-36` | Confirmed | +| 591 | `count()`/integer `sum()` past 2^53-1 throws `RUNTIME.DECODE_FAILED` | `postgres/src/core/codec-helpers.ts:152-196` | Confirmed | +| 592 | `avg()` is float8, `avgDecimal` exact | `postgres/src/core/aggregates.ts` (`avg` → float8 via cast; `avgDecimal` → numeric) | Confirmed | +| 593, 597-599 | Errors are `Error` with `code` | `utils/structured-error`, `postgres/src/core/errors.ts` | Confirmed | +| 606-617 | `EXTRACT` typed `pg/numeric@1` arrives as a string; `groupBy` by alias; repeat the fragment to group by the expression | `expression.ts:RawSqlBuilderImpl.returns` (no codec on the projection, so no decode); `codecs.ts:896-907` (numeric decodes to string); `builder-base.ts:resolveGroupBy` (alias in scope via `orderByScopeOf`) | Confirmed | +| 620 | Unknown `groupBy` name throws `ORM.COLUMN_UNKNOWN` at call; ungrouped column fails at run time with PostgreSQL's own error | `resolveGroupBy`; nothing validates grouping | Confirmed | +| 628 | `count()` vs `1n` → `RUNTIME.ENCODE_FAILED`; `countBigInt()` compares against `1n` | `codec-helpers.ts:77-84,174-176`; aggregate output codecs `pg/int8number@1` vs `pg/int8@1` | Confirmed | +| 629 | Selected alias in `having()` accepted by TypeScript, rejected by PostgreSQL | `grouped-query.ts:having` takes `OrderByScope` (aliases included); rejection is PostgreSQL's | Partly Q (see Q list) | +| 652, 667 | `having` with `fns.gt(fns.sum(...), 1000)` / `fns.gt(fns.count(), 1)` | `group-by.test.ts:38-50` | Confirmed | +| 674 | "The alias must be one you selected. Any other name throws `ORM.COLUMN_UNKNOWN`" | `builder-base.ts:resolveOrderBy` also accepts any base-table column in scope | **Corrected** → "A name that is neither a column of the table you started from nor an alias you selected throws…" (matches the `groupBy()` wording at line 620) | +| 693-694 | `where()` optional on `update()`/`delete()` | `mutation-query.ts` (`where` optional in the chain) | Confirmed | +| 697 | `execute` → `{ affectedRows }`, `query` with `returning()` | `relational-core/src/runtime-scope.ts:24-25`; `mutation.test.ts:103-109` (`toEqual({ affectedRows: 1 })`) | Confirmed | +| 705 | `insert()` always takes an array; empty array → `ORM.MUTATION_DATA_MISSING` | `mutation-impl.ts:254-260` (thrown at `build()`) | Confirmed | +| 706 | Defaults applied at `build()`; re-running inserts the same id | `mutation-impl.ts:253-270` (`buildParamValues` inside `build()`); `mutation-defaults.test.ts:8-22` | Confirmed | +| 707 | No `ON CONFLICT`, no upsert | no `conflict` handling in `mutation-impl.ts` or `InsertAst` | Confirmed | +| 713-715 | Every column optional in TypeScript; PostgreSQL still rejects a missing required column | `mutation-query.ts:InsertValues` (all optional) | Confirmed / database side Q | +| 745-746 | `returning()` on PostgreSQL and SQLite; column names only, no `'*'`, no expression | `mutation-query.ts:ReturningCapability`; both adapters set `sql.returning: true`; `mutation-impl.ts:202-226` (names resolved against scope) | Confirmed | +| 775-782 | `update()` object form or callback, cannot mix; callback values must be expressions; `fns.raw` for a fixed value | `table-proxy.ts:update` overloads; `table-proxy-impl.ts:update`; `mutation-impl.ts:evaluateUpdateCallback` | Confirmed | +| 819 | `delete()` takes no arguments | `table-proxy.ts:delete(): DeleteQuery` | Confirmed | +| 846 | `param` from `@prisma/orm-postgres/relational-core/expression` | export map `./relational-core/expression`; `relational-core/src/expression.ts:132` | Confirmed | +| 847 | "A bare string becomes `pg/text@1`, so a value going into a `uuid` column needs `param(...)`" | `functions.ts:binaryWithSharedCodec` threads the column codec, so a comparison needs no `param()`; only `fns.raw` interpolation infers `pg/text@1` (`expression.ts:336-354`, `postgres adapter.ts:121-140`) | **Corrected** → scoped to "Inside `fns.raw` no column lends its type, so …" | +| 847 | `Temporal.Instant` cannot be interpolated bare | `postgres/src/core/adapter.ts:132-139` (`RUNTIME.RAW_SQL_UNSUPPORTED_INTERPOLATION`) | Confirmed | +| 855, 861, 870 | `param(value, { codecId })` → `ParamRef`, usable in `fns.raw` | `expression.ts:132-134` | Confirmed | +| 888-894, 896-911 | Comparison/membership/boolean/existence signatures; `in`/`notIn` take an array or an unbuilt subquery; `exists`/`notExists` an unbuilt subquery | `expression.ts:BuiltinFunctions`; `functions.ts:inOrNotIn`; `test/integration/test/sql-builder/subquery.test.ts:6-47` | Confirmed | +| 917-924 | Aggregate table: `count` number, `countBigInt` bigint, `sum` number over int4/int8, string over numeric, number over float8, `sumBigInt` bigint, `avg` number, `avgDecimal` exact string, `min`/`max` input type with `VarChar` → `text` | `postgres/src/core/aggregates.ts:152-228`; `codecs.ts:896-907` (numeric → string); `control-mutation-defaults.ts:163-206,275` (Int/BigInt/Float/Decimal/Numeric/Uuid mappings) | Confirmed | +| 926 | `ilike(expr, pattern)` on text; `cosineDistance(a, b)` → number, pgvector only | `postgres/src/core/descriptor-meta.ts:155-168`; `pgvector/src/core/descriptor-meta.ts:19-36` (float8) | Confirmed | +| 930, 941-943 | `fns.raw` interpolation; `.returns()` always required; annotation only, no conversion; no `fns.coalesce`/`fns.cast` | `expression.ts:RawSqlBuilderImpl` (the Expression exists only after `.returns()`, and carries no codec); `BuiltinFunctions` has no coalesce/cast | Confirmed | +| 937 | `.returns(typeId)` type is `string` | `expression.ts:RawSqlBuilder` also takes `{ codecId, nullable }`; raw-queries.mdx:50 says so | **Corrected** → "`string`, or `{ codecId, nullable }`" | +| 963 | `build()` takes zero arguments on every query type | `types/shared.ts:WithBuild`, `mutation-query.ts` `build()` | Confirmed | +| 977-979 | `query` → rows (await or `for await`), `execute` → `{ affectedRows }`, which to use | `runtime-scope.ts:23-26` | Confirmed | +| 987-989 | `ResultType` from `@prisma/orm-postgres/components/runtime`; one row; works on ORM queries | `framework-components/src/exports/runtime.ts:14`, `execution/query-plan.ts:49`; `sql-orm-client/src/collection.ts:225` (`_row`); `docs/releases/v8.0.0-rc.10.md:27` | Confirmed | +| 1006 | `for await` gives no memory benefit on PostgreSQL; "use `await`" | `postgres.ts:246` (`cursor: { disabled: true }`) | Confirmed | +| 1008 | Re-awaiting is safe; mixing modes or looping twice throws `RUNTIME.ITERATOR_CONSUMED` | `framework-components/src/execution/async-iterable-result.ts:13-60` | Confirmed | + +## Q list (unverifiable from the source) + +These are PostgreSQL server behaviours the Prisma source neither implements nor tests. Page text left alone. + +1. `distinctOn()` columns must be the leftmost `orderBy()` keys, and later keys decide the kept row (lines 170, 445). Searches: `grep -rn "distinctOn" packages --include='*.ts'`, `grep -rn "DISTINCT ON" packages`, read `builder-base.ts:resolveDistinctOn` and `sql-renderer.ts:286-291`. Nothing validates or documents the ordering rule; the builder renders whatever you give it. +2. PostgreSQL rejects a selected alias inside `HAVING` (line 629). Searches: `grep -rn "having" packages/2-sql --include='*.ts'`, `group-by.test.ts`. No test uses an alias in `having()`; the type surface allows it. +3. PostgreSQL rejects an insert that omits a required column with no default (line 715). Searches: `grep -rn "MUTATION_DATA_MISSING\|NOT NULL" packages --include='*.ts'`. No Prisma-side check; it is the database's error. + +## Notes + +- The examples on this page are not copies of the SQL-builder integration tests: those run against a different fixture (`test/integration/test/sql-builder/fixtures/contract.ts` — `users`/`posts`/`comments` with `int4` ids). The page uses the shared reference example schema instead. I checked every example for call shape and argument type against the source rather than against fixture data. The page makes no claim of being copied from a test suite, so no change. +- Two accurate-but-incomplete spots I left alone because fixing them would lengthen the page: `distinctOn()`'s Options table lists column names only, while the source (`select-query.ts`) also accepts an expression callback; and `orderBy()`'s `nulls` option is described in prose only. +- The page no longer states that `db.runtime()` is synchronous on PostgreSQL (a round removed it). The `page-conventions.md` bullet asks for that sentence where `runtime()` first appears. The example at line 147 does not `await` it, so nothing is wrong, but the operator may want the sentence back. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/factcheck2.transactions-and-runtime.md b/docs/orm-docs-audit/c21-orm-reference/factcheck2.transactions-and-runtime.md new file mode 100644 index 00000000000..a006337bd5a --- /dev/null +++ b/docs/orm-docs-audit/c21-orm-reference/factcheck2.transactions-and-runtime.md @@ -0,0 +1,83 @@ +Fact re-check complete. Slice: `wip/c21/ref/fc2/transactions-and-runtime.mdx` (771 lines before, 770 after). + +## Claims checked + +| Slice line | Claim | Source | Verdict | +|---|---|---|---| +| 9 | `contract emit` writes `contract.json` + `contract.d.ts`; `findMany` → `db.orm.public.User.where(...).all()` | `cli/src/commands/init/templates/code-templates.ts:318-345`; `test/integration/test/cli-journeys/init-journey.e2e.test.ts:166-186` | Confirmed | +| 11 | `db` is what `postgres(...)` / `mongo(...)` return; `runtime()` runs queries | `3-extensions/postgres/src/runtime/postgres.ts:155-384`, `mongo.ts:116-240` | Confirmed | +| 17 | "Transactions are PostgreSQL only" | `3-extensions/sqlite/src/runtime/sqlite.ts:83` has `transaction()` | **Corrected** → "Transactions are not available on MongoDB. To run one there, …" | +| 17 | MongoDB route: `mongoClient` option + `session.withTransaction(...)` | `mongo.ts:56,197-214`; `projects/sqlite-mongo-transactions/spec.md:20` | Confirmed | +| 29 | `contractJson` or `contract`, exactly one | `postgres.ts:92-109` (`contract?: never` / `contractJson?: never`) | Confirmed | +| 30 | `` needed with `contractJson`, inferred with `contract`; `contract.d.ts` exports `Contract` | `postgres.ts:99-105,155-160`; init e2e test line 167 | Confirmed | +| 31 | Three ways (`url`, `pg`, `binding`); binding kinds `url` / `pgPool` / `pgClient`; exactly one, or pass to `connect()` | `postgres/src/runtime/binding.ts:11-31,67-127` | Confirmed | +| 32 | You own a `pg` pool; `poolOptions` only affects a pool built from `url` | `postgres.ts:128-146,194-220,374-379` | Confirmed (order "`db.close()` first, then `pool.end()`" is advice the source neither states nor contradicts) | +| 33 | pgvector: install, import `/runtime`, `extensions: [pgvector]` | `test/e2e/framework/test/transaction-orm.test.ts:5,35` | Confirmed | +| 34 | Middleware object with `name` + hooks; `query.sql`; eight hook names | `framework-components/src/execution/runtime-middleware.ts:172,200,235-240`; `2-sql/5-runtime/src/middleware/sql-middleware.ts:49,75,80,84,89,119,124,128`; `relational-core/src/sql-execution-plan.ts:24` | Confirmed | +| 35 | `verifyMarker` default `onFirstUse`, warns and runs anyway; `npx prisma db update` | `sql-runtime.ts:214-216,898-929`; `cli/src/control-api/operations/db-verify.ts:348` | Confirmed | +| 36 | `postgres()` does not connect; connects on first use | `postgres.ts:222-262` | Confirmed | +| 45-51 | Options table (`url`, `pg`, `binding`, `poolOptions` 20000/30000, `extensions`, `middleware`, `verifyMarker`) | `postgres.ts:76-90,140-142` | Confirmed | +| 59-62 | Client members `.orm`, `.sql`, `.raw`, `.enums`, `.nativeEnums`, `.contract` | `postgres.ts:56-74` | Confirmed (the client also has `.context` and `.stack`, which the page no longer lists) | +| 61 | `db.nativeEnums.public['AalLevel'].values` is `['aal1','aal2']` | `test/integration/test/authoring/parity/native-enum/expected.contract.json:96-146` — `AalLevel` is in namespace `auth`, values `aal1, aal2, aal3`; `contract/src/enum-accessor.ts:16-17` for `.values` | **Corrected** → "`db.nativeEnums.auth['AalLevel'].values` is `['aal1', 'aal2', 'aal3']`" | +| 73, 86 | `process.env.X!` | `code-templates.ts:331` (`process.env['DATABASE_URL']!`) | Confirmed | +| 76 | JSON import line, `resolveJsonModule` / `module: "preserve"` / `moduleResolution: "bundler"` | `cli/src/commands/init/templates/tsconfig.ts:23-27`; `code-templates.ts:327` | Confirmed | +| 76 | "needs Node.js 22.18 or newer" | not in the source | **Q** | +| 96 | "It has five calls" | `sql-runtime.ts:100-113` — `Runtime` also has `close()`, so six members | **Corrected** → "It has these calls:" | +| 97-101 | `query`, `execute` (`{ affectedRows }`), `connection()` + `release()`, `prepare(declaration, callback)`, `telemetry()` | `sql-runtime.ts:100-134`; `relational-core/src/runtime-scope.ts:23-26`; `relational-core/src/ast/driver-types.ts:24-26` | Confirmed | +| 102 | `connect({ url })` / `connect({ binding: { kind: 'url', url } })` | `postgres.ts:289-324`; `binding.ts:16-31` | Confirmed | +| 103 | Errors carry `code`; compare `error.code` | `framework-components/src/shared/runtime-error.ts:32-44` | Confirmed | +| 104 | A query connects, so a later `connect()` throws `DRIVER.ALREADY_CONNECTED`; a second `connect()` too | `postgres.ts:249-251,298-303` | Confirmed | +| 127 | PostgreSQL `runtime()` is synchronous, callable before connecting | `postgres.ts:66,326-328,222-262` | Confirmed | +| 141-142 | `close()` safe at any time; then `connect()` rejects and `runtime()` throws `DRIVER.NOT_CONNECTED` (`Postgres client is closed`) | `postgres.ts:223-229,290-296,374-376`; `postgres/test/postgres-close.test.ts:134-144` | Confirmed | +| 156 | `db.orm.public.User.where((u) => u.kind.eq('admin')).all()` | example schema in `orm-client.mdx` (`kind user_type`, values `admin`/`user`) | Confirmed | +| 162-167 | `Symbol.asyncDispose`, block-scoped disposal | `postgres.ts:381-383`; `postgres/test/postgres-close.test.ts:146-155` | Confirmed | +| 167 | `"esnext.disposable"` lib name | not in the source (a TypeScript fact) | **Q** | +| 190-194 | MongoDB contract options; four binding ways; kinds `url` / `mongoClient` with `dbName`; `dbName` separate; ownership; no connection on create | `3-extensions/mongo/src/runtime/binding.ts:5-41,82-151`; `mongo.ts:120-145,220-235` | Confirmed | +| 192 | "Use `uri` only when you must pass `dbName` separately" | `binding.ts:101-138` — `{ url, dbName }` is also allowed, which the same bullet says | **Corrected** → "`uri` takes the same kind of string, and always needs `dbName`." | +| 203-208 | MongoDB options table; `mode` ignored by Prisma ORM, passed to middleware | `mongo.ts:43-57,139-144`; `2-mongo-family/7-runtime/src/mongo-runtime.ts:120` (only fills the middleware context) | Confirmed | +| 214-218 | `MongoClient` members `.orm`, `.query`, `.raw`, `.enums`, `.contract`; `@@map("users")` | `mongo.ts:28-41`; `orm-client.mdx` MongoDB schema | Confirmed | +| 251-252 | MongoDB `connect()` returns `Promise`; `DRIVER.ALREADY_CONNECTED` after a query or a second call; takes the same options | `mongo.ts:147-169,197-214` | Confirmed | +| 266-267 | MongoDB `runtime()` returns a promise; `MongoRuntime` is `query`, `execute`, `close` only | `mongo.ts:216-218`; `mongo-runtime.ts:76-101` | Confirmed | +| 281-283 | `runtime.query(built)`; returns `AsyncIterableResult`; `runtime.execute(built)` returns `{ affectedRows }` for "a write that returns no rows" | `mongo-runtime.ts:37-48,270-307` — `execute` only supports `updateOne`, `updateMany`, `deleteOne`, `deleteMany`; anything else throws `RUNTIME.MONGO_STATISTICS_UNSUPPORTED` | **Corrected** → "For an update or delete command, use `runtime.execute(built)` … Other commands throw an error whose `code` is `RUNTIME.MONGO_STATISTICS_UNSUPPORTED`." | +| 299 (old) | Example ran `execute(built)` on the pipeline built by `db.query.from('posts')` | same as above: an aggregate command has no statistics field | **Corrected** → line removed | +| 296 | `'posts'` is the collection key | `orm-client.mdx` MongoDB schema `@@map("posts")` | Confirmed | +| 308-309 | Always `db.close()`; `runtime.close()` leaves the client looking open; after `close()` everything rejects `DRIVER.NOT_CONNECTED`; a supplied `mongoClient` stays open | `mongo.ts:128,147-150,220-235`; `mongo-runtime.ts:309-311` | Confirmed from the code; no test covers `runtime.close()` on the client | +| 332 | MongoDB `await using` | `mongo.ts:237-239`; `mongo/test/mongo.test.ts:448` | Confirmed | +| 355 | `tx.orm`, `tx.sql`, `tx.query`, `tx.execute` → `{ affectedRows }`; same connection | `postgres.ts:341-372`; `sql-runtime.ts:139-141`; `driver-types.ts:24-26` | Confirmed | +| 356 | No options, no isolation level, no retries, no time limit; `idle_in_transaction_session_timeout` | `postgres.ts:67,341`; `sql-runtime.ts:960-1087` (no retry) | Confirmed, except the PostgreSQL setting name, which is not in the source → **Q** | +| 357 | `signal` per call limits a statement | `runtime-scope.ts:23-26`; `runtime-core.ts:107-147` | Confirmed | +| 358 | No nesting; `TransactionContext` from `@prisma/orm-postgres/family-runtime` with `query`/`execute`; derived `Tx` type | `sql-runtime.ts:139-141`; `2-sql/5-runtime/src/exports/index.ts:66-74`; `orm-postgres/package.json` `./family-runtime` | Confirmed (`TransactionContext` also has a read-only `invalidated` property, which the page does not mention) | +| 360-363 | Read-your-own-writes; `tx.enums` / `tx.nativeEnums` and `.values`; return value passes through; escaped result throws `RUNTIME.TRANSACTION_CLOSED` | `postgres.ts:365-368`; `sql-runtime.ts:960-1050`; `test/e2e/framework/test/transaction-orm.test.ts:101-115`, `transaction.test.ts:72-83` | Confirmed | +| 382-411 | Commit, rollback, read-your-own-writes examples | `transaction.test.ts:13-44`, `transaction-orm.test.ts:101-115` | Confirmed | +| 417-421 | `AbortSignal.timeout` passed as the second argument to `tx.execute` | `runtime-scope.ts:25` | Confirmed | +| 425-435 | Escaped-result warning and example | `sql-runtime.ts:966-996`; `transaction.test.ts:72-83` | Confirmed | +| 446 | `tx.orm.public.User.create({ email, displayName })` | example schema: `kind user_type` is required and has no default; `orm-client.mdx:976-980` passes `kind` | **Corrected** → added `kind: 'user'` | +| 460-462 | `withTransaction` and `Runtime` from `/family-runtime`; handle has `query`/`execute` only; commits on return, rolls back on throw | `exports/index.ts:62-74`; `sql-runtime.ts:960-1087`; `transaction-orm.test.ts:6` | Confirmed | +| 484-491 | Helper signature taking `sql: typeof db.sql` | `postgres.ts:57` (`sql` is `Db`) | Confirmed (no test uses this exact helper shape) | +| 500-501 | `connection()`, `transaction()`, `commit()`, `rollback()`, `release()`, `destroy()` | `sql-runtime.ts:101,116-134` | Confirmed (`destroy(reason?)` takes an optional advisory argument the page no longer shows) | +| 518-552 | Manual-control examples, including the read on the connection before the transaction | `sql-runtime.ts:116-134`; `relational-core/src/runtime-scope.ts:23-26` | Confirmed (no integration test covers these exact snippets) | +| 558 | No MongoDB `db.transaction`; planned, not shipped | `mongo.ts:28-41`; `projects/sqlite-mongo-transactions/spec.md:20,128` | Confirmed | +| 562 | "Prepared statements are PostgreSQL only" | `sqlite/src/runtime/sqlite.ts:79,285` has `prepare` | **Corrected** → "Prepared statements are not available on MongoDB." | +| 570-573 | Declaration maps names to type ids; `columns..codecId`; single-argument callback returning the built query; `where((f, fns) => …)`; `RUNTIME.PREPARE_UNUSED_PARAM` with `details.unused`, at prepare time | `prepared/types.ts:20-51`; `sql-builder/src/types/table-proxy.ts:115-116`, `types/raw-query.ts:29-37`, `expression.ts:38-41`; `sql-runtime.ts:560-574`; `runtime-error.ts:37-41` | Confirmed | +| 595-610 | Prepare examples, including the unused-parameter rejection | `sql-runtime.ts:567-573`; `test/integration/test/raw-prepared.integration.test.ts:133-171` | Confirmed | +| 619-620 | `db.prepare` injects `sql` as the first callback argument; `prepare()` connects, so `db.connect()` throws `DRIVER.ALREADY_CONNECTED` | `postgres.ts:330-339,249-251,298-303` | Confirmed | +| 653-664 | `PreparedExecution` from `.affectedCount()`, `execute(target, params)` → `{ affectedRows }`; raw parameterised example; `target` required; no `close()` | `prepared/types.ts:53-106`; `raw-prepared.integration.test.ts:133-145` | Confirmed | +| 702 | Which calls take `RuntimeExecuteOptions`, and a prepared statement takes it third | `runtime-scope.ts:23-26`; `prepared/types.ts:60-83` | Confirmed | +| 710-712 | `signal`; pre-aborted rejects before any row with `RUNTIME.ABORTED`; `cause` is the signal reason; `details.phase`; an abort inside a transaction rolls it back | `runtime-core.ts:107-147`; `race-against-abort.ts:12-19`; `runtime-error.ts:22-47`; `sql-runtime.ts:1036-1060` | Confirmed | +| 740-744 | Telemetry: PostgreSQL only, `null` before the first query, `{ lane, target, fingerprint, outcome, durationMs? }`, most recent query only, `lane` values, fingerprint from the SQL text, `outcome` values, `durationMs` always set | `sql-runtime.ts:883-885,932-944,423,545,746`; `runtime-spi.ts:44`; `sql-orm-client/src/query-plan-meta.ts:51`; `relational-core/src/plan.ts:38-45` | Confirmed | +| 767-771 | `AsyncIterableResult` from `all()`, `createAll()`, `query()`; `await` vs `for await`; buffering on PostgreSQL; `.toArray()` equals `await`; re-await safe; mixing or a second loop throws `RUNTIME.ITERATOR_CONSUMED`; MongoDB shares the rules | `framework-components/src/execution/async-iterable-result.ts:3-86`; `sql-orm-client/src/collection.ts:1440-1452`; `3-targets/7-drivers/postgres/src/postgres-driver.ts:296-333`; `postgres.ts:245-247` | Confirmed | +| all links | `#executing-a-plan`, `#binding-a-bare-value-with-param`, `#asynciterableresult`, `#example-schema`, `/orm/middleware/how-middleware-works`, `#options-and-isolation-level`, `#transactions-on-mongodb` | the target pages in `apps/docs/content/docs/orm/` | Confirmed | + +## Q list (unverifiable, page text left alone) + +1. **"It needs Node.js 22.18 or newer"** (line 76). Searches: `grep -rn "22\.18\|22\.12\|>=22" --include='*.json' --include='*.md'`, `grep -rn -i "node 22|node.js 22"`, `grep -rn '"engines"' -A3`. The only engine requirement in the tree is `"node": ">=24"` (root `package.json:100`, `cli/package.json:103`), which is the repository's own build requirement, not the version an application needs for JSON import attributes. +2. **`"esnext.disposable"` in `lib`** (line 167). Searches: `grep -rn "esnext.disposable\|asyncDispose" --include='*.json' --include='*.ts'`. The source only shows `Symbol.asyncDispose` implementations and tests; no tsconfig in the tree sets that lib. +3. **`idle_in_transaction_session_timeout`** (line 356). Searches: `grep -rni "idle_in_transaction\|statement_timeout"` over the whole tree. No hit. It is a PostgreSQL server setting, outside this source. + +## Extra item the page conventions asked for + +The last bullet of `page-conventions.md` is about the generated error-reference page being out of scope. My slice only links to it and never quotes its wording, so there is nothing to change. The one code I added, `RUNTIME.MONGO_STATISTICS_UNSUPPORTED`, is present on that page (`error-reference.mdx:734`). + +## One gap the operator should decide on + +`RuntimeExecuteOptions` still has a second field, `scope` (`'runtime' | 'connection' | 'transaction'`), in `framework-components/src/execution/runtime-middleware.ts:335-338`, and `runtime-core.ts:117,141` reads it. A reader-review round deleted both the `scope` table row and the remark about it, so the Options table under `### RuntimeExecuteOptions` now lists only `signal`. Nothing on the page is false, but the table no longer lists every field of the type it documents. I left it alone because the brief says not to make the slice longer; restoring the row costs one line. \ No newline at end of file From 494bd6cbfacc22b247a756bfaf6cbb60eab4d850 Mon Sep 17 00:00:00 2001 From: willbot Date: Sun, 13 Sep 2026 09:45:35 +0200 Subject: [PATCH 11/14] docs: remove the orm/reference review reports before merge They stay in this branch's history at 13f900ad2. Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- .../c21-orm-reference/conventions.md | 24 --- .../factcheck1.pipeline-builder.md | 70 -------- .../factcheck1.raw-queries.md | 52 ------ .../factcheck1.sql-query-builder.md | 71 -------- .../factcheck1.transactions-and-runtime.md | 78 -------- .../factcheck2.pipeline-builder.md | 115 ------------ .../factcheck2.raw-queries.md | 99 ---------- .../factcheck2.sql-query-builder.md | 79 -------- .../factcheck2.transactions-and-runtime.md | 83 --------- .../c21-orm-reference/page-conventions.md | 12 -- .../pipeline-builder.part1.round1.md | 99 ---------- .../pipeline-builder.part1.round2.md | 101 ----------- .../pipeline-builder.part1.round3.md | 100 ----------- .../pipeline-builder.part1.round4.md | 133 -------------- .../pipeline-builder.part2.round1.md | 124 ------------- .../pipeline-builder.part2.round2.md | 119 ------------ .../pipeline-builder.part2.round3.md | 59 ------ .../pipeline-builder.part2.round4.md | 112 ------------ .../raw-queries-and-index.round1.md | 63 ------- .../raw-queries-and-index.round2.md | 169 ------------------ .../raw-queries-and-index.round3.md | 119 ------------ .../raw-queries-and-index.round4.md | 146 --------------- .../sql-query-builder.part1.round1.md | 81 --------- .../sql-query-builder.part1.round2.md | 97 ---------- .../sql-query-builder.part1.round3.md | 96 ---------- .../sql-query-builder.part1.round4.md | 75 -------- .../sql-query-builder.part2.round1.md | 94 ---------- .../sql-query-builder.part2.round2.md | 86 --------- .../sql-query-builder.part2.round3.md | 85 --------- .../sql-query-builder.part2.round4.md | 66 ------- .../transactions-and-runtime.part1.round1.md | 97 ---------- .../transactions-and-runtime.part1.round2.md | 89 --------- .../transactions-and-runtime.part1.round3.md | 93 ---------- .../transactions-and-runtime.part1.round4.md | 83 --------- .../transactions-and-runtime.part2.round1.md | 92 ---------- .../transactions-and-runtime.part2.round2.md | 88 --------- .../transactions-and-runtime.part2.round3.md | 98 ---------- .../transactions-and-runtime.part2.round4.md | 81 --------- 38 files changed, 3428 deletions(-) delete mode 100644 docs/orm-docs-audit/c21-orm-reference/conventions.md delete mode 100644 docs/orm-docs-audit/c21-orm-reference/factcheck1.pipeline-builder.md delete mode 100644 docs/orm-docs-audit/c21-orm-reference/factcheck1.raw-queries.md delete mode 100644 docs/orm-docs-audit/c21-orm-reference/factcheck1.sql-query-builder.md delete mode 100644 docs/orm-docs-audit/c21-orm-reference/factcheck1.transactions-and-runtime.md delete mode 100644 docs/orm-docs-audit/c21-orm-reference/factcheck2.pipeline-builder.md delete mode 100644 docs/orm-docs-audit/c21-orm-reference/factcheck2.raw-queries.md delete mode 100644 docs/orm-docs-audit/c21-orm-reference/factcheck2.sql-query-builder.md delete mode 100644 docs/orm-docs-audit/c21-orm-reference/factcheck2.transactions-and-runtime.md delete mode 100644 docs/orm-docs-audit/c21-orm-reference/page-conventions.md delete mode 100644 docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part1.round1.md delete mode 100644 docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part1.round2.md delete mode 100644 docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part1.round3.md delete mode 100644 docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part1.round4.md delete mode 100644 docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part2.round1.md delete mode 100644 docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part2.round2.md delete mode 100644 docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part2.round3.md delete mode 100644 docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part2.round4.md delete mode 100644 docs/orm-docs-audit/c21-orm-reference/raw-queries-and-index.round1.md delete mode 100644 docs/orm-docs-audit/c21-orm-reference/raw-queries-and-index.round2.md delete mode 100644 docs/orm-docs-audit/c21-orm-reference/raw-queries-and-index.round3.md delete mode 100644 docs/orm-docs-audit/c21-orm-reference/raw-queries-and-index.round4.md delete mode 100644 docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part1.round1.md delete mode 100644 docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part1.round2.md delete mode 100644 docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part1.round3.md delete mode 100644 docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part1.round4.md delete mode 100644 docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part2.round1.md delete mode 100644 docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part2.round2.md delete mode 100644 docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part2.round3.md delete mode 100644 docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part2.round4.md delete mode 100644 docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part1.round1.md delete mode 100644 docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part1.round2.md delete mode 100644 docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part1.round3.md delete mode 100644 docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part1.round4.md delete mode 100644 docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part2.round1.md delete mode 100644 docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part2.round2.md delete mode 100644 docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part2.round3.md delete mode 100644 docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part2.round4.md diff --git a/docs/orm-docs-audit/c21-orm-reference/conventions.md b/docs/orm-docs-audit/c21-orm-reference/conventions.md deleted file mode 100644 index 69d9ba86bb8..00000000000 --- a/docs/orm-docs-audit/c21-orm-reference/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`. There is no published list; the raw-queries page shows the ids a bare JavaScript value gets. -- Errors: "an error with code `X`" (it is `error.code`). -- Do not use: terminal, codec, envelope, facade, lane, junction model/table (say "join table" / "model for the join table"), surface, plan, "rides", "fan out", "drop one level", "field proxy" (say "the `p` argument"), "lambda" (say "callback"). -- `npx prisma skills sync` exists in the published CLI (8.0.0-rc.13, newer than the rc.9 source tree): "in an existing project, run `npx prisma skills sync`". Do not drop it because the rc.9 tree lacks it. -- The serverless client (`postgresServerless(...)` from `@prisma/orm-postgres/serverless`) has no `db.orm`, `runtime()`, or `transaction()`; it runs SQL builder and raw queries only, reading rows 100 at a time. Never show `db.orm` on it. diff --git a/docs/orm-docs-audit/c21-orm-reference/factcheck1.pipeline-builder.md b/docs/orm-docs-audit/c21-orm-reference/factcheck1.pipeline-builder.md deleted file mode 100644 index 209010818fc..00000000000 --- a/docs/orm-docs-audit/c21-orm-reference/factcheck1.pipeline-builder.md +++ /dev/null @@ -1,70 +0,0 @@ -Done. Report below. - -## Corrections made (page: `apps/docs/content/docs/orm/reference/pipeline-builder.mdx`) - -| Line (new) | Claim | Source that decides it | Verdict | What the page now says | -|---|---|---|---|---| -| 95 | `from()` takes "the lowercase plural collection name" | `query-builder/src/state-classes.ts:createCollectionHandle`; `contract-psl/src/interpreter.ts:1361` (`roots[collectionName]`) | Corrected | Takes the collection name from the `roots` map: the model's `@@map`, else the model name with a lowercase first letter | -| 96 | Unknown root error | `state-classes.ts` `ormError('ORM.MODEL_UNKNOWN', 'Unknown root: ...')`; `test/structured-error-codes.test.ts:53` | Corrected (wording only) | "throws right away, with an error whose `code` is `ORM.MODEL_UNKNOWN`" | -| 97 | Three builder states, root writes only before stages | `state-classes.ts` (CollectionHandle / FilteredCollection / PipelineChain) | Confirmed | — | -| 126 | `db.runtime()` returns a promise; `AsyncIterableResult`; no `db.execute` | `extensions/mongo/src/runtime/mongo.ts:38`; `framework-components/src/execution/async-iterable-result.ts:3` | Confirmed | — | -| 127–128 | "Read results are decoded"; only lookup sub-documents undecoded | `query-builder/src/pipeline-result-shape.ts:14,80`; `7-runtime/src/codecs/decoding.ts:53`; `mongo-runtime.ts:251` | **Corrected** | Decoding survives only `match`, `sort`, `limit`, `skip`, `sample`, `project`, `addFields`, `vectorSearch`. Every other stage (including `lookup()` and `group()`) drops the result shape and the **whole row**, top-level `_id` included, comes back raw | -| 150 | Filter values "not encoded to the stored type" | `2-mongo-adapter/src/resolve-value.ts:46-93` (only `MongoParamRef` with a codecId is encoded); ORM client wraps values in `MongoParamRef` (`orm/src/collection.ts:744`) | Confirmed | — | -| 151 | Filter operator list `eq ne gt gte lt lte in nin exists type` | `query-builder/src/field-accessor.ts:49-73` | Confirmed (exact match) | — | -| 190–193 | `_id` equality never matches; `ObjectId` walked as a plain object; `rawCommand` / ORM client are the escape hatches | `resolve-value.ts:73-93`; `orm/src/collection.ts:744` | Confirmed, still true at rc.10 | — | -| 202, 231, 237, 262, 287 | `sort`/`limit`/`skip`/`sample` signatures | `builder.ts:199-238` | Confirmed | — | -| 310, 344, 384–385 | `addFields`, `lookup(from().on().as())`, `project` two forms, implicit `_id` | `builder.ts:251,285,326`; `lookup-builder.ts` | Confirmed | — | -| 428–429 | `unwind()` "not exercised, verify it yourself" | `test/integration/test/mongo/query-builder.test.ts:395` runs `unwind('tags')` live | **Corrected** | Caveat removed; states the behaviour instead | -| 450–453 | `group()` single-argument callback; `acc` namespace; non-accumulator is "a compile error … throws at build time" | `builder.ts:395-430`; `types.ts:181` (the index signature allows a plain expression); `test/builder.test.ts:206` | **Corrected** | Throws when you call `group()`, `code` `ORM.ARGUMENT_INVALID`, exact message given; `null` for a non-`_id` key throws the same way. The "compile error" claim was dropped | -| 513 | `replaceRoot` rejects a bare scalar | MongoDB server message, not in the tree | **Q** | unchanged | -| 581 | `sortByCount` emits `{ _id, count }` sorted descending | `builder.ts:455`; integration test:514 | Confirmed | — | -| 612–616 | `redact()`: `fn.literal` fails; "no supported public pattern" for `$$KEEP` | `lowering.ts:37` (`fieldRef` → `'$'+path`), `:124` (`needsLiteralWrap`); `field-accessor.ts` `rawPath`; integration test:620 | **Corrected** | `fn.literal('KEEP')` emits the plain string and `fn.literal('$$KEEP')` is wrapped in `$literal`; both are rejected. The working route is `f.rawPath('$KEEP')` / `f.rawPath('$PRUNE')` inside `fn.cond(...)` | -| 624 | "The following stages take an options object: `unionWith` … `facet` …" | `builder.ts:535` (`unionWith(collection, pipeline?)`), `:588` (`facet(record)`) | **Corrected** | `unionWith` takes a collection name plus optional raw stages; `facet` takes a map of named raw sub-pipelines; the other seven take options objects | -| 625 | `bucketAuto`, `facet`, `setWindowFields` "not exercised" | integration test:447 (facet), :580 (bucketAuto), :666 (setWindowFields) | **Corrected** | Only `geoNear`, `densify`, `fill` are signature-only; the rest run live in Prisma ORM's test suite | -| 626 | Opaque expression fields | `builder.ts:546-645` (`MongoAggExpr` slots) | Confirmed; added where the classes are imported from | — | -| 643–650 | Atlas stages; `search(spec)`, `vectorSearch(spec)` | `builder.ts:646,655,664` | Corrected (additions) | `search`/`searchMeta` also take an optional index name; `vectorSearch` also takes `filter`; nothing in the builder refuses the stages off Atlas | -| 663 | "all nineteen MongoDB accumulators" | `accumulator-helpers.ts` — 19 exported, but MongoDB has more (`$mergeObjects`, `$accumulator`, `$percentile`, `$median`) | Corrected | "nineteen accumulators" | -| 665–685 | The 19 names and signatures | `accumulator-helpers.ts` | Confirmed (exact match, none missing, none extra) | Added that `sortBy` is a plain object and `n` is an expression | -| 774 | "`fn.*` mirrors MongoDB's operators; each is the camelCase name without `$`" | `expression-helpers.ts:139-519` | **Corrected** | 73 helpers, not all of MongoDB's; no `and/or/not/switch/ifNull/map/reduce/filter`; arithmetic stops at four; five are renamed (`firstElem`, `lastElem`, `isIn`, `typeOf`, `toString_`) | -| 789 | Named-argument helpers | `expression-helpers.ts:216,284,311,465` | Added | one new bullet | -| 790 | `fn.cond` condition must be `.node` | `expression-helpers.ts:180` (`condition: MongoAggExpr`); `filter-expressions.ts:217` (`visitor.expr`) | Confirmed | — | -| 792 | `fn.toObjectId(fn.literal(x))` throws `…reading 'codecId'` | `expression-helpers.ts:98` (`docUnaryExpr` reads `arg._field.codecId`), `:130` (`literal` sets `_field: undefined`) | Confirmed exactly | — | -| 860–861 | `pipe()` "not exercised"; "collapses the row shape" | integration test:713, 625 use `.pipe(...)` live; `builder.ts:679-687` (first overload keeps `Shape`) | **Corrected** | `pipe(stage)` keeps the current row type; `pipe` declares a new one; at run time the rows stop being decoded | -| 874–877 | `aggregate()` is an alias of `build()` | `builder.ts:849` (`return this.build()`) | Confirmed | — | -| 899–902 | Updater array rules and the three error strings | `update-ops.ts:199-227` | Confirmed; error codes added | Codes `ORM.MUTATION_DATA_MISSING` / `ORM.ARGUMENT_INVALID`; "as soon as you call the write method" instead of "at build time"; added the `Update spec collision` error | -| 908 | Root writes: `insertOne`, `insertMany`, `updateAll`, `deleteAll`, `upsertOne` | `state-classes.ts` `CollectionHandle` | Confirmed (exact) | — | -| 913 | Write result objects | `query-ast/src/result-types.ts`; `3-mongo-driver/src/mongo-driver.ts:117-175` | **Corrected** | Per-method shapes spelled out, including `insertedCount` and `matchedCount`/`upsertedCount` | -| 936 | `// { insertedIds: { '0': …, '1': … } }` | `mongo-driver.ts:144` (`Object.values(result.insertedIds)`) | **Corrected** | `// { insertedIds: [, ], insertedCount: 2 }` | -| 969 | Writes after `match()`: the seven methods | `state-classes.ts` `FilteredCollection` | Confirmed (exact) | — | -| 974 | `updateOne`/`deleteOne` at most one | `state-classes.ts` | Confirmed; added that the driver picks which, and that `findOneAndUpdate` after `sort()` is the ordered form | — | -| 975 | `findOneAndUpdate`/`findOneAndDelete` return values | `state-classes.ts` (plans carry no `resultShape`); `mongo-runtime.ts:251` | Added | The returned document is undecoded, so `_id` is a raw `ObjectId`; both vanish from the type after `skip()` and throw `ORM.OPERATION_UNSUPPORTED` if forced | -| 1018 | `returnDocument` default `'after'`; `returnNewDocument` invalid | `state-classes.ts` (`opts.returnDocument ?? 'after'`, excess-property check) | Confirmed; added the `upsert` option (default `false`) | — | -| 1058–1060 | Operator-form operator list (14 names) | `field-accessor.ts:74-92` | Confirmed (exact match) | — | -| 1059 | Pipeline form `f.stage.*` | `field-accessor.ts:139-146` | Confirmed; added the four emitters `set`, `unset`, `replaceRoot`, `replaceWith` | — | -| 1090–1092 | `out(collection)`, `merge({ into })` | `builder.ts:503,513` | Corrected (additions) | `out` takes an optional database name; `merge` takes `into` as a name or `{ db, coll }`, plus `on`, `whenMatched`, `whenNotMatched` | -| 1117 | `rawCommand()` takes a raw command, rows are `unknown` | `query.ts:29-59`; `test/raw-command.test.ts` | Corrected (wording) | It takes any command node, typed or raw, and does not translate it | -| 1126 | `RawAggregateCommand` import path | `9-public/@prisma/orm-mongo/package.json` exports `./query-ast/execution` | Confirmed | — | -| 9, 11, 17 | MongoDB only; no `find`/`distinct`; PostgreSQL has no `db.query` | `extensions/postgres/src/runtime/postgres.ts:50-61` (no `query`); no `find`/`distinct` on the builder | Confirmed | — | - -## Q list (unverifiable against `wip/prisma-src`) - -1. **"Every example is transcribed from an executable test suite that runs against a live MongoDB database" and the whole example schema (`User` / `Post` / `Article` / `Tutorial`).** The source's own pipeline-builder integration test uses a different fixture, `Product` / `Order` (`test/integration/test/mongo/query-builder.test.ts:36-130`). The page's harness is external to this checkout, so I cannot confirm or refute the provenance. What I could check does hold: root names are collection names, and a discriminated variant's root collapses into the base model's root (`contract-psl/src/interpreter.ts:487-493`), so `from('posts')` is the right call for that schema. Searches: `grep -rn "Tutorial\|Article\|@@discriminator" test/integration packages --include='*.ts'`, `ls test/integration/test/mongo/`. -2. **The "not exercised by the validation harness" and "verified empirically" statements** generally. These describe the docs team's own harness. I left them where the source also has no live test (`geoNear`, `densify`, `fill`, the Atlas stages) and removed them where the source does run the code live. -3. **`replaceRoot()`'s error text `'newRoot' expression must evaluate to an object`.** That string comes from the MongoDB server. Searches: `grep -rn "newRoot" packages --include='*.ts'`. -4. **`sortByCount()` "order of tied counts is not guaranteed".** MongoDB behaviour; the source test says the same in a comment (`query-builder.test.ts:524`) but does not prove it. - -## Things the source has that the page does not document - -Reported, not added, to keep the page at its original length: - -- `f('address.city')`, the callable form of the `f` argument for dot-paths into value objects, and `f.rawPath(...)` beyond the `redact()` use (`field-accessor.ts:160-210`). -- The no-argument `updateMany()` / `updateOne()` forms on a pipeline chain. The source says they are unreachable from user code (`builder.ts:711` comment), so leaving them out is right. -- `ObjectExpression`, the reduced operator set on a value-object path: `exists`, `eq(null)`, `ne(null)`, `set`, `unset` (`field-accessor.ts:104-113`). - -## Page-conventions last bullet - -It says the error-reference page is generated and out of scope. My slice does not touch it. - -## Line count - -Before: 1147. After: 1147. `check-plain.sh`: clean. `pnpm lint:links`: 0 errors. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/factcheck1.raw-queries.md b/docs/orm-docs-audit/c21-orm-reference/factcheck1.raw-queries.md deleted file mode 100644 index 554601ffdcd..00000000000 --- a/docs/orm-docs-audit/c21-orm-reference/factcheck1.raw-queries.md +++ /dev/null @@ -1,52 +0,0 @@ -Fact check complete. The page is 356 lines before and after. - -## Claims checked - -| Line | Claim | Deciding source | Verdict | -|---|---|---|---| -| 11 | Two escape hatches: `fns.raw`, `db.raw.sql`, `db.raw.collection(...)`, `db.query.rawCommand(...)` | `sql-builder/src/runtime/functions.ts:227`; `sql-builder/src/types/raw-query.ts:164`; `mongo-raw.ts:6`; `query-builder/src/query.ts:30` | Confirmed | -| 14 | Only a whole `db.raw.sql` statement converts results; `.returnsRow` converts; `fns.raw`'s `.returns()` is TypeScript only | `2-sql/5-runtime/src/codecs/decoding.ts:77-100` (row spec supplies codecs) vs `sql-builder/src/runtime/builder-base.ts:332` (projection codec comes from `returnType.codec`, which a raw expression has not got) | Confirmed | -| 14 | MongoDB raw results are native BSON; write methods return driver result objects | `mongo-runtime.ts:251` (no `resultShape`, row passes through); `mongo-driver.ts:118-198` | Confirmed | -| 21 | `fns.raw` usable in `select()`, `where()`, `orderBy()`, `update()` | `builder-base.ts:325,342,387,416`; `mutation-impl.ts:330` | Confirmed | -| 23 | `.returnsRow(spec).build()` → `runtime.query`; `.affectedCount().build()` → `runtime.execute` | `relational-core/src/expression.ts:460-484` | Confirmed | -| 25 | `db.sql.public` keyed by table name | conventions requirement | **Corrected**: added "which is keyed by table name (the model name with a lowercase first letter unless the model sets `@@map`)" | -| 28-33 | `postgres({ contractJson, url })`, `await db.connect()` | `3-extensions/postgres/src/runtime/postgres.ts:65,102,155` | Confirmed | -| 38-50 | `fns.raw` tagged template, `.returns(codecId)` compile-time only | `expression.ts:430-449`; integration test `test/integration/test/sql-builder/raw-sql.integration.test.ts:169` | Confirmed | -| 54 | A `.returns('pg/bool@1')` fragment satisfies `where()` alone | `sql-builder/src/expression.ts:12,38-41` (`BooleanCodecType`) | Confirmed | -| 67 | Interpolated `Expression` splices its AST, stays parameterized | `expression.ts:336-361` (`resolveInterpolation` → `value.buildAst()`) | Confirmed | -| 84 | Bare values work; `number`→`pg/int4@1`/`pg/int8number@1`/`pg/float8@1`, `bigint`→`pg/int8@1`, `string`→`pg/text@1`, `boolean`→`pg/bool@1`, `Uint8Array`→`pg/bytea@1` | `3-targets/6-adapters/postgres/src/core/adapter.ts:100-140`; integration `raw-query.integration.test.ts:118` uses bare `${1}` | Confirmed — **bare-scalar interpolation is not broken in rc.10**, so the earlier audit finding no longer holds and the page's current text is right | -| 84/87 | `param(value, { codecId })` from `@prisma/orm-postgres/relational-core/expression` | `relational-core/src/expression.ts:132`; `exports/expression.ts`; `@prisma/orm-postgres/package.json:127` | Confirmed | -| 100 | Object form of `.returns()` declares nullable | `expression.ts:434-449` | Confirmed | -| 118 | `db.raw` has one key, `sql` | `sql-builder/src/types/raw-query.ts:164` | Confirmed | -| 123 | `pg/timestamptz-temporal@1` is a real type id | codec ids in `3-targets/6-adapters/postgres/src` | Confirmed | -| 131-153 | `.returnsRow(spec)` entries: contract column or explicit type id; `user.columns.id` exists; `pg/int8@1` decodes to `bigint` | `types/raw-query.ts:29-63,115`; `table-proxy-impl.ts:95`; `raw-query.integration.test.ts:195` (`invitee_count: 2n`) | Confirmed | -| 156-168 | `.affectedCount()` takes no spec; `runtime.execute` returns `affectedRows` | `expression.ts:481`; `raw-query.integration.test.ts:140-148` | Confirmed | -| 162 | `SET "viewCount" = "viewCount" + 1` | The named example schema (sql-query-builder page) has no `viewCount` column on `Post` | **Corrected**: now `SET priority = ${'urgent'}` (`priority` is a real column of the example `Post` model, and the enum's storage type is `pg/text@1`, which a bare string binds to) | -| 171-196 | Row-returning raw queries compose into CTEs; `.returns.X` inherits a declaration | `expression.ts:378-386`; `raw-query.integration.test.ts:151-200` | Confirmed | -| 198 | `.affectedCount()` result cannot be interpolated | `expression.ts:304` (`RawAffectedCountQuery` has only `build()`); `RawSqlInterpolation` at `:334` | Confirmed | -| 202-208 | Accepted interpolations; `Date` throws `RUNTIME.RAW_SQL_UNSUPPORTED_INTERPOLATION` synchronously; TypeScript rejects it first | `ast/types.ts:44` (`RawSqlLiteral`); `expression.ts:356-360`; thrown from `templateParts` at tag-call time | Confirmed | -| 205 | Error message text | `expression.ts:358-359` and `adapter.ts:135-138`, identical strings | Confirmed | -| 212 | `db.raw.collection(rootName)`, nine methods, root name is the contract root | `raw-collection.ts:20-40`; `mongo-raw.ts:16-35` | Confirmed | -| 214 | `.build()` then `(await db.runtime()).query(plan)` | `raw-collection.ts:46-50`; `3-extensions/mongo/src/runtime/mongo.ts:33,38` | **Corrected**: added that `db.runtime()` returns a promise on MongoDB and is synchronous on PostgreSQL (conventions ask for this where it first appears) | -| 226-238 | `aggregate(pipeline)` | `raw-collection.ts:21,53` | Confirmed | -| 240-263 | `insertOne` → `{ insertedId }`; `insertMany` → `{ insertedCount, insertedIds }`, ids reshaped from the driver's index-keyed map | `mongo-driver.ts:118-145` (`Object.values(result.insertedIds)`) | Confirmed | -| 267 | `updateOne`/`updateMany` argument shapes and `{ matchedCount, modifiedCount, upsertedCount, upsertedId }` | `raw-collection.ts:65-71`; `mongo-driver.ts:124-158` | Confirmed | -| 293-305 | `deleteOne`/`deleteMany` → `{ deletedCount }` | `mongo-driver.ts:160-173` | Confirmed | -| 309 | `findOneAndUpdate` third argument `{ upsert }` | `raw-collection.ts:81-89` | Confirmed | -| 313 | `sort` and `returnDocument` "silently dropped, not a type or runtime error" | `raw-collection.ts:33-37` — the options type is `{ upsert?: boolean }`, so TypeScript's excess-property check rejects an options literal naming either key | **Corrected**: now says TypeScript rejects an options literal that names either key, and an untyped value is dropped without a run-time error; also names the direct route (`new RawFindOneAndUpdateCommand(...)` through `db.query.rawCommand(...)`), which the exports confirm | -| 314 | `returnDocument` never forwarded, driver default `'before'` applies | `mongo-driver.ts:176-186` (option omitted when undefined); `raw-commands.ts:106-111` | Confirmed | -| 318 | First upsert call returns empty; driver "yields `null`" | `mongo-driver.ts:183-185` — the driver returns `null` and the generator yields nothing | **Corrected**: "the driver returns `null`, and the query yields no row" | -| 336 | `findOneAndDelete` yields the raw matched document, not a wrapper | `mongo-driver.ts:188-198` | Confirmed | -| 346-354 | `db.query.rawCommand(command)` returns a plan with no `.build()`; rows typed `unknown`; `RawAggregateCommand` from `@prisma/orm-mongo/query-ast/execution` | `query-builder/src/query.ts:30,44-59`; `query-ast/src/exports/execution.ts:41-51`; `@prisma/orm-mongo/package.json` exports | Confirmed | - -## Q (unverifiable) - -- Line 290 `// { matchedCount: 2, modifiedCount: 2, ... }` and line 304 `// { deletedCount: 2 }`. These counts depend on seed data that neither this page nor the pipeline builder page shows. Searches run: `grep -rn "findOneAndUpdate" --include='*.test.ts' .`, `grep -rn "rawCollection\|mongoRaw\|\.collection(" --include='*.ts' test/integration/ packages/2-mongo-family/5-query-builders/orm/test/`, and a read of `packages/2-mongo-family/5-query-builders/orm/test/mongo-raw.test.ts` (which only checks the built command, never runs it). No live-database test exercises `db.raw.collection(...)` writes. -- Line 47 `// rows === [{ id: aliceId, upperEmail: 'ALICE@EXAMPLE.COM' }]` and the other example-schema result comments. The live tests use a different fixture (`users`/`posts` with int4 ids, `test/integration/test/sql-builder/fixtures/contract.ts`), not the schema this page names. - -## Notes for the operator - -- **The page's "every example is transcribed from an executable test suite" sentence is weaker than it reads.** The PostgreSQL raw tests do run against a live database, but against the `users`/`posts`/`comments` fixture in `wip/prisma-src/test/integration/test/sql-builder/fixtures/contract.ts`, not the `user`/`post` schema this page borrows from the SQL query builder page. The MongoDB raw collection methods have no live-database test at all; only the built command shape is tested. Every MongoDB example on this page is derived from the code, not from a test run. -- **No test covers the upsert-counter behaviour** described at lines 316-332. The driver tests (`packages/3-mongo-target/3-mongo-driver/test/mongo-driver.test.ts:176-230`) only pass `returnDocument: 'after'` explicitly. The page's claim follows from `mongo-driver.ts:176-186`, where the option is omitted when undefined. -- **The word "plan" appears 31 times**, and `conventions.md` bans it in favour of "the built query". I left it alone because the brief said to keep the banned-term agent's wording and the check script passes clean. If you want it removed, that is a separate pass. -- The bare word "codec" survives once, at line 205, inside the verbatim error message from the source. It has to stay. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/factcheck1.sql-query-builder.md b/docs/orm-docs-audit/c21-orm-reference/factcheck1.sql-query-builder.md deleted file mode 100644 index ec457767e50..00000000000 --- a/docs/orm-docs-audit/c21-orm-reference/factcheck1.sql-query-builder.md +++ /dev/null @@ -1,71 +0,0 @@ -Done. Report below. - -## 1. Claims checked - -| Line | Claim | Source that decides it | Verdict | Now says | -|---|---|---|---|---| -| 14 | SQL builder targets PostgreSQL and SQLite; MongoDB has none | `packages/3-targets/6-adapters/{postgres,sqlite}/src/core/adapter.ts`; no SQL builder under `2-mongo-family` | Confirmed | — | -| 19 | "every example is transcribed from an executable test suite that runs against a live PostgreSQL database" | `test/integration/test/sql-builder/setup.ts:41-95` seeds `users`/`posts`/`comments`/`profiles` with `int4` ids, not the page's `user`/`post`/`tag`/`post_tag` | **Corrected** | The test-suite claim is removed. Now: "All examples on this page run against the following schema." | -| 25-125 | Example schema | Matches `examples/prisma-8-demo/src/prisma/contract.prisma` except `Priority`, which is `pg/int4@1` with numeric values there. The page's text form matches the finished orm-client page's schema exactly, so the page is self-consistent | Confirmed (as the page's own schema) | — | -| 131 | Table accessors use "mapped table names (snake_case)" | `packages/2-sql/2-authoring/contract-psl/src/psl-attribute-parsing.ts:8` `lowerFirst`; used at `psl-field-resolution.ts:727` | **Corrected** | Default table name is the model name with a lowercase first letter unless `@@map`; column names are field names unless `@map`; `public` is the PostgreSQL schema | -| 135, 143 | Runtime comes from `await db.connect()` | `packages/3-extensions/postgres/src/runtime/postgres.ts:65-66, 289-328`. `connect()` exists but throws `DRIVER.ALREADY_CONNECTED` on a second call; `runtime()` is synchronous and is what the finished orm-client page uses | **Corrected** | "take a runtime from `db.runtime()` ... On PostgreSQL `db.runtime()` is synchronous, so there is nothing to await"; code uses `const runtime = db.runtime();` | -| 149-155 | `db.sql` is the result of `sql({ context, rawCodecInferer })`, importable from `@prisma/orm-postgres/builder/runtime` | `packages/2-sql/4-lanes/sql-builder/src/runtime/sql.ts:11-19`; `runtime/index.ts` exports `sql`; `@internal/sql-builder` exports `./runtime`; `orm-postgres/package.json` exports `./builder/runtime` | Confirmed (see note 3) | — | -| 154 | `inferCodec: () => 'pg/text'` | `packages/3-targets/3-targets/postgres/src/core/codec-ids.ts:8` (`pg/text@1`); `packages/3-targets/6-adapters/postgres/src/core/adapter.ts:129` | **Corrected** | `'pg/text@1'` | -| 187 | `f` is one property per column; joined columns under the table name; `fns` is the second argument | `runtime/field-proxy.ts` (whole file) | Confirmed | — | -| 195-201 | Three `select()` forms | `src/types/shared.ts` `WithSelect` | Confirmed | — | -| 199 | Unknown column name error | `runtime/builder-base.ts:310` throws `ORM.COLUMN_UNKNOWN`, message `Column "x" not found in scope` | **Corrected** | "throws an error whose `code` is `ORM.COLUMN_UNKNOWN`" | -| 255 | `where(predicate)` | `src/types/select-query.ts:38` | Confirmed | — | -| 288-296 | `innerJoin(other, on)`, other may be a table or an aliased subquery | `src/types/shared.ts` `WithJoin`; `runtime/joined-tables-impl.ts:168-190` | Confirmed | — | -| 313-317 | `outerLeftJoin`/`outerRightJoin`/`outerFullJoin` null semantics | `WithJoin` + `NullableScope` in `src/scope.ts:69-77` | Confirmed | — | -| 339 | "An adapter that does not has no `lateralJoin()` method" | `src/scope.ts:31` `GatedMethod` is type-level only; `runtime/builder-base.ts:280-293` throws `ORM.CAPABILITY_MISSING`; `test/e2e/framework/test/sqlite/sql-builder.test.ts:335-342` | **Corrected** | PostgreSQL only; on SQLite TypeScript types it `never`, and calling it anyway throws an error whose `code` is `ORM.CAPABILITY_MISSING`. Also added one sentence naming `outerLateralJoin()`, which the page had omitted | -| 340 | `lateral.from(otherTable)` then the usual SELECT chain | `src/types/shared.ts` `LateralBuilder`; `joined-tables-impl.ts:192-210` | Confirmed | — | -| 341 | `.as()` inside the callback throws `subquery.getRowFields is not a function` | `joined-tables-impl.ts:213` calls `subquery.getRowFields()`; `runtime/query-impl.ts:142-158` shows `.as()` returns only `getJoinOuterScope`/`buildAst` | Confirmed | — | -| 342 | Ambiguous names are dropped from the bare scope | `runtime/builder-base.ts:235-247`; `MergeScopes` in `src/scope.ts:56-61` | Confirmed | — | -| 388-390 | `orderBy` options `direction` and `nulls` | `src/expression.ts:46-49` declares both, but `runtime/builder-base.ts:357-390` reads only `options?.direction`. `OrderByItem` (`relational-core/src/ast/types.ts:1080-1098`) carries no null placement, and no renderer emits `NULLS FIRST`/`NULLS LAST` (grep over all of `packages` finds the strings nowhere) | **Corrected** | The `nulls` row now says TypeScript accepts it but nothing reads it, so no `NULLS FIRST`/`NULLS LAST` reaches the SQL and the database's own null ordering applies. The example keeps its heading and code, with a trailing comment saying `nulls` adds nothing to the SQL | -| 435, 466 | `distinct()` and `distinctOn()` return types | `src/types/shared.ts` `WithDistinct`; `src/types/select-query.ts:63-78` | Confirmed | — | -| 453 | `distinctOn()` availability wording | `select-query.ts:64` gates on `{ postgres: { distinctOn: true } }`; SQLite adapter has no `postgres` group (`adapters/sqlite/src/core/adapter.ts:76-85`); `sqlite/sql-builder.test.ts:344-352` | **Corrected** | PostgreSQL only; `DISTINCT ON` is a PostgreSQL extension to SQL; on SQLite it does not compile and throws `ORM.CAPABILITY_MISSING` if called | -| 454 | `distinctOn` ordering is not type-enforced | `runtime/builder-base.ts:418-446` does no order check | Confirmed | — | -| 491-492 | `limit(n)`/`offset(n)` take `number` | `src/types/shared.ts` `PaginationValue` = `number \| TraitExpression<['numeric']>` | **Corrected** | "`number`, or a numeric expression" | -| 516-528 | `.as(alias)` gives a join source, not a buildable query | `runtime/query-impl.ts:142-158` | Confirmed | — | -| 549-550, 693 | `GroupedQuery` has `having()`, aggregates, `orderBy`, `limit`, `offset`, `distinct`, `distinctOn` | `src/types/grouped-query.ts` | Confirmed | — | -| 584 | Aggregate decode note: `count`/integer `sum`/integer `avg` give `number`; out-of-range raises `RUNTIME.DECODE_FAILED`; `avg` is `float8` and unguarded; `countBigInt`/`sumBigInt`/`avgDecimal` | `postgres/src/core/aggregates.ts:5-6, 160-215`; `codec-helpers.ts:152-197` | Confirmed | — | -| 593-596 | `groupBy` forms | `src/types/select-query.ts:52-62`; `builder-base.ts:394-416` | Confirmed | — | -| 629 | `EXTRACT` result is a string because a raw expression is not converted | `builder-base.ts:317-334` passes `field.codec`, which a `.returns()` result does not carry (`relational-core/src/expression.ts:443-448`) | Confirmed (reworded only, to drop a banned term) | "Prisma ORM does not convert what a raw expression returns" | -| 634-638 | `having()` builds from aggregates and compares against a JavaScript literal, evaluated server-side | `grouped-query.ts:53-58`; `functions.ts:126-148` comment; `PgInt8NumberCodec.encode(value: number)` at `codecs.ts:707-716` | Confirmed | — | -| 721-722 | `insert()` always takes an array; multiple rows in one statement | `src/types/table-proxy.ts` `insert(rows: ReadonlyArray<...>)`; `adapters/postgres/src/core/sql-renderer.ts:913` | Confirmed | — | -| 728 | Insert values are sent as query parameters | `relational-core/src/expression.ts:336-360` | Confirmed | — | -| 763 | `returning()` availability | `src/types/mutation-query.ts:12` gates on `{ sql: { returning: true } }`; SQLite sets `returning: true` (`adapters/sqlite/src/core/adapter.ts:82`); `sqlite/sql-builder.test.ts:354-360` asserts it is **not** `never` | **Corrected** | "PostgreSQL and SQLite. Both support SQL's `RETURNING`." Plus the `never`/`ORM.CAPABILITY_MISSING` behaviour for an adapter without it | -| 796-799 | `update()` values-object and callback forms; `where()` on update/delete; no `where()` on insert | `src/types/table-proxy.ts`; `src/types/mutation-query.ts` | Confirmed | — | -| 841 | `delete()` returns a `DeleteQuery` | `src/types/table-proxy.ts` | Confirmed | — | -| 863-872 | `param(value, { codecId })` returns a `ParamRef`, imported from `@prisma/orm-postgres/relational-core/expression` | `relational-core/src/expression.ts:132-134`; `src/exports/expression.ts`; `orm-postgres/package.json` exports `./relational-core/expression` | Confirmed | — | -| 865 | Comparison values are parameterized from the neighbouring column | `runtime/functions.ts:46-96` (`resolveOperand` threads the column's type) | Confirmed | — | -| 866, 975 | No `build({ params })`; `build()` takes zero arguments everywhere | `src/types/shared.ts` `WithBuild`; `mutation-query.ts` `build()` | Confirmed | — | -| 906-910 | Built-in function list: `eq ne gt gte lt lte`, `in notIn`, `and or`, `exists notExists`, `raw` | `runtime/functions.ts:203-228` `createBuiltinFunctions` — exactly these thirteen | Confirmed | — | -| 912 | Aggregates come from the contract's aggregate map; PostgreSQL contributes eight | `postgres/src/core/aggregates.ts:157-227`; `runtime/sql.ts:21-30` | Confirmed | — | -| 916-923 | Per-aggregate return types | Same file | **Corrected** (two rows) | `sum(field)`: "`number` over an integer column; over any other column, the type PostgreSQL's own `sum` produces for it" (the old wording missed `time`/`interval` → `interval`). `min`/`max`: added "a `varchar` column widens to `text`" (`aggregates.ts:137-152`) | -| 925 | `ilike` from the Postgres adapter for textual columns; `cosineDistance` from pgvector | `adapters/postgres/src/core/descriptor-meta.ts:153-169`; `extensions/pgvector/src/core/descriptor-meta.ts:19` | Confirmed | — | -| 927-929 | No `fns.coalesce` or `fns.cast` | `runtime/functions.ts:203-228` | Confirmed | — | -| 933 | `fns.raw` parameterizes each interpolation; bare values get a type from their JavaScript type; `.returns(codecId)` declares the result type | `relational-core/src/expression.ts:336-360, 420-448` | Confirmed | — | -| 944 | `.returns()` is a type annotation only, no SQL cast, no conversion on read | `expression.ts:443-448` returns `{ returnType: { codecId, nullable } }` with no codec instance; `builder-base.ts:317-334` | Confirmed | — | -| 985 | `runtime.query` returns an `AsyncIterableResult`; `runtime.execute` resolves to `{ affectedRows }` | `relational-core/src/runtime-scope.ts:23-26`; `ast/driver-types.ts:24-26`; `mutation.test.ts:109` asserts `{ affectedRows: 1 }` | Confirmed (the `db.connect()` aside was corrected, see line 135) | — | -| 998-1000 | `ResultType` import path; one row not an array | `orm-postgres/package.json` `./components/runtime`; matches the finished orm-client page | Confirmed | — | -| 1001 | Since rc.10 `ResultType` also works on ORM queries | `docs/releases/v8.0.0-rc.10.md:27` | Confirmed | — | -| 1017 | Streaming versus collecting | `runtime-scope.ts:24`; `docs/releases/v8.0.0-rc.10.md:47` | **Corrected** | Now says to loop the same result with `for await`, that a result is read once one way, and records the rc.10 change: a buffered query hands its pooled connection back before the rows are read; a cursor stream keeps its connection until it finishes | - -## 2. Q (unverifiable) - -- **Line 244** `// rows === [{ id: aliceId, upperEmail: 'ALICE@EXAMPLE.COM', emailLength: 17 }]` and the other result comments (lines 308, 330, 375, 444, 480, 664, 687-688, 708, 785, 818, 830, 854). Searched `test/integration/test/sql-builder/*.test.ts` for these values and for the page's table names (`grep -rn "post_tag\|displayName\|'hp'\|latestPost" test/integration/test/sql-builder`). Nothing matches: those tests use a different fixture. The comments are arithmetically consistent with the page's own schema and seed values, so I left them, but nothing in prisma/orm confirms them. -- **Line 687** `avgDecimal(f.amount) would give the exact '300.0000000000000000'`. The type is confirmed (numeric decodes to a string, `codecs.ts:896-906`), the exact digit count is not. Searched `grep -rn "avgDecimal" packages test` — no test asserts a scale for an `avg` over `int4`. -- **Line 444** `// distinct priorities: ['high', 'low', 'urgent']` depends on the page's own seed data, which does not exist in the source. - -## 3. What page-conventions asked for - -- **`db.sql` keyed by table name, said once where `db.sql` first appears**: done at line 131, and corrected from "snake_case" to "the model name with a lowercase first letter unless `@@map`". -- **`db.runtime()` stated where it first appears**: done at line 135. -- **`sql({ context, rawCodecInferer })` paragraph (lines 149-157)**: every name is real and every one is publicly exported. `sql` and `SqlOptions` come from `@internal/sql-builder/runtime`, which the public `@prisma/orm-postgres` package republishes at `./builder/runtime`; `context` is `db.context` on the client (`PostgresClient.context`, `postgres.ts:61`); `RawCodecInferer` is `{ inferCodec(value): string }`. So nothing here is private API. It is still a low-level assembly path that no test or example in prisma/orm uses outside the test harness itself, and the paragraph is the densest on the page. **Recommendation for the reader round: cut lines 149-157 down to one sentence, or drop them.** I left them in place because the facts hold. -- **Banned terms**: `check-plain.sh` reports clean. The only remaining hits for the extra list are the pinned anchor `[#the-dbsql-facet]` (kept, as required) and `plan` as a variable name inside code blocks (prose says "the built query"). -- **Headings and anchors**: none renamed, renumbered, or reordered. No new sections. - -## 4. Line count - -Before: 1,017. After: 1,017. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/factcheck1.transactions-and-runtime.md b/docs/orm-docs-audit/c21-orm-reference/factcheck1.transactions-and-runtime.md deleted file mode 100644 index 98664c3f88d..00000000000 --- a/docs/orm-docs-audit/c21-orm-reference/factcheck1.transactions-and-runtime.md +++ /dev/null @@ -1,78 +0,0 @@ -Fact check complete for `apps/docs/content/docs/orm/reference/transactions-and-runtime.mdx`, against `wip/prisma-src/` at 8.0.0-rc.10. - -## 1. Claims checked - -| Line | Claim | Source that decides it | Verdict | Text now | -|---|---|---|---|---| -| 9–13 | Intro: page scope; "every PostgreSQL example is transcribed from an executable test suite"; "MongoDB behavior is verified the same way" | No fixture in the tree uses a `Tag` model with a `label` field (`test/integration/test/sql-orm-client/fixtures/mn-psl/contract.prisma:26` has `Tag { id, name }`) | Corrected | Dropped both process claims. Intro now ends "Transactions are fully supported on PostgreSQL and not shipped on MongoDB." | -| 28 | `postgres()` binds "in one of two ways" (`url`, `pg`) | `packages/3-extensions/postgres/src/runtime/binding.ts:16-31, 67-82` | Corrected | "one of three ways": `url`, `pg`, or a `binding` object naming its kind. Pass exactly one. | -| 29 | Pool defaults `20000` / `30000`, "source-read from `postgres.ts`" | `.../runtime/postgres.ts:141-142` | Corrected | Defaults confirmed; "source-read" dropped. Added that `poolOptions` applies only to a pool the client creates from `url` (`postgres.ts:132-135`). | -| 31 | `postgres(options)` does not connect; "call `connect()` to acquire a runtime" | `postgres.ts:222-262` (`getRuntime()` connects lazily); `packages/3-extensions/postgres/test/postgres-close.test.ts:146-154` calls `db.runtime()` with no `connect()` | Corrected | "The client opens one on first use, so `connect()` is optional: call it to open the connection up front and fail early." | -| 35–42 | `postgres` options table | `postgres.ts:76-109` | Corrected | Added `verifyMarker` (`'onFirstUse'` or `false`, default `'onFirstUse'`; `packages/2-sql/5-runtime/src/runtime-spi.ts:26-43`). Reworded `poolOptions`. `binding` covered in the Remarks instead of a table row, to hold the line count. | -| 48 | `PostgresClient` exposes `.orm`, `.sql`, `.enums`, `connect`, `runtime`, `transaction`, `prepare`, `close` | `postgres.ts:56-74` | Corrected | Added `.raw`, `.nativeEnums`, `.contract`, `.context`. | -| 81 | `connect()` returns `Promise`; runtime runs queries, opens connections, prepares | `postgres.ts:289-324`; `packages/2-sql/5-runtime/src/sql-runtime.ts:100-113` | Corrected | Added `runtime.telemetry()` to the list, plus the optional binding argument (`postgres.ts:305-307`). | -| 82 | `connect()` after close rejects `DRIVER.NOT_CONNECTED` (`Postgres client is closed`) | `postgres.ts:291-295`; `postgres-close.test.ts:140-144` | Confirmed | — | -| new | `connect()` twice rejects `DRIVER.ALREADY_CONNECTED` | `postgres.ts:298-303` | Corrected (added) | Stated in `connect()` Remarks; the page previously only implied it from the MongoDB section. | -| 105 | PostgreSQL `runtime()` is synchronous | `postgres.ts:66, 326-328` | Confirmed | — | -| 128 | After `close()`, `connect()` rejects `DRIVER.NOT_CONNECTED` | `postgres.ts:223-229, 291-295`; `postgres-close.test.ts:134-144` | Corrected | Now also names `runtime()`, which throws the same code. | -| 129 | You close your own `pg` instance | `postgres.ts:199-207, 374-379` (only a `url` binding sets `ownedDispose`) | Confirmed | — | -| 150–156 | `Symbol.asyncDispose`, disposal at end of block, closed afterwards | `postgres.ts:381-383`; `postgres-close.test.ts:146-154` | Confirmed | Error-code wording normalised. | -| 156 | `await using` needs `@types/node` or a `lib` declaring the symbol | — | **Q** | See section 2. | -| 181 | `mongo()` binds "in one of three ways" | `packages/3-extensions/mongo/src/runtime/binding.ts:13-41, 82-95` | Corrected | "one of four ways", adding the `binding` object. | -| 182 | Client ownership, "source-read from `binding.ts` / `mongo-driver.ts`" | `.../runtime/mongo.ts:131-145, 220-235` | Corrected | Fact confirmed; "source-read" dropped. | -| 183 | `mongo()` does not connect; runtime built lazily | `mongo.ts:147-169` | Confirmed | — | -| 187–193 | `mongo` options table | `mongo.ts:43-57` | Corrected | Added `mode` (`'strict'` / `'permissive'`, default `'strict'`; `packages/2-mongo-family/7-runtime/src/mongo-runtime.ts:120`). `binding` covered in Remarks. | -| 199 | `MongoClient` exposes `.orm`, `.query`, `.raw`, `.contract`, `.enums`, `.context`, `connect`, `runtime`, `close` | `mongo.ts:28-41` | Confirmed | — | -| 233 | Second `connect()` rejects `DRIVER.ALREADY_CONNECTED`; first connection can be implicit | `mongo.ts:197-214`; `packages/3-extensions/mongo/test/mongo.test.ts:229-243` | Confirmed | Error wording normalised; added that `connect()` accepts a binding (`mongo.test.ts:236-243`). | -| 234 | After close, `connect()` rejects `DRIVER.NOT_CONNECTED` (`Mongo client is closed`) | `mongo.ts:198-199`; `mongo.test.ts:414-422` | Confirmed | — | -| 257 | MongoDB `runtime()` returns a promise | `mongo.ts:38, 216-218` | Confirmed | The duplicate `:::note` was folded into the bullet (same facts, two fewer lines). | -| 263 | `MongoRuntime` has `query`, `execute`, `close` only | `packages/2-mongo-family/7-runtime/src/mongo-runtime.ts:76-101` | Confirmed | — | -| 285–301 | `(await db.runtime()).query(...)`; client has no `execute()`; `db.query.from('posts').build()` | `mongo.ts:28-41`; `packages/2-mongo-family/5-query-builders/query-builder/src/query.ts:27-29`; `.../state-classes.ts:99-108` | Confirmed | Example variable renamed `plan` → `built` per the conventions. | -| 311 | After close, `db.runtime()` and ORM access reject `DRIVER.NOT_CONNECTED` | `mongo.ts:147-150, 171-182`; `mongo.test.ts:405-412` | Confirmed | Wording normalised ("rejects", not "throws"). | -| 360 | `tx` offers `orm`, `sql`, `query`, `execute`, `enums`; parenthetical about what the test suite verifies | `postgres.ts:48-54, 341-372`; `packages/2-sql/5-runtime/src/sql-runtime.ts:139-141` | Corrected | Parenthetical removed; `tx.nativeEnums` added. `tx.raw` does **not** exist, and the page never claimed it. | -| new | No `isolationLevel` / `timeout` / `maxWait`; no nested transactions | `test/integration/test/ports/prisma/functional/interactive-transactions/interactive-transactions.test.ts:13-16` and its disposition list | Corrected (added) | One bullet added; the page had no such statement. | -| 362 | `tx.sql` keyed like `db.sql` | `postgres.ts:344-347` | Confirmed | — | -| 363–364 | Reads see own uncommitted writes; return value passes through | `sql-runtime.ts:961-1078` | Confirmed | — | -| 365, 424 | Escaped `AsyncIterableResult` rejects `RUNTIME.TRANSACTION_CLOSED` before any row | `sql-runtime.ts:968-982, 989-996, 948-953` | Confirmed | Error wording normalised. | -| 383–421 | Commit / rollback-on-throw / read-your-writes / `tx.execute` examples | `interactive-transactions.test.ts` ("basic", "rollback throw", "rollback query") | Confirmed | API shapes all exist; the `Tag { label }` model is the page's own example schema, not a repo fixture. | -| 437–451 | Prisma ORM 7 `$transaction([...])` becomes a callback | `interactive-transactions.test.ts:9-12` | Confirmed | — | -| 459 | `withTransaction` imported from `@prisma/orm-postgres/family-runtime` | `packages/9-public/@prisma/orm-postgres/package.json:113`; `.../test/facade-tarball.test.ts:188` | Confirmed | — | -| 460–461 | `withTransaction(runtime, cb)`; `tx` has `query` and `execute` only; commits on return, rolls back on throw | `sql-runtime.ts:961-1078` | Confirmed | — | -| 511 | `runtime.connection()` / `connection.transaction()` / `commit()` / `rollback()` / `release()` | `sql-runtime.ts:100-133` | Confirmed | — | -| 513 | `destroy(reason)` "source-read from `postgres-driver.ts`"; evicts one connection; runtime opens a replacement | `packages/3-targets/7-drivers/postgres/src/postgres-driver.ts:545-570`; `sql-runtime.ts:114-127` | Corrected | "source-read" dropped; added that `reason` is optional and advisory. | -| 523 | `destroy(reason)` type `Error`, required | `sql-runtime.ts:126` (`destroy(reason?: unknown)`) | Corrected | Now "`Error`, optional". | -| 562 | MongoDB has no `db.transaction`; runtime has no `connection()`/`prepare()`/`telemetry()`; "probing returns `undefined`, calling throws `TypeError`" | `mongo.ts:28-41`; `mongo-runtime.ts:76-101` | Corrected | Fact kept; the probe sentence replaced with "it has `query`, `execute`, and `close` and nothing else", per page-conventions. | -| 567 | Planned MongoDB transactions: `db.transaction(fn)` with `tx.orm` and `tx.query`, driver session, replica set required, must not degrade silently | `projects/sqlite-mongo-transactions/spec.md:113-131` (present in the rc.10 tree) | Confirmed | Banned phrase "source-read from an internal design document" was already gone; trimmed the trailing note about the page itself. | -| 579 | Declaration maps parameter name to a type id | `packages/2-sql/5-runtime/src/prepared/types.ts:20-30` | Confirmed | — | -| 580 | `runtime.prepare` callback takes one argument | `sql-runtime.ts:549-552`; `prepared/types.ts:51` | Confirmed | — | -| 581 | Unused declared parameter rejected at prepare time, `details.unused` | `sql-runtime.ts:568-573`; `packages/2-sql/5-runtime/test/prepared.test.ts:193-205` | Confirmed | Error wording normalised. | -| 634 | `db.prepare` callback takes `(sql, params)` | `postgres.ts:330-339` | Confirmed | — | -| 635 | `prepare()` starts the connection, so a later `db.connect()` fails `DRIVER.ALREADY_CONNECTED` | `postgres.ts:249-250, 298-303` | Confirmed | Wording normalised. | -| 672 | "A query ending with `.affectedCount()` prepares into a `PreparedExecution`" | `sql-runtime.ts:602-605` (needs `ast.kind === 'raw-query'`); `packages/2-sql/4-lanes/sql-builder/src/types/raw-query.ts:142` | Corrected | Now "A **raw** query ending with `.affectedCount()`". | -| 673–674 | `target` can be runtime, connection, or transaction; one statement runs against any | `prepared/types.ts:53-65`; `sql-runtime.ts:766-880, 986-1028` | Confirmed | — | -| 716 | `RuntimeExecuteOptions` is the second argument to every `query`/`execute` | `packages/1-framework/1-core/framework-components/src/execution/runtime-middleware.ts:352-359` | Confirmed | — | -| 724 | Pre-aborted signal rejects `RUNTIME.ABORTED`, `details.phase` `'stream'` | `.../execution/runtime-core.ts:118-148`; `.../execution/runtime-error.ts:36-46` | Confirmed | Added that `cause` is the signal's reason verbatim (`runtime-error.ts:41-46`), which the page did not state. | -| 725 | "Aborting mid-stream is not separately verified in the validation harness" | `runtime-core.ts:123, 148`; `runtime-error.ts:7-21` | Corrected | Replaced with the fact: the signal is checked between rows, and a mid-stream abort ends the stream with `RUNTIME.ABORTED` and a phase of `'stream'`, `'encode'`, or `'decode'`. | -| 726 | "`scope` selects the execution scope for the query" | `runtime-middleware.ts:60-81`; `sql-runtime.ts:428-441` | **Corrected (real error)** | `scope` is only the label middleware reads. The runtime sets it per scope; setting it yourself changes the label, not where the query runs. The options-table row was corrected too. | -| 755 | MongoDB runtime has no `telemetry()` | `mongo-runtime.ts:76-101` | Confirmed | Probe sentence removed. | -| 756 | `telemetry()` is `null` before any query | `sql-runtime.ts:217` | Confirmed | — | -| 757 | Shape `{ lane, target: 'postgres', fingerprint, outcome: 'success', durationMs? }` | `runtime-spi.ts:45-52`; `sql-runtime.ts:932-944` | Corrected | `outcome` is `'success'` or `'runtime-error'`; the shape line no longer pins it to `'success'`. | -| 780 | `AsyncIterableResult`: one consumption per mode; re-`await` safe; mode switch throws `RUNTIME.ITERATOR_CONSUMED` | `.../execution/async-iterable-result.ts:13-61` | Confirmed | Wording normalised. | -| new | rc.10: buffered PostgreSQL queries release the pooled connection before rows are read | `docs/releases/v8.0.0-rc.10.md` (Fixes, #30259); `postgres-driver.ts:296-333, 449-473`; `postgres.ts:245-247` | Corrected (added) | Two sentences added to the `AsyncIterableResult` section. I wrote it as "a query run through the client", because `postgres()` builds its driver with `cursor: { disabled: true }`, so every client query takes the buffered path; I did not claim cursor-stream behaviour the client cannot reach. | - -## 2. Q list (unverifiable) - -**`await using` requires `@types/node` (which augments `SymbolConstructor` with `asyncDispose`) or a `lib` that declares the symbol.** This is a TypeScript toolchain fact, not a Prisma ORM one, and nothing in the tree states it. Left as written. -Searches run: `grep -rn "asyncDispose" packages --include='*.ts'` (only the two client implementations and their tests); `grep -rn "await using" --include='*.ts' packages test`; `grep -rn "esnext.disposable\|ESNext.Disposable" -ri .`; checked `packages/9-public/@prisma/orm-postgres/package.json` (it does depend on `@types/node`, but nothing says `await using` requires it). - -## 3. Page-conventions last bullet - -The last bullet says the error-reference page is generated upstream and out of scope. My slice does not link to or restate the error-reference page, so nothing applied. - -## 4. Line count - -Before: 782. After: 782. - -Notes for the operator: -- The example schema on this page (`Tag` with a `label` column) does not match any fixture in the rc.10 tree. The API shapes in every example are confirmed, but the claim that the examples were transcribed from a live test suite is not supportable from this source, so I removed it rather than restating it. -- The one substantive behaviour error found was the `scope` option, which the page described as choosing where a query runs. It does not. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/factcheck2.pipeline-builder.md b/docs/orm-docs-audit/c21-orm-reference/factcheck2.pipeline-builder.md deleted file mode 100644 index 7f2784ea3b3..00000000000 --- a/docs/orm-docs-audit/c21-orm-reference/factcheck2.pipeline-builder.md +++ /dev/null @@ -1,115 +0,0 @@ -Checks complete. Report below. - -## 1. Claims checked - -Line numbers are in the corrected slice (`wip/c21/ref/fc2/pipeline-builder.mdx`). Source paths are relative to `wip/prisma-src/`. `qb/` = `packages/2-mongo-family/5-query-builders/query-builder/src/`, `ast/` = `packages/2-mongo-family/4-query/query-ast/src/`. - -| Line | Claim | Source | Verdict | Now says | -|---|---|---|---|---| -| 9-11 | Aggregation only, no `find()`/`distinct()`, `limit(1)` for one document | qb/builder.ts, qb/query.ts (no such methods) | Confirmed | | -| 19 | Example schema is the same as the ORM client page | `apps/docs/.../orm-client.mdx` | Confirmed | | -| 26-75 | Schema block fields (`_id`, `kind`, `authorId`, `createdAt`, `duration`) | same | Confirmed | | -| 84 | `mongo(...)` client, `dbName` option | packages/3-extensions/mongo/src/runtime/mongo.ts:55 | Confirmed | | -| 84 | "Three import paths appear on this page" | page also imports `ObjectId` from `mongodb` (line 1233) | Corrected | "Three Prisma ORM import paths appear on this page" | -| 84 | `/runtime`, `/query-builder`, `/query-ast/execution` exist and hold those names | packages/9-public/@prisma/orm-mongo/package.json; qb/exports/*.ts; ast/exports/execution.ts | Confirmed | | -| 86 | `prisma contract emit` writes `contract.json` and `contract.d.ts` | cli/src/commands/init/hygiene-gitattributes.ts:8,35 | Confirmed | | -| 86 | `with { type: 'json' }` works with the settings `prisma orm init` writes | cli/src/commands/init/templates/tsconfig.ts:23-27 (`module: 'preserve'`, `moduleResolution: 'bundler'`, `resolveJsonModule: true`) | Confirmed | | -| 94 | `from()` takes the collection name from the roots map | qb/query.ts:29; qb/state-classes.ts:640-660 | Confirmed | | -| 95 | `ORM.MODEL_UNKNOWN`, message `Unknown root: "". Valid roots: ...` | qb/state-classes.ts:648 | Confirmed | | -| 96 | `insertOne()`/`insertMany()` on `from()` only, before any stage | qb/state-classes.ts (on `CollectionHandle`, absent from `FilteredCollection`) | Confirmed | | -| 97 | `mongoQuery({ contractJson }).from(...)` builds only, never runs | qb/query.ts:33-60 | Confirmed | | -| 116-124 | Entry example: default `mongo` import, `contract.d`, `contract.json` | cli/.../code-templates.ts:336-345 | Confirmed | | -| 129-135 | `build()` returns the built query; `db.runtime()` is a promise; no `db.execute` | qb/builder.ts `build()`; mongo.ts:38, `MongoClient` interface has no `execute` | Confirmed | | -| 136 | Decoding: the eight stages keep the shape, anything else makes every document raw; `_id` hex string vs `ObjectId` | qb/pipeline-result-shape.ts:14, 68-89 | Confirmed | | -| 136 | A `DateTime` field is a `Date` either way | `mongo/date@1` output is `Date`; raw driver returns `Date` | Confirmed | | -| 157-158 | `match()` callback; scalar operators `eq, ne, gt, gte, lt, lte, in, nin, exists, type` | qb/field-accessor.ts:51-67 | Confirmed | | -| 159 | `expr(fn.gt(...))` form for computed comparisons | qb/field-accessor.ts:222 | Confirmed | | -| 165-166 | `.node` table under `match()` listed `redact()` as needing `.node` | qb/builder.ts `redact()` takes the callback's value and reads `.node` itself | **Corrected** | `redact()` moved to the "the `fn.*` value itself" row; the `.node` row now reads "The condition argument of `fn.cond()`, and the stages that take an options object" | -| 206-213 | `_id` equality in `match()` never matches; `db.orm.posts.where({ _id }).first()` works | pipeline builder sends a bare `MongoFieldFilter` with no codec (ast/filter-expressions.ts:51); ORM client wraps values in `MongoParamRef(value, { codecId })` (orm/src/collection.ts:744) | Confirmed | | -| 222 | `sort()` spec `{ field: 1 \| -1 }` | qb/builder.ts `sort()` | Confirmed | | -| 251-322 | `limit`, `skip`, `sample` signatures | qb/builder.ts | Confirmed | | -| 330 | `addFields()` callback returns new fields, existing fields preserved | qb/builder.ts `addFields()`; qb/pipeline-result-shape.ts:56-66 | Confirmed | | -| 364-365 | `lookup()` shape; `on()` returns fixed `local`/`foreign` keys | qb/lookup-builder.ts:36-37, 195-200 | Confirmed | | -| 366 | No match gives an empty array | MongoDB `$lookup` behaviour; no source test | **Q** | | -| 367 | Both `_id`s are `ObjectId`; `String(post.author[0]._id) === String(post.authorId)` | qb/pipeline-result-shape.ts (lookup drops the shape) | Confirmed | | -| 404 | Key-list form; `_id` retained implicitly | qb/builder.ts `project()` overload 1 (`Pick`); test/integration/.../query-builder.test.ts:370 | Confirmed | | -| 405, 412, 436 | Callback form accepts `0` to drop a field; example used `_id: 0` | qb/builder.ts `project()` spec type is `Record`, and the body does `val === 1 ? 1 : val.node` | **Corrected** | "Write `1` to keep a field, or an expression to compute it. There is no `0` form here: the callback form cannot drop a field." Options row is now `1 \| Expression`; `_id: 0` removed from the example | -| 447 | "The named field must hold an array" | qb/builder.ts `unwind`; qb/types.ts:211 `UnwoundShape` has no array constraint | **Corrected** | "Name the field holding the array. TypeScript accepts any field of the document; nothing checks that it holds an array." | -| 454 | `preserveNullAndEmptyArrays` defaults to `false` | qb/builder.ts (`?? false`) | Confirmed | | -| 484-485 | `group()` takes one callback argument; `acc` is imported, not a second argument | qb/builder.ts `group()`; qb/accumulator-helpers.ts | Confirmed | | -| 487 | Non-accumulator message and `ORM.ARGUMENT_INVALID`; "a `null` value throws the same way" | qb/builder.ts (two distinct messages) | **Corrected** | The null case now quotes its own message: `group() field "" must not be null. Only _id can be null.` | -| 546 | `replaceRoot()` callback returns an object-valued expression; MongoDB's `'newRoot' expression must evaluate to an object` | qb/builder.ts `replaceRoot()` confirmed; the MongoDB message is not in the source | Confirmed / **Q** on the message text | | -| 583 | `count()` stage is not `acc.count()` | qb/builder.ts `count()`; qb/accumulator-helpers.ts `count()` | Confirmed | | -| 612 | `sortByCount()` emits `{ _id, count }` sorted descending | qb/builder.ts `sortByCount()` return type; integration test line 750 | Confirmed | | -| 644-645 | `redact()` needs `$$KEEP`/`$$DESCEND`/`$$PRUNE`; `f.rawPath` prefixes `$` | qb/field-accessor.ts `rawPath`; packages/3-mongo-target/2-mongo-adapter/src/lowering.ts:37 (`` `$${expr.path}` ``); integration test lines 620-626 | Confirmed | | -| 682 | `bucket({ groupBy, boundaries, default_?, output? })` | qb/builder.ts | Confirmed | | -| 683 | `bucketAuto({ groupBy, buckets, output?, granularity? })`, `granularity` a plain string passed through | qb/builder.ts (`granularity?: string`) | Confirmed; "MongoDB rejects the ones it does not know" is **Q** | | -| 684 | `geoNear` options; "`near` is a raw expression" | qb/builder.ts types `near: unknown`, not `MongoAggExpr` | **Corrected** | "`near` is the point to measure from, and Prisma ORM passes it through to MongoDB unchanged." | -| 685 | `graphLookup({ from, startWith, connectFromField, connectToField, as, maxDepth?, depthField?, restrictSearchWithMatch? })`, `startWith` raw | qb/builder.ts | Confirmed | | -| 686 | `setWindowFields` options and the `output` example | qb/builder.ts; ast/stages.ts:777-783; integration test lines 666-681 | Confirmed | | -| 687 | `densify({ field, partitionByFields?, range })`, `range` `{ step, unit?, bounds }` | ast/stages.ts:825-829 | Confirmed | | -| 688 | `fill` options; `output` is `{ method }` or `{ value }`; `method` a plain string | ast/stages.ts:860-863 | Confirmed | | -| 689-691 | `facet(facets)`, `unionWith(collection, pipeline?)`, `Mongo*Stage` classes from `/query-ast/execution` | qb/builder.ts; ast/exports/execution.ts | Confirmed | | -| 704-740 | `facet()` and `bucket()` examples and their result comments | integration test lines 446-490 (same call shapes; `_id` is the lower boundary, rows are `{ _id, count }`) | Confirmed | | -| 749-752 | Atlas stages build anywhere, `search(config, index?)`, `searchMeta(config, index?)`, `vectorSearch({...})` | qb/builder.ts | Confirmed; "the error only appears when you run the query" is **Q** | | -| 768-790 | Nineteen accumulators and their signatures | qb/accumulator-helpers.ts (19 exported) | Confirmed | | -| 792-795 | `{ input, n }`; `top`/`bottom` `{ output, sortBy }`; `topN`/`bottomN` add `n`; `sortBy` plain object; `n: fn.literal(2)` | qb/accumulator-helpers.ts:75-165 | Confirmed | | -| 797 | `sort()` before `group()` for `first`/`last` | integration test lines 800-810 sorts first; MongoDB `$first` is order-dependent | Confirmed | | -| 860-869 | Renames `isIn`, `typeOf`, `toString_`, `firstElem`, `lastElem` | qb/expression-helpers.ts | Confirmed | | -| 871-884 | The full helper list; no `and`/`or`/`not`/`switch`/`ifNull`/`map`/`reduce`/`filter` | qb/expression-helpers.ts (76 helpers, every group name matched one by one) | Confirmed | | -| 886 | `expr(...)` where `.node` is expected throws a `TypeError` | `MongoExprFilter.accept` calls `visitor.expr`, which the aggregation lowering visitor does not define (lowering.ts:36-56) | Confirmed from code; **no test covers it** | | -| 890-894 | The `.node` table; `redact()` listed as needing `.node` | as above | **Corrected** | `redact()` moved to the value row; the options-object row no longer names `redact()` | -| 892 | `f.stage.set()`, `replaceRoot()`, `replaceWith()` take `.node`; `unset()` is not listed | qb/field-accessor.ts:140-145 (`unset(...paths: string[])`) | Confirmed | | -| 907-921 | `MongoOrExpr.of([...])`, `MongoAndExpr.of([...])`, `.not()`; `MongoFieldFilter` has `eq, neq, gt, gte, lt, lte, in, nin, isNull, isNotNull` | ast/filter-expressions.ts:17, 51-89, 113, 140 | Confirmed | | -| 922 | `MongoFieldFilter.neq` versus `fn.ne` | ast/filter-expressions.ts:55; qb/expression-helpers.ts `ne` | Confirmed | | -| 924 | `fn.slice` and `fn.range` positional, and so is every helper not in the table | qb/expression-helpers.ts — seven more named-args helpers were missing from the table | **Corrected** via the table (below); the sentence is now true | -| 925 | `fn.toObjectId(fn.literal(id))` throws a `TypeError` | qb/expression-helpers.ts:96-101 `docUnaryExpr` reads `arg._field.codecId`, and `literal()` sets `_field: undefined as never` | Confirmed from code; **no test covers it** | | -| 931-941 | The named-argument helper table | qb/expression-helpers.ts | **Corrected** | `fn.dateToString` → `{ date, format?, timezone?, onNull? }`; `fn.trim` → `{ input, chars? }` grouped with `ltrim`/`rtrim`; `fn.regexMatch` → `{ input, regex, options? }` grouped with `regexFind`/`regexFindAll`; added `fn.dateFromString` and `fn.replaceOne`/`fn.replaceAll` | -| 943 | `fn.convert`'s `to` takes a type name as an expression | qb/expression-helpers.ts `convert` | Confirmed | | -| 951-971 | Arithmetic, string and `cond()` examples, including `.node` on the condition | qb/expression-helpers.ts `cond(condition: MongoAggExpr, ...)`; integration test lines 780-795 | Confirmed | | -| 979-982 | `pipe(stage)` / `pipe(stage)`; naming rule; rows undecoded after it | qb/builder.ts `pipe` overloads; qb/pipeline-result-shape.ts | Confirmed | | -| 1009 | `aggregate()` is another name for `build()` | qb/builder.ts `aggregate() { return this.build(); }` | Confirmed | | -| 1015-1020 | Updater array rule; bare operation throws; `ORM.MUTATION_DATA_MISSING`; `Cannot mix ...`; `Update spec collision: ...` | qb/update-ops.ts:185-225, 130-140 | Confirmed | | -| 1024 | `insertOne`, `insertMany`, `updateAll`, `deleteAll`, `upsertOne` on the root only | qb/state-classes.ts | Confirmed | | -| 1031 | Write result shapes; "`{ matchedCount, modifiedCount, upsertedCount, upsertedId }`" | ast/result-types.ts:26-31 — the last two are optional | **Corrected** | "…give `{ matchedCount, modifiedCount }`, plus `upsertedCount` and `upsertedId` when an upsert inserted a document" | -| 1032 | `insertOne` takes any record, no contract check | qb/state-classes.ts:173 (`Record`) | Confirmed | | -| 1033 | `_id` may be left out | same, plus `InsertOneResult.insertedId` | Confirmed | | -| 1034 | Values stored exactly as passed | packages/3-mongo-target/2-mongo-adapter/src/mongo-adapter.ts:94-95 passes the document through | Confirmed | | -| 1036 | `insertMany([])` throws `ORM.MUTATION_DATA_MISSING` | qb/state-classes.ts:201 | Confirmed | | -| 1066 | `upsertOne(filterCallback, updaterCallback)` on the root | qb/state-classes.ts:265 | Confirmed | | -| 1082 | After `match()`, `upsertOne()` takes the updater only | qb/state-classes.ts:516 | Confirmed | | -| 1083 | "put a `sort()` between the `match()` and the write and use `findOneAndUpdate()`" | `FilteredCollection` starts `F = 'fam-cleared'`; `sort()` keeps it cleared, so TypeScript withdraws `findOneAndUpdate`. Test asserts exactly this: qb test/state-machine-surface.test-d.ts:46-60 (`@ts-expect-error — sort-past-FilteredCollection keeps F = 'fam-cleared'`) | **Corrected** | "Which one they pick is up to the driver, and the typed builder gives you no way to order that choice." | -| 1084 | `findOneAndUpdate`/`findOneAndDelete` return the driver's document, `_id` an `ObjectId` | qb/state-classes.ts (no `resultShape` on those plans) | Confirmed | | -| 1085 | Only `skip()` withdraws them | same test file, lines 62-112: `group`, `limit`, `addFields`, `project`, `unwind`, `skip`, and `sort` all clear the marker | **Corrected** | "offered straight after `match()` and nowhere else. TypeScript withdraws them after every other stage, `sort()` and `skip()` included. If a cast gets one past TypeScript after a `skip()`, it throws an error whose `code` is `ORM.OPERATION_UNSUPPORTED`." | -| 1116 | `findOneAndUpdate()`'s second argument optional, `returnDocument` default `'after'`, `upsert` default `false` | qb/state-classes.ts:545-575 (`opts … = {}`, `?? 'after'`, `?? false`) | Confirmed | | -| 1125-1127 | `returnNewDocument` is not a valid option; unknown key ignored | the options type admits only `upsert` and `returnDocument`, and the constructor reads only those | Confirmed | | -| 1139-1140 | Operator form has fourteen operators; pipeline form has `set`, `unset`, `replaceRoot`, `replaceWith` | qb/field-accessor.ts:70-90 (14 operators); `StageEmitters` (4) | Confirmed | | -| 1145-1160 | The operator argument table, including `pop(direction)` defaulting to `1` | qb/field-accessor.ts:262 (`pop: (direction = 1)`); qb/update-ops.ts | Confirmed | | -| 1160 | `setOnInsert` does nothing outside `upsertOne()` | qb/update-ops.ts emits `$setOnInsert`; MongoDB applies it only on upsert insert | Confirmed | | -| 1189 | `out(collection, db?)` | qb/builder.ts `out(collection: string, db?: string)` | Confirmed | | -| 1190-1191 | `merge({ into, on, whenMatched, whenNotMatched })`; `whenMatched` also takes `MongoAddFieldsStage`/`MongoProjectStage`/`MongoReplaceRootStage` | qb/builder.ts `merge()`; `MongoUpdatePipelineStage` union | Confirmed | | -| 1192 | Both return an empty array | `#writeTerminal` returns `MongoQueryPlan`; `$out`/`$merge` emit no documents | Confirmed | | -| 1214 | `rawCommand()` takes `RawAggregateCommand(collection, pipeline)`, rows `unknown` | qb/query.ts:30, 41-59; ast/raw-commands.ts:4-15 | Confirmed | | - -## 2. Q list (left as written) - -| Claim | Searches run | -|---|---| -| `lookup()` with no match gives an empty array (line 366) | `grep -rn "lookup" test/integration/test/mongo/query-builder.test.ts`; qb test dir; only the matching case is tested | -| `replaceRoot()` message `'newRoot' expression must evaluate to an object` (line 546) | `grep -rn "newRoot" packages/ test/` — the string is nowhere in the tree; it is MongoDB's own | -| `bucketAuto`'s "MongoDB rejects the ones it does not know" about `granularity` (line 683) | `grep -rn "granularity" packages/` — only the pass-through `granularity?: string` | -| Atlas stages: "the error only appears when you run the query" (line 749) | `grep -rn "Atlas\|mongot\|\$search" packages/2-mongo-family packages/3-mongo-target` — nothing checks for Atlas; the builder appends the stage and sends it. Nothing in the tree runs one | -| The result comments `// { total: 2, counted: 2 }`, `// { firstTitle: 'Hello world', lastTitle: 'Tutorial one' }`, `// [{ total: 2 }]`, `// { earliest: , avgYear: 2024 }` | The integration suite uses a different fixture (Products/Orders, five documents), so no test seeds this page's `posts`. The numbers are internally consistent with a two-document `posts` collection and match the ORM client page's data, but no source file decides them | - -Two claims the code supports but **no test covers**: `fn.toObjectId(fn.literal(...))` throwing a `TypeError` (line 925) and `expr(...)` in a `.node` slot throwing a `TypeError` (line 886). Both follow directly from the code, so I left them stated as facts. - -## 3. Page-conventions bullets - -- The rc.10 bullet: the one rc.10 change named in the brief (buffered PostgreSQL queries releasing their pooled connection early) is PostgreSQL-only. This page is MongoDB-only and says nothing about connection release, so nothing to change. -- The last bullet (the generated error-reference page is out of scope): nothing in this slice touches it. The page links to `/orm/reference/raw-queries`, `/orm/reference/orm-client`, `/orm/reference/sql-query-builder`, `/orm/reference/transactions-and-runtime` and `/orm/coming-from-prisma-orm-7`, none of them the error reference. -- Anchors: no heading text, numbering, order, or pinned anchor (`[#read-terminals]`, `[#pipeline-write-terminals-out-and-merge]`) was changed. -- `bash .claude/skills/docs-reader-review/scripts/check-plain.sh wip/c21/ref/fc2/pipeline-builder.mdx` → `plain-language check: clean`. - -## 4. Line count - -Before: 1238. After: 1240. The two extra lines are rows in the named-argument helper table for helpers that were missing from it, which the sentence above the table asserted were positional. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/factcheck2.raw-queries.md b/docs/orm-docs-audit/c21-orm-reference/factcheck2.raw-queries.md deleted file mode 100644 index c1ab0f2c779..00000000000 --- a/docs/orm-docs-audit/c21-orm-reference/factcheck2.raw-queries.md +++ /dev/null @@ -1,99 +0,0 @@ -Fact re-check complete. Both slices are the same length as before (raw-queries 354 lines, index 41 lines). Five corrections, all on the raw-queries page; the index page needed none. - -## 1. Claims checked - -### `index.mdx` - -| Line | Claim | Source | Verdict | -| --- | --- | --- | --- | -| 11 | ORM client at `db.orm`, works on PostgreSQL and MongoDB | `packages/3-extensions/postgres/src/runtime/postgres.ts:51-61`, `packages/3-extensions/mongo/src/runtime/mongo.ts:30-40` | Confirmed | -| 12 | SQL query builder at `db.sql`; not on the MongoDB client | same two files (no `sql` on `MongoClient`) | Confirmed | -| 13 | Pipeline builder at `db.query`, MongoDB only | `mongo.ts:31`; no `query` on `PostgresClient` | Confirmed | -| 17 | `.build()` then `runtime.query(...)`, or `runtime.execute(...)` for a write with no rows | `packages/2-sql/5-runtime/src/sql-runtime.ts:99-113`, `packages/2-mongo-family/7-runtime/src/mongo-runtime.ts:37-47,99` | Confirmed | -| 20, 26 | `const runtime = db.runtime();` on PostgreSQL, `await db.runtime()` on MongoDB | `postgres.ts:66` (`runtime(): Runtime`), `mongo.ts:38` (`runtime(): Promise`) | Confirmed | -| 21 | `db.orm.public.User.where((u) => u.kind.eq('admin')).all()` | `packages/3-extensions/sql-orm-client/src/collection.ts:319-325`; `test/integration/test/sql-orm-client/mn-include.test.ts:257` | Confirmed | -| 22 | `db.sql.public.user.select('id','email').where((f, fns) => fns.eq(f.kind,'admin')).build()` | `packages/2-sql/4-lanes/sql-builder/src/types/select-query.ts:40`, `src/expression.ts:38-41` | Confirmed | -| 22 | `'admin'` is the stored value of the `user_type` enum | example schema in `sql-query-builder.mdx:17-40` (member with no `=`) | Confirmed | -| 26 | `.all()` replaced `findMany` | conventions.md; `collection.ts` doc comments | Confirmed | -| 28 | `User` is the model name, `user` the table name | `@@map("user")` in the shared example schema | Confirmed | -| 30 | Two things named `query` | `packages/2-mongo-family/5-query-builders/query-builder/src/query.ts:30`; `sql-runtime.ts` | Confirmed | -| 12 | SQL query builder "is PostgreSQL only" | `@prisma/orm-sqlite` exists, so the builder also runs on SQLite; page-conventions.md sanctions this wording for the docs' PostgreSQL/MongoDB scope | Left as is, noted below | - -### `raw-queries.mdx` - -| Line | Claim | Source | Verdict | -| --- | --- | --- | --- | -| 11 | Two kinds: `fns.raw` / `db.raw.sql`, and `db.raw.collection(...)` / `db.query.rawCommand(...)` | `sql-builder/src/expression.ts:116`, `types/raw-query.ts:165-172`, `orm/src/mongo-raw.ts`, `query-builder/src/query.ts:30` | Confirmed | -| 19 | `.returnsRow(spec).build()` → `runtime.query`; `.affectedCount().build()` → `runtime.execute` | `relational-core/src/expression.ts:304-316,460-482`; `test/integration/test/raw-query.integration.test.ts:117-149` | Confirmed | -| 22 | Type id names the JavaScript type; the property is `codecId` | `types/raw-query.ts:29-36`, `expression.ts:226` | Confirmed | -| 22 | `.returnsRow()` converts, `.returns()` does not | `2-sql/5-runtime/src/codecs/decoding.ts:66-100` (row-spec codecs) vs `sql-builder/src/runtime/builder-base.ts:331-332` (a raw fragment's `returnType` has no `codec`, so no codec is attached) | Confirmed. No test covers the fragment case directly; it follows from the code. | -| 22 | A `numeric` column arrives as a string | `postgres/src/core/codecs.ts:922` (wire form is a decimal string) | Confirmed | -| 22 | A date column gets no conversion by Prisma ORM | same mechanism as the fragment case above | Confirmed | -| 25 | `db.runtime()` direct on PostgreSQL, a promise on MongoDB | `postgres.ts:66`, `mongo.ts:38` | Confirmed | -| 38 | `select()` callback takes `f` and `fns`; `fns.raw` is a tagged template | `sql-builder/src/runtime/builder-base.ts:318-336`, `src/expression.ts:116` | Confirmed | -| 38 | Fragments go in `select()`, `where()`, `orderBy()`, `update()` | `types/select-query.ts:40-53`, `types/table-proxy.ts:132-137` | Confirmed | -| 50 | `.returns()` takes a type id string or `{ codecId, nullable }` | `types/raw-query.ts:126-134`, `expression.ts:430-441` | Confirmed | -| 50 | "`.returns(db.sql.public.user.columns.id)` is a type error; only `.returnsRow(spec)` takes columns" | `types/raw-query.ts:29-36,130-134`: a `ContractColumnRef` is `{ codecId, nullable, [symbol]? }`, which matches the object overload of `.returns()`; nothing excludes it | **Corrected** — both sentences removed. The paragraph now stops after the two accepted forms. | -| 54 | A `.returns('pg/bool@1')` fragment works straight in `where()` | `sql-builder/src/expression.ts:12,38-41` | Confirmed | -| 67 | "Interpolated values are sent as query parameters, so they are safe. Column and table names cannot be parameters, so they would be pasted into the SQL text. Never build an identifier from user input." | `relational-core/src/expression.ts:336-360`: an interpolated string is bound as a `ParamRef`; there is no way to interpolate an identifier at all, so nothing is pasted into the SQL | **Corrected** — now: "The expression itself goes into the fragment, not its rendered SQL, so it stays type-checked. An interpolated value is sent as a query parameter, never pasted into the SQL. A column or table name cannot be interpolated as a string, because a string becomes a parameter." The run-time-chosen-column advice is kept. | -| 84-92 | The bare-value type id table, all seven rows | `packages/3-targets/6-adapters/postgres/src/core/adapter.ts:120-141` | Confirmed | -| 94 | Type id shape `pg/` + name + `@1`, suffixes such as `int8number`, `timestamptz-temporal` | `postgres/src/core/codec-ids.ts` (full list) | Confirmed | -| 94 | `pg/uuid@1`, `pg/numeric@1`, `pg/jsonb@1` exist | `codec-ids.ts:21,37,39` | Confirmed | -| 94 | `db.sql.public.
.columns..codecId` | `sql-builder/test/types/raw-query.types.test-d.ts:113-117` | Confirmed | -| 94 | `contract.json` path `storage.namespaces..entries.table.
.columns..codecId` | `test/integration/test/fixtures/contract.json` (read the tree directly) | Confirmed | -| 94 | `pg/timestamptz-temporal@1` takes a `Temporal.Instant`, not a `Date` | `postgres/src/core/temporal-codecs.ts:139-160`; `authoring.ts:735-741` | Confirmed | -| 94 | "`Temporal` is a global in Node.js 26 and later" | `skills/prisma-8/references/contract.md:118`: Node.js **26.8.2** and later ship `globalThis.Temporal`; 26.8.1 and earlier do not | **Corrected** — "Node.js 26.8.2 and later". | -| 94, 99 | Polyfill `import 'temporal-polyfill/full/global'` | `test/integration/test/setup-temporal.ts:1` | Confirmed | -| 97 | `param` from `@prisma/orm-postgres/relational-core/expression` | `packages/9-public/@prisma/orm-postgres/package.json:127`; `relational-core/src/expression.ts:132` | Confirmed | -| 110-115 | `{ codecId, nullable: true }` object form of `.returns()` | `expression.ts:433-441` | Confirmed | -| 123 | `db.raw` holds `sql` on PostgreSQL, `collection(...)` on MongoDB | `types/raw-query.ts:165-172`, `orm/src/mongo-raw.ts:8-11` | Confirmed | -| 136 | Spec entries are a contract column or a type id; object form for a nullable column | `types/raw-query.ts:50-62`; `test/types/raw-query.types.test-d.ts:20-75` | Confirmed | -| 136 | Missing spec column throws `RUNTIME.RAW_ROW_COLUMN_MISSING` | `2-sql/5-runtime/src/codecs/decoding.ts:316-323`; `raw-query.integration.test.ts:224-233` | Confirmed | -| 136 | `SqlQueryError` with `sqlState`, `23505` for a unique violation | `packages/2-sql/1-core/errors/src/errors.ts:7-56` | Confirmed | -| 150-154 | `postCount: 'pg/int8@1'` reads back as a `bigint` | `raw-query.types.test-d.ts:40-47`; `raw-query.integration.test.ts:167-200` | Confirmed | -| 142-148 | `FROM "user"` with no schema qualifier | the statement is sent verbatim; `public` is on the default `search_path` | Confirmed (a PostgreSQL fact, not a Prisma one) | -| 157 | `.affectedCount()` + `runtime.execute` → `{ affectedRows }` | `raw-query.integration.test.ts:140-149` | Confirmed | -| 157 | `tx.query` / `tx.execute` take the same built queries | `sql-runtime.ts:132-140` (`TransactionContext extends RuntimeQueryable`) | Confirmed | -| 170 | Nesting: interpolate before `.build()`, outer `.build()` only, `.returns` property, no nesting of `.affectedCount()`, `UPDATE ... RETURNING` nests | `expression.ts:296-316,460-482`; `raw-query.types.test-d.ts:89-108` | Confirmed | -| 172-173 | The three similar names | same as above | Confirmed | -| 195-199 | Accepted interpolations; a `Date` is a TypeScript error; the exact error code and message | `relational-core/src/ast/types.ts:44`; `expression.ts:329-360` (message matches character for character) | Confirmed | -| 205 | Nine methods, no raw `find()`, no `public` segment, no MongoDB transactions | `orm/src/raw-collection.ts:22-39`; `mongo-raw.ts`; `transactions-and-runtime.mdx:555` | Confirmed | -| 207 | Every method, writes included, runs through `runtime.query(...)` and yields a one-element array | `mongo-runtime.ts:37-47` — `execute()` supports only the typed `updateOne/updateMany/deleteOne/deleteMany` kinds and throws `RUNTIME.MONGO_STATISTICS_UNSUPPORTED` for the `raw*` kinds; `7-runtime/test/raw-commands.test.ts` runs every raw write through `query()` | Confirmed | -| 210 | "The write methods give you the `mongodb` package's own result objects" | `packages/3-mongo-target/3-mongo-driver/src/mongo-driver.ts:118-175` builds new objects from the driver's fields, and reshapes `insertedIds` from a map to an array | **Corrected** — "give you the counts and ids under the `mongodb` package's own key names". The listed shapes were already right. | -| 210 | `_id` is an `ObjectId`, compare with `String(...)` | `mongo-driver.ts:176-200` (documents pass through undecoded) | Confirmed | -| 228-233 | `aggregate()` sends the pipeline unchanged, so a real `ObjectId` matches | `mongo-adapter.ts:285` (`rawAggregate` passes `draft.pipeline` through) | Confirmed | -| 238-248 | `{ insertedId }`; `{ insertedCount, insertedIds }` as an array in input order | `mongo-driver.ts:118-146` | Confirmed | -| 253-270 | `updateOne/updateMany`, document or pipeline form, `{ matchedCount, modifiedCount, upsertedCount, upsertedId }` | `raw-collection.ts:26-29`; `mongo-driver.ts:126-158`; `7-runtime/test/raw-commands.test.ts:69-150` | Confirmed | -| 275-284 | `deleteOne/deleteMany` → `{ deletedCount }` | `mongo-driver.ts:160-174` | Confirmed | -| 293 | `findOneAndUpdate()` takes only `{ upsert }`; TypeScript rejects `sort` or `returnDocument` | `raw-collection.ts:33-37,81-90` | Confirmed | -| 295-302 | `RawFindOneAndUpdateCommand` and its six arguments, the optional ones, the pre-image default | `query-ast/src/raw-commands.ts:96-127` | Confirmed | -| 305 | Import from `@prisma/orm-mongo/query-ast/execution` | `packages/9-public/@prisma/orm-mongo/package.json:96`; `query-ast/src/exports/execution.ts:41-51` | Confirmed | -| 314-323 | Upsert counter: first call gives `[]`, then `count: 1`, then `count: 2`; a plain string `_id` is allowed | `mongo-driver.ts:176-187` (`if (result) yield`); `7-runtime/test/raw-commands.test.ts:113-127` uses `_id: 'pageViews'` | Confirmed. The test pins `returnDocument: 'after'`; the `[]`-then-`1`-then-`2` sequence follows from the code, and no test covers it. | -| 328-334 | `findOneAndDelete()` yields the raw document; `String(deleted._id) === aliceId` | `mongo-driver.ts:189-200`; `7-runtime/test/raw-commands.test.ts:186-196` | Confirmed | -| 339 | "runs a raw MongoDB **aggregate** command"; "takes no type parameter" | `query-builder/src/query.ts:30,44-59`: `rawCommand` accepts any `AnyMongoCommand` (the page's own example passes a `RawFindOneAndUpdateCommand`), and it does have a type parameter, just not one for the row | **Corrected** — "runs a raw MongoDB command"; "has no row type parameter". | -| 339 | Returns a built query, so no `.build()`; rows are `unknown` | `query.ts:44-59` | Confirmed | -| 349-353 | `rawCommand()` sends the command unchanged, so a real `ObjectId` matches | `mongo-adapter.ts:285` | Confirmed | -| Cross-page links | `#example-schema` (both), `#select`, `#fnsraw-and-returns`, `#dbtransactioncallback`, `#transactions-mongodb`, `#rawcommand` | the target headings in the four sibling pages | Confirmed | - -## 2. Q list (unverifiable) - -None. Every claim resolved for or against. - -Two claims rest on code rather than a test, which I state above rather than marking Q: a raw fragment's `.returns()` not converting the value, and the first upsert-counter call returning `[]`. - -## 3. Page-conventions' last bullet - -The last bullet says the error-reference page is generated upstream and out of scope. Neither of my slices touches it. The two error codes they name, `RUNTIME.RAW_SQL_UNSUPPORTED_INTERPOLATION` and `RUNTIME.RAW_ROW_COLUMN_MISSING`, are written in the prescribed "throws an error whose `code` is `X`" form and match the source strings exactly. - -## 4. Line counts - -| File | Before | After | -| --- | --- | --- | -| `wip/c21/ref/fc2/raw-queries.mdx` | 354 | 354 | -| `wip/c21/ref/fc2/index.mdx` | 41 | 41 | - -`check-plain.sh` is clean on both. - -## 5. One item for the operator - -The index page says the SQL query builder "is PostgreSQL only". The source tree ships `@prisma/orm-sqlite`, so the builder also runs on SQLite. I left the wording alone because `page-conventions.md` names "PostgreSQL only" as the approved phrasing, and the same sentence predates these rounds. If SQLite is meant to be visible in the docs, this sentence and its twin on the SQL query builder page both need a decision. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/factcheck2.sql-query-builder.md b/docs/orm-docs-audit/c21-orm-reference/factcheck2.sql-query-builder.md deleted file mode 100644 index 70cde08d3af..00000000000 --- a/docs/orm-docs-audit/c21-orm-reference/factcheck2.sql-query-builder.md +++ /dev/null @@ -1,79 +0,0 @@ -Fact re-check done on `wip/c21/ref/fc2/sql-query-builder.mdx`. Five corrections, 1008 lines before and after. - -## Claims checked - -| Line | Claim | Deciding source | Verdict | -|---|---|---|---| -| 131 | `npm install @prisma/orm-postgres`; `contract emit` writes `contract.json` + `contract.d.ts`; files in `src/prisma/`; type import `./contract.d` | `packages/9-public/@prisma/orm-postgres/package.json:2`; `packages/1-framework/3-tooling/cli/test/control-api/contract-emit.test.ts:282-283`; `packages/3-extensions/postgres/README.md:33-38` | Confirmed | -| 133 | `public` is the PostgreSQL schema | `packages/3-targets/6-adapters/postgres/.../contract.ts` default namespace `public`; `packages/2-sql/2-authoring/contract-psl/src/interpreter.ts:1172` | Confirmed | -| 133 | "Prisma ORM 8 puts every table in `public`" | `packages/1-framework/2-authoring/psl-parser/src/parse.ts:519` (`namespace` blocks); `test/integration/test/authoring/parity/native-enum/schema.prisma:3,15` (`public` and `auth`); `interpreter.ts:1172` (default only) | **Corrected** → "A model goes in `public` unless your contract puts it in a `namespace` block." | -| 133 | Table accessor keyed by table name; `@@map`; `@map` columns | `sql-builder/src/runtime/sql.ts:38-55`, `table-proxy-impl.ts` | Confirmed | -| 137 | `db.runtime()`; `db.close()` | `packages/3-extensions/postgres/src/runtime/postgres.ts:66,72,374` | Confirmed | -| 139, 146-150 | `postgres({ contractJson, url })`; synchronous `runtime()`; build then `query` | `postgres.ts:86-105,266`; `builder-base.ts:buildPlan` | Confirmed | -| 139 | A built query can run more than once | plan is a frozen data object; `query()` returns a new `AsyncIterableResult` each call (`runtime-scope.ts:24`) | Confirmed | -| 153 | SQLite: `sqlite(...)` from `@prisma/orm-sqlite/runtime`; no `public` segment; `lateralJoin`/`outerLateralJoin`/`distinctOn` absent, everything else present | `packages/9-public/@prisma/orm-sqlite/README.md:24`; `packages/3-extensions/sqlite/src/static/sqlite-static.ts:33-38` (unbound namespace); `packages/3-targets/6-adapters/sqlite/src/core/adapter.ts:76-84` (`lateral: false`, no `postgres` namespace, `returning: true`) | Confirmed | -| 166, 505-519 | `.as()` before `build()`; join source; not inside `lateralJoin`; not buildable; SELECT and grouped only | `query-impl.ts:150-168` (`as()` returns only `getJoinOuterScope`/`buildAst`); `types/shared.ts` `WithAlias` on SelectQuery/GroupedQuery, absent on Insert/Update/Delete | Confirmed | -| 170 | Join must precede calls naming joined columns | scope grows only at join (`joined-tables-impl.ts:#addJoin`) | Confirmed | -| 170, 445 | `distinctOn()` columns must be the first `orderBy()` keys; later keys pick the kept row | nothing in the builder checks it (`builder-base.ts:resolveDistinctOn`) | **Q** (PostgreSQL rule, see Q list) | -| 172-174 | Scope; `f` per column; after a join columns under table name, unique names also bare | `scope.ts:MergeScopes`, `builder-base.ts:mergeScopes`, `field-proxy.ts` | Confirmed | -| 174 | Enum: pass the stored value; `High = "high"` → `'high'`; bare `admin` → `'admin'` | `contract-psl/test/interpreter.enum.test.ts:126-133` and `:624-648` (bare members store their names) | Confirmed | -| 186 | Unknown `select()` name throws `ORM.COLUMN_UNKNOWN` | `builder-base.ts:resolveSelectArgs`; `test/runtime/structured-errors.test.ts:59-68` | Confirmed | -| 187-188 | Aliased-expression and object forms | `types/shared.ts:WithSelect` | Confirmed | -| 190 | `select()` adds, never replaces | `query-impl.ts:select` (`rowFields: {...old, ...new}`, projections appended) | Confirmed | -| 192 | Type ids `pg/text@1`, `pg/int4@1`, `pg/int8@1`, `pg/float8@1`, `pg/bool@1`, `pg/uuid@1`, `pg/timestamptz-temporal@1`; `DateTime` → timestamptz-temporal; read from `.columns..codecId` | `postgres/src/core/codec-ids.ts:8-40`; `contract-psl/test/ts-psl-parity.test.ts:217`; `table-proxy-impl.ts:get columns` | Confirmed | -| 281 | `innerJoin()` scope wording | as line 172-174 | Confirmed | -| 312 | `outerLeftJoin`/`outerRightJoin`/`outerFullJoin` = LEFT/RIGHT/FULL OUTER JOIN | `joined-tables-impl.ts` join types; `postgres/src/core/sql-renderer.ts:804-809` renders `LEFT JOIN` (SQL synonym of `LEFT OUTER JOIN`) | Confirmed | -| 334, 444 | SQLite rejects `lateralJoin`/`distinctOn` in TypeScript; a cast call throws `ORM.CAPABILITY_MISSING` | `scope.ts:GatedMethod`; `builder-base.ts:assertCapability`; `test/runtime/structured-errors.test.ts:83-96`; `builders.test.ts:365-401` | Confirmed | -| 335-337, 357-371 | `lateral.from(...)`, return the chain directly, alias scoping, ambiguous bare names throw | `joined-tables-impl.ts:#buildLateral`; `builders.test.ts:373-389` | Confirmed | -| 338 | `outerLateralJoin(alias, callback)` is `LEFT JOIN LATERAL` | `joined-tables-impl.ts` (`'left'` + lateral); `sql-renderer.ts:804-809` | Confirmed | -| 385-391 | `nulls` accepted by TypeScript, never reaches the SQL; CASE-fragment workaround | `expression.ts:OrderByOptions`; `builder-base.ts:resolveOrderBy` (only `asc`/`desc`); no `NULLS` string anywhere in `packages` | Confirmed | -| 420, 433 | `distinct()` is `SELECT DISTINCT` | `sql-renderer.ts:204,286-291` | Confirmed | -| 482-483 | `limit(n)` / `offset(n)` take `number` only | `types/shared.ts:PaginationValue` = `number` \| numeric `TraitExpression`; `query-impl.ts:limit/offset` | **Corrected** → "`number`, or a numeric expression" (restores the pre-round wording) | -| 540 | Aggregates in `select()` first, then `groupBy()` | `types/grouped-query.ts` has no `select()` | Confirmed | -| 583-587 | `fns.count(f.id)` grouped example | `test/integration/test/sql-builder/group-by.test.ts:8-36` | Confirmed | -| 591 | `count()`/integer `sum()` past 2^53-1 throws `RUNTIME.DECODE_FAILED` | `postgres/src/core/codec-helpers.ts:152-196` | Confirmed | -| 592 | `avg()` is float8, `avgDecimal` exact | `postgres/src/core/aggregates.ts` (`avg` → float8 via cast; `avgDecimal` → numeric) | Confirmed | -| 593, 597-599 | Errors are `Error` with `code` | `utils/structured-error`, `postgres/src/core/errors.ts` | Confirmed | -| 606-617 | `EXTRACT` typed `pg/numeric@1` arrives as a string; `groupBy` by alias; repeat the fragment to group by the expression | `expression.ts:RawSqlBuilderImpl.returns` (no codec on the projection, so no decode); `codecs.ts:896-907` (numeric decodes to string); `builder-base.ts:resolveGroupBy` (alias in scope via `orderByScopeOf`) | Confirmed | -| 620 | Unknown `groupBy` name throws `ORM.COLUMN_UNKNOWN` at call; ungrouped column fails at run time with PostgreSQL's own error | `resolveGroupBy`; nothing validates grouping | Confirmed | -| 628 | `count()` vs `1n` → `RUNTIME.ENCODE_FAILED`; `countBigInt()` compares against `1n` | `codec-helpers.ts:77-84,174-176`; aggregate output codecs `pg/int8number@1` vs `pg/int8@1` | Confirmed | -| 629 | Selected alias in `having()` accepted by TypeScript, rejected by PostgreSQL | `grouped-query.ts:having` takes `OrderByScope` (aliases included); rejection is PostgreSQL's | Partly Q (see Q list) | -| 652, 667 | `having` with `fns.gt(fns.sum(...), 1000)` / `fns.gt(fns.count(), 1)` | `group-by.test.ts:38-50` | Confirmed | -| 674 | "The alias must be one you selected. Any other name throws `ORM.COLUMN_UNKNOWN`" | `builder-base.ts:resolveOrderBy` also accepts any base-table column in scope | **Corrected** → "A name that is neither a column of the table you started from nor an alias you selected throws…" (matches the `groupBy()` wording at line 620) | -| 693-694 | `where()` optional on `update()`/`delete()` | `mutation-query.ts` (`where` optional in the chain) | Confirmed | -| 697 | `execute` → `{ affectedRows }`, `query` with `returning()` | `relational-core/src/runtime-scope.ts:24-25`; `mutation.test.ts:103-109` (`toEqual({ affectedRows: 1 })`) | Confirmed | -| 705 | `insert()` always takes an array; empty array → `ORM.MUTATION_DATA_MISSING` | `mutation-impl.ts:254-260` (thrown at `build()`) | Confirmed | -| 706 | Defaults applied at `build()`; re-running inserts the same id | `mutation-impl.ts:253-270` (`buildParamValues` inside `build()`); `mutation-defaults.test.ts:8-22` | Confirmed | -| 707 | No `ON CONFLICT`, no upsert | no `conflict` handling in `mutation-impl.ts` or `InsertAst` | Confirmed | -| 713-715 | Every column optional in TypeScript; PostgreSQL still rejects a missing required column | `mutation-query.ts:InsertValues` (all optional) | Confirmed / database side Q | -| 745-746 | `returning()` on PostgreSQL and SQLite; column names only, no `'*'`, no expression | `mutation-query.ts:ReturningCapability`; both adapters set `sql.returning: true`; `mutation-impl.ts:202-226` (names resolved against scope) | Confirmed | -| 775-782 | `update()` object form or callback, cannot mix; callback values must be expressions; `fns.raw` for a fixed value | `table-proxy.ts:update` overloads; `table-proxy-impl.ts:update`; `mutation-impl.ts:evaluateUpdateCallback` | Confirmed | -| 819 | `delete()` takes no arguments | `table-proxy.ts:delete(): DeleteQuery` | Confirmed | -| 846 | `param` from `@prisma/orm-postgres/relational-core/expression` | export map `./relational-core/expression`; `relational-core/src/expression.ts:132` | Confirmed | -| 847 | "A bare string becomes `pg/text@1`, so a value going into a `uuid` column needs `param(...)`" | `functions.ts:binaryWithSharedCodec` threads the column codec, so a comparison needs no `param()`; only `fns.raw` interpolation infers `pg/text@1` (`expression.ts:336-354`, `postgres adapter.ts:121-140`) | **Corrected** → scoped to "Inside `fns.raw` no column lends its type, so …" | -| 847 | `Temporal.Instant` cannot be interpolated bare | `postgres/src/core/adapter.ts:132-139` (`RUNTIME.RAW_SQL_UNSUPPORTED_INTERPOLATION`) | Confirmed | -| 855, 861, 870 | `param(value, { codecId })` → `ParamRef`, usable in `fns.raw` | `expression.ts:132-134` | Confirmed | -| 888-894, 896-911 | Comparison/membership/boolean/existence signatures; `in`/`notIn` take an array or an unbuilt subquery; `exists`/`notExists` an unbuilt subquery | `expression.ts:BuiltinFunctions`; `functions.ts:inOrNotIn`; `test/integration/test/sql-builder/subquery.test.ts:6-47` | Confirmed | -| 917-924 | Aggregate table: `count` number, `countBigInt` bigint, `sum` number over int4/int8, string over numeric, number over float8, `sumBigInt` bigint, `avg` number, `avgDecimal` exact string, `min`/`max` input type with `VarChar` → `text` | `postgres/src/core/aggregates.ts:152-228`; `codecs.ts:896-907` (numeric → string); `control-mutation-defaults.ts:163-206,275` (Int/BigInt/Float/Decimal/Numeric/Uuid mappings) | Confirmed | -| 926 | `ilike(expr, pattern)` on text; `cosineDistance(a, b)` → number, pgvector only | `postgres/src/core/descriptor-meta.ts:155-168`; `pgvector/src/core/descriptor-meta.ts:19-36` (float8) | Confirmed | -| 930, 941-943 | `fns.raw` interpolation; `.returns()` always required; annotation only, no conversion; no `fns.coalesce`/`fns.cast` | `expression.ts:RawSqlBuilderImpl` (the Expression exists only after `.returns()`, and carries no codec); `BuiltinFunctions` has no coalesce/cast | Confirmed | -| 937 | `.returns(typeId)` type is `string` | `expression.ts:RawSqlBuilder` also takes `{ codecId, nullable }`; raw-queries.mdx:50 says so | **Corrected** → "`string`, or `{ codecId, nullable }`" | -| 963 | `build()` takes zero arguments on every query type | `types/shared.ts:WithBuild`, `mutation-query.ts` `build()` | Confirmed | -| 977-979 | `query` → rows (await or `for await`), `execute` → `{ affectedRows }`, which to use | `runtime-scope.ts:23-26` | Confirmed | -| 987-989 | `ResultType` from `@prisma/orm-postgres/components/runtime`; one row; works on ORM queries | `framework-components/src/exports/runtime.ts:14`, `execution/query-plan.ts:49`; `sql-orm-client/src/collection.ts:225` (`_row`); `docs/releases/v8.0.0-rc.10.md:27` | Confirmed | -| 1006 | `for await` gives no memory benefit on PostgreSQL; "use `await`" | `postgres.ts:246` (`cursor: { disabled: true }`) | Confirmed | -| 1008 | Re-awaiting is safe; mixing modes or looping twice throws `RUNTIME.ITERATOR_CONSUMED` | `framework-components/src/execution/async-iterable-result.ts:13-60` | Confirmed | - -## Q list (unverifiable from the source) - -These are PostgreSQL server behaviours the Prisma source neither implements nor tests. Page text left alone. - -1. `distinctOn()` columns must be the leftmost `orderBy()` keys, and later keys decide the kept row (lines 170, 445). Searches: `grep -rn "distinctOn" packages --include='*.ts'`, `grep -rn "DISTINCT ON" packages`, read `builder-base.ts:resolveDistinctOn` and `sql-renderer.ts:286-291`. Nothing validates or documents the ordering rule; the builder renders whatever you give it. -2. PostgreSQL rejects a selected alias inside `HAVING` (line 629). Searches: `grep -rn "having" packages/2-sql --include='*.ts'`, `group-by.test.ts`. No test uses an alias in `having()`; the type surface allows it. -3. PostgreSQL rejects an insert that omits a required column with no default (line 715). Searches: `grep -rn "MUTATION_DATA_MISSING\|NOT NULL" packages --include='*.ts'`. No Prisma-side check; it is the database's error. - -## Notes - -- The examples on this page are not copies of the SQL-builder integration tests: those run against a different fixture (`test/integration/test/sql-builder/fixtures/contract.ts` — `users`/`posts`/`comments` with `int4` ids). The page uses the shared reference example schema instead. I checked every example for call shape and argument type against the source rather than against fixture data. The page makes no claim of being copied from a test suite, so no change. -- Two accurate-but-incomplete spots I left alone because fixing them would lengthen the page: `distinctOn()`'s Options table lists column names only, while the source (`select-query.ts`) also accepts an expression callback; and `orderBy()`'s `nulls` option is described in prose only. -- The page no longer states that `db.runtime()` is synchronous on PostgreSQL (a round removed it). The `page-conventions.md` bullet asks for that sentence where `runtime()` first appears. The example at line 147 does not `await` it, so nothing is wrong, but the operator may want the sentence back. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/factcheck2.transactions-and-runtime.md b/docs/orm-docs-audit/c21-orm-reference/factcheck2.transactions-and-runtime.md deleted file mode 100644 index a006337bd5a..00000000000 --- a/docs/orm-docs-audit/c21-orm-reference/factcheck2.transactions-and-runtime.md +++ /dev/null @@ -1,83 +0,0 @@ -Fact re-check complete. Slice: `wip/c21/ref/fc2/transactions-and-runtime.mdx` (771 lines before, 770 after). - -## Claims checked - -| Slice line | Claim | Source | Verdict | -|---|---|---|---| -| 9 | `contract emit` writes `contract.json` + `contract.d.ts`; `findMany` → `db.orm.public.User.where(...).all()` | `cli/src/commands/init/templates/code-templates.ts:318-345`; `test/integration/test/cli-journeys/init-journey.e2e.test.ts:166-186` | Confirmed | -| 11 | `db` is what `postgres(...)` / `mongo(...)` return; `runtime()` runs queries | `3-extensions/postgres/src/runtime/postgres.ts:155-384`, `mongo.ts:116-240` | Confirmed | -| 17 | "Transactions are PostgreSQL only" | `3-extensions/sqlite/src/runtime/sqlite.ts:83` has `transaction()` | **Corrected** → "Transactions are not available on MongoDB. To run one there, …" | -| 17 | MongoDB route: `mongoClient` option + `session.withTransaction(...)` | `mongo.ts:56,197-214`; `projects/sqlite-mongo-transactions/spec.md:20` | Confirmed | -| 29 | `contractJson` or `contract`, exactly one | `postgres.ts:92-109` (`contract?: never` / `contractJson?: never`) | Confirmed | -| 30 | `` needed with `contractJson`, inferred with `contract`; `contract.d.ts` exports `Contract` | `postgres.ts:99-105,155-160`; init e2e test line 167 | Confirmed | -| 31 | Three ways (`url`, `pg`, `binding`); binding kinds `url` / `pgPool` / `pgClient`; exactly one, or pass to `connect()` | `postgres/src/runtime/binding.ts:11-31,67-127` | Confirmed | -| 32 | You own a `pg` pool; `poolOptions` only affects a pool built from `url` | `postgres.ts:128-146,194-220,374-379` | Confirmed (order "`db.close()` first, then `pool.end()`" is advice the source neither states nor contradicts) | -| 33 | pgvector: install, import `/runtime`, `extensions: [pgvector]` | `test/e2e/framework/test/transaction-orm.test.ts:5,35` | Confirmed | -| 34 | Middleware object with `name` + hooks; `query.sql`; eight hook names | `framework-components/src/execution/runtime-middleware.ts:172,200,235-240`; `2-sql/5-runtime/src/middleware/sql-middleware.ts:49,75,80,84,89,119,124,128`; `relational-core/src/sql-execution-plan.ts:24` | Confirmed | -| 35 | `verifyMarker` default `onFirstUse`, warns and runs anyway; `npx prisma db update` | `sql-runtime.ts:214-216,898-929`; `cli/src/control-api/operations/db-verify.ts:348` | Confirmed | -| 36 | `postgres()` does not connect; connects on first use | `postgres.ts:222-262` | Confirmed | -| 45-51 | Options table (`url`, `pg`, `binding`, `poolOptions` 20000/30000, `extensions`, `middleware`, `verifyMarker`) | `postgres.ts:76-90,140-142` | Confirmed | -| 59-62 | Client members `.orm`, `.sql`, `.raw`, `.enums`, `.nativeEnums`, `.contract` | `postgres.ts:56-74` | Confirmed (the client also has `.context` and `.stack`, which the page no longer lists) | -| 61 | `db.nativeEnums.public['AalLevel'].values` is `['aal1','aal2']` | `test/integration/test/authoring/parity/native-enum/expected.contract.json:96-146` — `AalLevel` is in namespace `auth`, values `aal1, aal2, aal3`; `contract/src/enum-accessor.ts:16-17` for `.values` | **Corrected** → "`db.nativeEnums.auth['AalLevel'].values` is `['aal1', 'aal2', 'aal3']`" | -| 73, 86 | `process.env.X!` | `code-templates.ts:331` (`process.env['DATABASE_URL']!`) | Confirmed | -| 76 | JSON import line, `resolveJsonModule` / `module: "preserve"` / `moduleResolution: "bundler"` | `cli/src/commands/init/templates/tsconfig.ts:23-27`; `code-templates.ts:327` | Confirmed | -| 76 | "needs Node.js 22.18 or newer" | not in the source | **Q** | -| 96 | "It has five calls" | `sql-runtime.ts:100-113` — `Runtime` also has `close()`, so six members | **Corrected** → "It has these calls:" | -| 97-101 | `query`, `execute` (`{ affectedRows }`), `connection()` + `release()`, `prepare(declaration, callback)`, `telemetry()` | `sql-runtime.ts:100-134`; `relational-core/src/runtime-scope.ts:23-26`; `relational-core/src/ast/driver-types.ts:24-26` | Confirmed | -| 102 | `connect({ url })` / `connect({ binding: { kind: 'url', url } })` | `postgres.ts:289-324`; `binding.ts:16-31` | Confirmed | -| 103 | Errors carry `code`; compare `error.code` | `framework-components/src/shared/runtime-error.ts:32-44` | Confirmed | -| 104 | A query connects, so a later `connect()` throws `DRIVER.ALREADY_CONNECTED`; a second `connect()` too | `postgres.ts:249-251,298-303` | Confirmed | -| 127 | PostgreSQL `runtime()` is synchronous, callable before connecting | `postgres.ts:66,326-328,222-262` | Confirmed | -| 141-142 | `close()` safe at any time; then `connect()` rejects and `runtime()` throws `DRIVER.NOT_CONNECTED` (`Postgres client is closed`) | `postgres.ts:223-229,290-296,374-376`; `postgres/test/postgres-close.test.ts:134-144` | Confirmed | -| 156 | `db.orm.public.User.where((u) => u.kind.eq('admin')).all()` | example schema in `orm-client.mdx` (`kind user_type`, values `admin`/`user`) | Confirmed | -| 162-167 | `Symbol.asyncDispose`, block-scoped disposal | `postgres.ts:381-383`; `postgres/test/postgres-close.test.ts:146-155` | Confirmed | -| 167 | `"esnext.disposable"` lib name | not in the source (a TypeScript fact) | **Q** | -| 190-194 | MongoDB contract options; four binding ways; kinds `url` / `mongoClient` with `dbName`; `dbName` separate; ownership; no connection on create | `3-extensions/mongo/src/runtime/binding.ts:5-41,82-151`; `mongo.ts:120-145,220-235` | Confirmed | -| 192 | "Use `uri` only when you must pass `dbName` separately" | `binding.ts:101-138` — `{ url, dbName }` is also allowed, which the same bullet says | **Corrected** → "`uri` takes the same kind of string, and always needs `dbName`." | -| 203-208 | MongoDB options table; `mode` ignored by Prisma ORM, passed to middleware | `mongo.ts:43-57,139-144`; `2-mongo-family/7-runtime/src/mongo-runtime.ts:120` (only fills the middleware context) | Confirmed | -| 214-218 | `MongoClient` members `.orm`, `.query`, `.raw`, `.enums`, `.contract`; `@@map("users")` | `mongo.ts:28-41`; `orm-client.mdx` MongoDB schema | Confirmed | -| 251-252 | MongoDB `connect()` returns `Promise`; `DRIVER.ALREADY_CONNECTED` after a query or a second call; takes the same options | `mongo.ts:147-169,197-214` | Confirmed | -| 266-267 | MongoDB `runtime()` returns a promise; `MongoRuntime` is `query`, `execute`, `close` only | `mongo.ts:216-218`; `mongo-runtime.ts:76-101` | Confirmed | -| 281-283 | `runtime.query(built)`; returns `AsyncIterableResult`; `runtime.execute(built)` returns `{ affectedRows }` for "a write that returns no rows" | `mongo-runtime.ts:37-48,270-307` — `execute` only supports `updateOne`, `updateMany`, `deleteOne`, `deleteMany`; anything else throws `RUNTIME.MONGO_STATISTICS_UNSUPPORTED` | **Corrected** → "For an update or delete command, use `runtime.execute(built)` … Other commands throw an error whose `code` is `RUNTIME.MONGO_STATISTICS_UNSUPPORTED`." | -| 299 (old) | Example ran `execute(built)` on the pipeline built by `db.query.from('posts')` | same as above: an aggregate command has no statistics field | **Corrected** → line removed | -| 296 | `'posts'` is the collection key | `orm-client.mdx` MongoDB schema `@@map("posts")` | Confirmed | -| 308-309 | Always `db.close()`; `runtime.close()` leaves the client looking open; after `close()` everything rejects `DRIVER.NOT_CONNECTED`; a supplied `mongoClient` stays open | `mongo.ts:128,147-150,220-235`; `mongo-runtime.ts:309-311` | Confirmed from the code; no test covers `runtime.close()` on the client | -| 332 | MongoDB `await using` | `mongo.ts:237-239`; `mongo/test/mongo.test.ts:448` | Confirmed | -| 355 | `tx.orm`, `tx.sql`, `tx.query`, `tx.execute` → `{ affectedRows }`; same connection | `postgres.ts:341-372`; `sql-runtime.ts:139-141`; `driver-types.ts:24-26` | Confirmed | -| 356 | No options, no isolation level, no retries, no time limit; `idle_in_transaction_session_timeout` | `postgres.ts:67,341`; `sql-runtime.ts:960-1087` (no retry) | Confirmed, except the PostgreSQL setting name, which is not in the source → **Q** | -| 357 | `signal` per call limits a statement | `runtime-scope.ts:23-26`; `runtime-core.ts:107-147` | Confirmed | -| 358 | No nesting; `TransactionContext` from `@prisma/orm-postgres/family-runtime` with `query`/`execute`; derived `Tx` type | `sql-runtime.ts:139-141`; `2-sql/5-runtime/src/exports/index.ts:66-74`; `orm-postgres/package.json` `./family-runtime` | Confirmed (`TransactionContext` also has a read-only `invalidated` property, which the page does not mention) | -| 360-363 | Read-your-own-writes; `tx.enums` / `tx.nativeEnums` and `.values`; return value passes through; escaped result throws `RUNTIME.TRANSACTION_CLOSED` | `postgres.ts:365-368`; `sql-runtime.ts:960-1050`; `test/e2e/framework/test/transaction-orm.test.ts:101-115`, `transaction.test.ts:72-83` | Confirmed | -| 382-411 | Commit, rollback, read-your-own-writes examples | `transaction.test.ts:13-44`, `transaction-orm.test.ts:101-115` | Confirmed | -| 417-421 | `AbortSignal.timeout` passed as the second argument to `tx.execute` | `runtime-scope.ts:25` | Confirmed | -| 425-435 | Escaped-result warning and example | `sql-runtime.ts:966-996`; `transaction.test.ts:72-83` | Confirmed | -| 446 | `tx.orm.public.User.create({ email, displayName })` | example schema: `kind user_type` is required and has no default; `orm-client.mdx:976-980` passes `kind` | **Corrected** → added `kind: 'user'` | -| 460-462 | `withTransaction` and `Runtime` from `/family-runtime`; handle has `query`/`execute` only; commits on return, rolls back on throw | `exports/index.ts:62-74`; `sql-runtime.ts:960-1087`; `transaction-orm.test.ts:6` | Confirmed | -| 484-491 | Helper signature taking `sql: typeof db.sql` | `postgres.ts:57` (`sql` is `Db`) | Confirmed (no test uses this exact helper shape) | -| 500-501 | `connection()`, `transaction()`, `commit()`, `rollback()`, `release()`, `destroy()` | `sql-runtime.ts:101,116-134` | Confirmed (`destroy(reason?)` takes an optional advisory argument the page no longer shows) | -| 518-552 | Manual-control examples, including the read on the connection before the transaction | `sql-runtime.ts:116-134`; `relational-core/src/runtime-scope.ts:23-26` | Confirmed (no integration test covers these exact snippets) | -| 558 | No MongoDB `db.transaction`; planned, not shipped | `mongo.ts:28-41`; `projects/sqlite-mongo-transactions/spec.md:20,128` | Confirmed | -| 562 | "Prepared statements are PostgreSQL only" | `sqlite/src/runtime/sqlite.ts:79,285` has `prepare` | **Corrected** → "Prepared statements are not available on MongoDB." | -| 570-573 | Declaration maps names to type ids; `columns..codecId`; single-argument callback returning the built query; `where((f, fns) => …)`; `RUNTIME.PREPARE_UNUSED_PARAM` with `details.unused`, at prepare time | `prepared/types.ts:20-51`; `sql-builder/src/types/table-proxy.ts:115-116`, `types/raw-query.ts:29-37`, `expression.ts:38-41`; `sql-runtime.ts:560-574`; `runtime-error.ts:37-41` | Confirmed | -| 595-610 | Prepare examples, including the unused-parameter rejection | `sql-runtime.ts:567-573`; `test/integration/test/raw-prepared.integration.test.ts:133-171` | Confirmed | -| 619-620 | `db.prepare` injects `sql` as the first callback argument; `prepare()` connects, so `db.connect()` throws `DRIVER.ALREADY_CONNECTED` | `postgres.ts:330-339,249-251,298-303` | Confirmed | -| 653-664 | `PreparedExecution` from `.affectedCount()`, `execute(target, params)` → `{ affectedRows }`; raw parameterised example; `target` required; no `close()` | `prepared/types.ts:53-106`; `raw-prepared.integration.test.ts:133-145` | Confirmed | -| 702 | Which calls take `RuntimeExecuteOptions`, and a prepared statement takes it third | `runtime-scope.ts:23-26`; `prepared/types.ts:60-83` | Confirmed | -| 710-712 | `signal`; pre-aborted rejects before any row with `RUNTIME.ABORTED`; `cause` is the signal reason; `details.phase`; an abort inside a transaction rolls it back | `runtime-core.ts:107-147`; `race-against-abort.ts:12-19`; `runtime-error.ts:22-47`; `sql-runtime.ts:1036-1060` | Confirmed | -| 740-744 | Telemetry: PostgreSQL only, `null` before the first query, `{ lane, target, fingerprint, outcome, durationMs? }`, most recent query only, `lane` values, fingerprint from the SQL text, `outcome` values, `durationMs` always set | `sql-runtime.ts:883-885,932-944,423,545,746`; `runtime-spi.ts:44`; `sql-orm-client/src/query-plan-meta.ts:51`; `relational-core/src/plan.ts:38-45` | Confirmed | -| 767-771 | `AsyncIterableResult` from `all()`, `createAll()`, `query()`; `await` vs `for await`; buffering on PostgreSQL; `.toArray()` equals `await`; re-await safe; mixing or a second loop throws `RUNTIME.ITERATOR_CONSUMED`; MongoDB shares the rules | `framework-components/src/execution/async-iterable-result.ts:3-86`; `sql-orm-client/src/collection.ts:1440-1452`; `3-targets/7-drivers/postgres/src/postgres-driver.ts:296-333`; `postgres.ts:245-247` | Confirmed | -| all links | `#executing-a-plan`, `#binding-a-bare-value-with-param`, `#asynciterableresult`, `#example-schema`, `/orm/middleware/how-middleware-works`, `#options-and-isolation-level`, `#transactions-on-mongodb` | the target pages in `apps/docs/content/docs/orm/` | Confirmed | - -## Q list (unverifiable, page text left alone) - -1. **"It needs Node.js 22.18 or newer"** (line 76). Searches: `grep -rn "22\.18\|22\.12\|>=22" --include='*.json' --include='*.md'`, `grep -rn -i "node 22|node.js 22"`, `grep -rn '"engines"' -A3`. The only engine requirement in the tree is `"node": ">=24"` (root `package.json:100`, `cli/package.json:103`), which is the repository's own build requirement, not the version an application needs for JSON import attributes. -2. **`"esnext.disposable"` in `lib`** (line 167). Searches: `grep -rn "esnext.disposable\|asyncDispose" --include='*.json' --include='*.ts'`. The source only shows `Symbol.asyncDispose` implementations and tests; no tsconfig in the tree sets that lib. -3. **`idle_in_transaction_session_timeout`** (line 356). Searches: `grep -rni "idle_in_transaction\|statement_timeout"` over the whole tree. No hit. It is a PostgreSQL server setting, outside this source. - -## Extra item the page conventions asked for - -The last bullet of `page-conventions.md` is about the generated error-reference page being out of scope. My slice only links to it and never quotes its wording, so there is nothing to change. The one code I added, `RUNTIME.MONGO_STATISTICS_UNSUPPORTED`, is present on that page (`error-reference.mdx:734`). - -## One gap the operator should decide on - -`RuntimeExecuteOptions` still has a second field, `scope` (`'runtime' | 'connection' | 'transaction'`), in `framework-components/src/execution/runtime-middleware.ts:335-338`, and `runtime-core.ts:117,141` reads it. A reader-review round deleted both the `scope` table row and the remark about it, so the Options table under `### RuntimeExecuteOptions` now lists only `signal`. Nothing on the page is false, but the table no longer lists every field of the type it documents. I left it alone because the brief says not to make the slice longer; restoring the row costs one line. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/page-conventions.md b/docs/orm-docs-audit/c21-orm-reference/page-conventions.md deleted file mode 100644 index 651ca34beff..00000000000 --- a/docs/orm-docs-audit/c21-orm-reference/page-conventions.md +++ /dev/null @@ -1,12 +0,0 @@ -# Conventions for the orm/reference pages other than orm-client (in addition to conventions.md) - -- These are reference pages. Keep the per-method layout (Remarks, Options, Return type, Examples, PostgreSQL/MongoDB tabs where present). No tutorial prose; a "how do I" question gets one sentence and a link to the fundamentals page, unless the answer is a fact about the method. -- The finished orm-client page (`apps/docs/content/docs/orm/reference/orm-client.mdx` on main) is the model for tone and structure. Reuse its wording for shared ideas: "collection" (the query object you chain on), "the built query" instead of "plan", "an error whose `code` is `X`", "does not exist on MongoDB" instead of "throws TypeError", "TypeScript rejects it, nothing checks at run time". -- Do not renumber, rename, or reorder headings. If a heading must change because it contains a banned term, pin the old anchor with `[#old-anchor]`. -- Banned on these pages in addition to the list: terminal (write "the call that runs the query", or name it: `.build()`, `.all()`), capability-gated / "the adapter reports the X capability" (say "PostgreSQL only" or "available on PostgreSQL; not on SQLite"), codec (say "how the value is stored and read", or name the type id such as `pg/int4@1`), codec inferer, facet, factory, "wired to", execution context, envelope (say "the driver's own result object"), materialize, surface, "source-read from an internal design document" (say "planned, not shipped"), namespace-qualified (show `db.sql.public.user`, and say `public` is the PostgreSQL schema), "auto-parameterized" (say "sent as a query parameter, never pasted into the SQL"), field accessor (say "the `f` argument, one property per column"). -- `db.sql.public.user` is keyed by table name (the model name with a lowercase first letter unless `@@map`), which differs from `db.orm.public.User`. Say this once per page where `db.sql` first appears. -- `db.runtime()` is synchronous on PostgreSQL and returns a promise on MongoDB: `(await db.runtime()).query(built)`. State this where `runtime()` first appears on a page. -- Every raw or builder query ends with `.build()` and is then passed to `runtime.query(...)` for rows or `runtime.execute(...)` for a count. -- Type ids such as `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 source of truth is prisma/orm at 8.0.0-rc.10 (`cfccb09be2`), checked out at `wip/prisma-src/`. Where the page names an rc.9 behaviour that rc.10 changed, rc.10 wins. -- The error-reference page is generated from prisma/orm by `apps/docs/scripts/generate-error-reference.mjs` and is out of scope; its wording is fixed upstream. diff --git a/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part1.round1.md b/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part1.round1.md deleted file mode 100644 index 839a816edb0..00000000000 --- a/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part1.round1.md +++ /dev/null @@ -1,99 +0,0 @@ -# Reader review: `pipeline-builder.part1.mdx` - -I used Prisma ORM 7 for two years. I have never seen this API. - -## Sentences I could not restate after one reading - -**Line 95** — "`from(root)` takes a contract **root name**: the collection name from the contract's `roots` map (`'posts'`, `'users'`), the same names the ORM client uses (`db.orm.users`), not the model names." - -What stopped me: "contract" and "the contract's `roots` map". I have no idea what a contract is, where it lives, or what file I would open to see this map. It is bolded like a defined term, but it is never defined and never linked. Plainest wording I would have understood: "`from()` takes the collection name — the same name you use on `db.orm` (`'posts'`, `'users'`), not the model name `Post`." - -**Line 97** — "The builder moves through three states as you chain: a starting collection, a filtered collection after `match()`, and a pipeline chain after any other stage. Each state exposes the methods that are valid at that point." - -What stopped me: too many ideas, and all of them about the library's internal design. The only part I can act on is the parenthetical about `insertOne` / `insertMany`. Plainest wording: "Some methods are only available before you add stages — `insertOne` and `insertMany`, for example. If a method is missing from autocomplete, you have already chained past it." - -**Line 126** — the whole bullet. "`runtime.query(plan)` is the executor. It accepts any plan, including one built by the pipeline builder. On MongoDB `db.runtime()` returns a promise, so the call reads `(await db.runtime()).query(plan)`; hold the runtime in a variable if you run several plans. It returns an `AsyncIterableResult`: `await` it for an array of documents, or `for await` over it to take one at a time. There is no `db.execute` on the MongoDB client." - -What stopped me: five separate facts in one bullet. Split it into five bullets, or better, show the "hold the runtime in a variable" version as code — you never do, despite every example paying the double-`await` cost. - -**Line 127** — "**Read results are decoded, up to a point.** ... so `_id` comes back as a decoded hex string and other fields come back as their contract types." - -What stopped me: "up to a point" is vague until I read the next bullet, "contract types" again leans on the undefined word "contract", and "carries a result shape" is internal vocabulary. The actionable rule is buried: the list of eight stages that preserve decoding. - -**Line 150** — "`match()` filter values are **not encoded to the stored type**. The value you pass is compared as-is against the stored value." - -What stopped me: "encoded to the stored type" means nothing to me until the `_id` warning 40 lines later. On its own I cannot tell what could go wrong. - -**Line 151** — "`eq` and `expr(...)`-wrapped comparisons run in Prisma ORM's own tests; the remaining operators follow the matching MongoDB query operators." - -What stopped me: I could not tell what this means for me. Do `gt` and `in` work or not? "Follow the matching MongoDB query operators" sounds like a description of behavior, but the first clause implies a warning. Say plainly: "These operators are untested by Prisma but behave like the MongoDB operators of the same name." - -**Line 191** — "the pipeline builder's value resolver walks the value as a plain object, destructuring an `ObjectId` into its internal `buffer` bytes, so the filter that reaches MongoDB never carries a real `ObjectId`." - -What stopped me: this is a description of a bug in Prisma's internals. I only needed sentence one: `_id` equality in `match()` silently returns nothing; use `rawCommand()` or `db.orm`. - -**Line 191, last two sentences** — "Note that the hex-string form **compiles without any cast**. ... Only the driver-`ObjectId` form requires a cast to attempt." - -What stopped me: "cast" — a cast to what type? Neither sentence tells me what I would type, and I cannot work out why "requires a cast to attempt" matters if neither form works. - -**Line 616** — "`rawPath` builds a field path without checking it against the model, and a field path is compiled by prefixing a `$`, so `'$KEEP'` reaches the server as `$$KEEP`." - -What stopped me: I had to hold three steps in my head and there is no example to check them against. Plainest wording: "Write `f.rawPath('$KEEP')`. The builder adds one more `$`, so the server sees `$$KEEP`." - -**Line 626** — "**The expression fields inside these options are opaque.** Fields like `bucket.groupBy` ... are raw aggregation expressions (`MongoAggExpr`), not reachable through the typed `(f) => ...` callback the classic stages use. You build these values with expression helpers and pass the underlying node (`fn.year(ref).node`), or name a field directly with `MongoAggFieldRef.of('price')`." - -What stopped me: "opaque", "the underlying node", and `ref` — which appears from nowhere and is never defined. This is the hardest paragraph on the page and the only one with no example. - -**Line 644** — "they need Atlas Search's `mongot` process ... because those tests use an in-memory MongoDB with no Atlas Search." - -What stopped me: `mongot` is a word I have never seen, and both halves are about Prisma's test setup. The fact I need is the last sentence: it will fail at runtime, not at build time. - -## Words and phrases I had to guess - -- **"contract"** (lines 95, 127) — I guessed it is some generated description of my schema, maybe a file, maybe a type. I still do not know. -- **"roots map"** — guessed: a list of collection names somewhere in that contract. -- **"plan"** (line 116 onward) — guessed: the object `build()` returns, which you hand to the executor. The page never says this in one sentence; I inferred it from the variable name. -- **"discriminated model"** (line 23) — guessed: one collection storing two kinds of document, told apart by a field. `@@discriminator`, `@@base`, and `@@type("mongo/string@1")` in the schema are all new to me and unexplained. -- **"result shape"** / **"carries the shape forward"** — guessed: Prisma knows the column types, so it converts values back. -- **"decoded hex string"** vs **"raw driver `ObjectId`"** — guessed: one is a `string`, the other is the mongodb driver's class. -- **"inert"** (line 122) — guessed: nothing runs until you execute. -- **"the executor"** (line 126) — guessed: just a name for `runtime.query`. -- **"opaque"** (line 626) — guessed: not type-checked. -- **"the underlying node"** — guessed: some internal AST object; `.node` is a property I am told to reach for without being told what it is. - -## Where I asked "so what do I actually type?" - -1. **Where does `db` come from?** Every single code block starts with `db`, and no block imports it or creates it. The `fn` / `acc` / `expr` imports are shown, so the omission is glaring. One line and a link would fix it. -2. **`mongoQuery(...)`** (line 94) — named as an alternative entry point, with no import, no signature, no example, no link. -3. **`build()` vs `aggregate()`** (line 122) — I am told they are aliases. I am never told which one to prefer, so I do not know which is idiomatic. -4. **Holding the runtime in a variable** (line 126) — the page recommends it and then never shows it. I would have to invent `const rt = await db.runtime()` myself. -5. **`redact()`** — no Options table, no Return type table, and **no example**, unlike every other stage on the page. It is also the stage with the most confusing workaround. I would have to assemble `fn.cond(someCondition, f.rawPath('$KEEP'), f.rawPath('$PRUNE'))` from prose, and I am not confident that is right. -6. **`unwind()`** — no example, and the reason given is "the example schema has no array field." Add an array field to the schema. -7. **Every option-object stage** — `bucket`, `bucketAuto`, `geoNear`, `graphLookup`, `setWindowFields`, `densify`, `fill`, `facet` are named and then shown zero code. The one example is `unionWith('users')`, the simplest of the group. `facet` in particular ("an object that maps each output field name to an array of raw stages") is impossible for me to write from that sentence. -8. **`search()` / `searchMeta()` / `vectorSearch()`** — no example block, only a one-line table cell. The `vectorSearch` spec is listed as bare field names with no types. -9. **Comparing `ObjectId`s** — "Compare them with `String(...)`" appears twice. Show the line: `String(a._id) === String(b._id)`. -10. **`lookup()` chain** — `from(root).on((local, foreign) => ({ local, foreign })).as(name)` is written once as a signature, where `local` and `foreign` are both parameter names and object keys. I had to read the example twice to see that the keys are fixed. - -## Places explaining internals when I only wanted to know what to do - -- Line 15: "Where Prisma ORM's own tests do not run a method against a live MongoDB, the Remarks say so." I do not care about your test suite. Say "some methods are unverified" if you must, but the repeated test-coverage notes (lines 151, 625, 644) read like release notes for the Prisma team. -- Line 97: the three builder states. -- Line 126–128: "result shape" carried and dropped. -- Line 191: the value resolver and the `buffer` bytes. -- Line 451: "(Some older internal material shows a two-argument callback. That is incorrect.)" This is a note to whoever wrote the previous draft. Delete it; just show the correct signature. -- Line 616: the double-`$` compilation trick, explained as mechanism rather than as an instruction. -- Line 626: `MongoAggExpr`, `.node`, `MongoAggFieldRef`, `MongoMatchStage`, `MongoCountStage`, and "a known gap in the typed API." -- Line 644: `mongot`, in-memory test MongoDB. - -## Could I do what the page is for, after one reading? - -Partly. I could write the simple stages — `match`, `sort`, `limit`, `skip`, `sample`, `project`, `addFields`, `group`, `count`, `sortByCount`, `lookup`, `replaceRoot`. The examples for those are complete and I could copy them. - -What I would still not know: - -- **How to get `db`.** I cannot run a single example on the page. This is the biggest failure. -- **What a "contract" is**, so I cannot check what names `from()` will accept without guessing from `@@map`. -- **How to write `unwind`, `bucket`, `bucketAuto`, `geoNear`, `graphLookup`, `setWindowFields`, `densify`, `fill`, `facet`, `redact`, `search`, `searchMeta`, or `vectorSearch`.** That is 13 of the stages the page claims to document. The page says "This page documents every stage" (line 15); for these it names them and stops. -- **Whether my results will be decoded.** I understand the rule exists. I would not remember which eight stages preserve it, and the page gives me no way to check at runtime other than inspecting values. -- **Why `Article` and `Tutorial` are in the schema.** No example uses them, and the schema syntax around them is all unfamiliar. They cost me reading time for nothing. -- **Whether `gt`, `in`, `exists`, `type` actually work**, because line 151 hedges without saying what the hedge means for me. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part1.round2.md b/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part1.round2.md deleted file mode 100644 index 2d168541b44..00000000000 --- a/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part1.round2.md +++ /dev/null @@ -1,101 +0,0 @@ -# Reader review: `pipeline-builder.part1.mdx` - -Reading as a Prisma ORM 7 user, first time on Prisma 8, no source access. - -## Sentences I could not restate after one reading - -**1. Line 86, the contract paragraph.** Too many new ideas in one place. - -> "Your contract is the `contract.prisma` file that replaced `schema.prisma`. Running `npx prisma contract emit` compiles it into two generated files: `contract.json`, read at run time, and `contract.d.ts`, read by TypeScript." - -What stopped me: "contract" is a noun I have never seen, and the page uses it as if I know it. I know `schema.prisma`. I do not know what "your contract" is, why the file was renamed, or whether "contract" means the file, the generated JSON, or a concept. Plainest wording I would have understood: "Prisma 8 renames `schema.prisma` to `contract.prisma`. Run `npx prisma contract emit` to generate `contract.json` (used at run time) and `contract.d.ts` (used for types)." Then never use the bare word "contract" to mean something else. - -**2. Line 137, the decoding bullet.** - -> "Only `match()`, `sort()`, `limit()`, `skip()`, `sample()`, `project()`, `addFields()`, and `vectorSearch()` keep that." - -What stopped me: "keep that." I had to scroll back up to work out that "that" means the decoding described in the bullet above. The bullet before it is a bolded paragraph, so the link is not obvious. Plainer: "Decoding only survives these stages: ... After any other stage, including `lookup()` and `group()`, documents come back raw." - -**3. Line 159, the match() comparison rule.** - -> "The value you pass is compared exactly as you wrote it, with no conversion into the stored type." - -What stopped me: "conversion into the stored type" made me stop and build a mental model of encode/decode that the page has not yet given me. Plainer: "Prisma sends your value to MongoDB unchanged. It does not turn a string into an `ObjectId`, a date, or anything else." - -**4. Line 643, the redact workaround.** - -> "Write `f.rawPath('$KEEP')` instead. The builder adds one more `$`, so the server sees `$$KEEP`." - -What stopped me: `rawPath` is introduced here and nowhere else. I do not know what a "path" is in this API, why a method named "rawPath" is the way to express a constant, or whether the one-extra-`$` rule applies anywhere else. This reads as a bug I am being asked to work around, and I cannot tell whether it will keep working. - -**5. Line 691, the raw-expression rule for option-object stages.** - -> "Build a raw expression with the `fn.*` helpers and pass its `.node` property, or name a field with `MongoAggFieldRef.of('price')`." - -What stopped me: `.node`. Nothing on the page says what `.node` is or when I need it. It appears again in the `redact()` example on line 669 — `fn.eq(f.kind, fn.literal('tutorial')).node` — where the first argument has `.node` and the two `rawPath` arguments do not. After one reading I could not state the rule for when to add `.node`. - -**6. Line 692.** - -> "The stages you pass to `facet` and `unionWith` are classes too: `MongoMatchStage`, `MongoCountStage`, `MongoSortStage`, `MongoLimitStage`, and the rest." - -What stopped me: "too" (too, as opposed to what?) and "and the rest." There is no list of the rest and no link to one. I cannot write a `facet` with a `$group` in it from this page. - -## Words and phrases I had to guess - -- **"contract"** — guessed: the schema file, renamed. -- **"root"** (`from(root)`, "Unknown root") — guessed: the collection name. The page says this on line 95, but uses "root" in the signature before explaining it. -- **"plan"** — every example writes `const plan = ....build()`, while line 127 calls the result "a built query." Guessed these are the same thing and the variable name is arbitrary. -- **"decoded"** (line 136) — guessed: Prisma converts driver values into the types my schema declares. -- **`fn`, `acc`, `expr`** — guessed: three import namespaces for expressions, accumulators, and "wrap this as an aggregation expression." `acc` is explained at line 478. `fn` is never introduced; it just appears in the line 194 example. -- **`fn.literal(2023)`** — guessed: I must wrap plain constants so the builder does not read them as field paths. The page never says why I cannot pass `2023`. -- **`.node`** — guessed: the underlying expression object inside a helper's wrapper. Low confidence. -- **`default_`** (line 737, trailing underscore) — guessed: `default` is a reserved word in JavaScript, so the key was renamed. The page does not say, and I would have typed `default` and been confused. -- **`@@type("mongo/string@1")`, `@@discriminator(kind)`, `@@base(Post, "article")`** — guessed from the prose on line 21. The `@1` version suffix I could not guess at all. -- **"aggregation-time writes with `$out` / `$merge`"** (line 11) — guessed: stages that write results back to a collection. -- **`mongo(...)`** (line 86) — guessed: the Prisma 8 replacement for `new PrismaClient()`. - -## Places I asked "so what do I actually type?" - -1. **Line 98, the client-free builder.** "passing the `contract.json` you import." I do not know how to import a JSON file in my project, or what path it is at. There is no code block here, and every other entry point has one. - -2. **Line 117.** The first example uses `db` and `runtime` with no import lines and no `mongo(...)` call, just a link. The rest of the page has import lines. This is the one example where I most need them. - -3. **Lines 199–203, the `_id` warning.** This tells me a thing does not work and names two escape hatches, neither with code. `rawCommand()` links forward to a section I do not have. `db.orm.posts.where({ _id })` is a fragment, not runnable. "Fetch one post by id" is the single most common thing I do, and after reading this section I still cannot type it. - -4. **Lines 683–687.** Six stages given as bare parameter lists and nothing else: - > "`geoNear({ near, distanceField, spherical?, maxDistance?, minDistance?, query?, key?, distanceMultiplier?, includeLocs? })`." - - No types, no descriptions, no example. `near` also appears in the line 691 list as needing a raw expression, so I cannot even guess its shape. Same for `graphLookup`, `setWindowFields`, `densify`, `fill`, `bucketAuto`. - -5. **Line 458, the `unwind()` example.** "This example assumes a `tags String[]` field on `Post`, which the example schema above does not have." So the one example I would copy cannot be run against the schema the page gave me. - -6. **Line 734, `bucket()`.** `groupBy: MongoAggFieldRef.of('duration')` — but `duration` lives on `Tutorial`, not `Post`, and I am aggregating `'posts'`. Whether that is legal, and whether `MongoAggFieldRef.of` accepts any string or only fields of the root model, is not stated. - -7. **Line 669, `redact()`.** The example passes `.node` on one argument and not the others. I would not know which way to write my own. - -8. **Line 527, the ORM 7 migration diff.** The `+` line calls `acc.count()` with no import in the diff. Small, but this is the block a v7 user copies first. - -## Places that explain the machinery when I only wanted the instruction - -- **Line 131:** "`build()` and `aggregate()` are two names for the same method. This page uses `build()`." I only need to be told which one to type. -- **Line 137:** the whole decoding bullet is an explanation of Prisma's internal encode/decode behavior. What I need is the instruction: "after `lookup()` or `group()`, compare ids with `String(a._id) === String(b._id)`." That instruction is there, but buried under the mechanism. -- **Line 643:** "The builder adds one more `$`, so the server sees `$$KEEP`." I do not need to know what the builder adds. I need "write `f.rawPath('$KEEP')`." -- **Line 97:** "If a method is missing from autocomplete, you have already chained past it." This explains why my editor behaves a certain way rather than telling me the rule, which is: call `insertOne`/`insertMany` directly on `from()`, before any stage. -- **Line 13:** "Where a method has not been verified against a running MongoDB, the Remarks say so." This tells me about the state of the documentation team's testing. As a reader I read it as "some of this page may be wrong," which makes me trust the parts with no such note less, not more. - -## Could I do what the page is for, after one reading? - -Partly. - -**Yes:** filter, sort, page, sample, project, add computed fields, join with `lookup()`, group and count, promote a sub-document. The stage sections from `match()` through `sortByCount()` each have a runnable example with imports, and I could copy them and change field names. - -**No, I would still not know:** - -- **How to get `db` and `runtime` in the first place.** The page assumes them from line 117 onward and hands the setup to two links. -- **How to fetch one document by its id.** The page tells me it is impossible through this builder and does not show me the working alternative in code. -- **What `fn` is, what it contains, or when a value needs `fn.literal(...)` or `.node`.** `fn` is used in six examples and defined in none of them. The page says the definition is in "Expression helpers," which is on the other half. -- **How to use any of the option-object stages except `unionWith`, `facet`, and `bucket`.** The other six are a list of parameter names. -- **Which `Mongo*Stage` classes exist and what their constructors take.** "and the rest" is not enough to write a `facet`. -- **Whether the Atlas stages work.** Line 753: "None of the three has been verified against a running Atlas deployment." - -One structural note: the page's opening says it "documents every stage, accumulator, expression helper, and write method." Reading only this half, I hit forward links to `#expression-helpers` and `#rawcommand` four times before the page ends, and two of those (the `_id` workaround and the `fn` namespace) are things I need to finish a basic task, not advanced extras. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part1.round3.md b/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part1.round3.md deleted file mode 100644 index 845e1b27d4c..00000000000 --- a/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part1.round3.md +++ /dev/null @@ -1,100 +0,0 @@ -# Reader review: `pipeline-builder.part1.mdx` - -I have used Prisma 7 for two years. I have never used MongoDB aggregation pipelines seriously. Here is what stopped me. - -## 1. Sentences I could not restate after one reading - -**Line 19** — "The `posts` collection holds two kinds of document, `Article` and `Tutorial`, told apart by the value of the `kind` field. No example on this page uses them." - -This is wrong, or I cannot tell what "them" means. The very next stage example is `.match((f) => f.kind.eq('tutorial'))`, and line 726 says "`duration` is a field on `Tutorial`". So examples do use them. I stopped and re-read three times trying to work out whether "them" meant the models, the `kind` field, or something else. Plainer: say which, or delete the sentence. - -**Line 19** — "A type id such as `mongo/string@1` is the stored type plus a version, always `@1` today." - -I do not know what a "type id" is, where it comes from, or why I am being told this on a pipeline-builder page. It appears once in the schema, on `@@type(...)` inside an enum, and is never mentioned again. I cannot restate it because I do not know what it is for. - -**Line 84** — "`db` is the client you create with `mongo(...)`. See [Transactions and runtime] for how to create it. Prisma ORM 8 renames `schema.prisma` to `contract.prisma`. Run `npx prisma contract emit` to generate `contract.json`, used at run time, and `contract.d.ts`, used for types." - -Four unrelated facts in one paragraph: what `db` is, a file rename, a codegen command, and two output files. I had to read it twice to work out that `contract emit` is a thing I must run before any code on this page compiles. That is important and it is buried in the middle of a paragraph about something else. - -**Line 135** — "**After `lookup()` or `group()`, compare ids with `String(a._id) === String(b._id)`.** Both a looked-up sub-document's `_id` and the top-level `_id` are the driver's own `ObjectId` values there, not hex strings." - -`a` and `b` are never defined. I do not know what two things I am comparing. I guessed: a joined sub-document and a parent document. The plain version would be: "After `lookup()` or `group()`, `_id` values come back as the MongoDB driver's `ObjectId` objects, not strings. `==` between two `ObjectId`s is false even when they are the same id, so wrap both sides in `String(...)` before comparing." - -**Line 136** — "Decoding only survives these stages: `match()`, `sort()`, `limit()`, `skip()`, `sample()`, `project()`, `addFields()`, and `vectorSearch()`. After any other stage, including `lookup()` and `group()`, documents come back raw." - -"Decoding only survives these stages" — survives means what? I eventually guessed: if my pipeline contains *only* stages from this list, results are converted; add one other stage anywhere and the whole result is raw. But the sentence could equally mean it survives *past* each of those stages and is lost only at the point of a different stage. Those are different rules and I would write different code for each. Also: "come back raw" — raw meaning what, exactly? `ObjectId` objects and driver `Date`s? Say the two or three things that actually change. - -**Line 158** — "It is why an `_id` filter never matches: your hex string is compared as a string against a stored `ObjectId`." - -Understandable, but then line 199 says the driver's own `ObjectId` does not match either. So the explanation given here does not explain the actual behaviour. After reading both I still do not know why the `ObjectId` form fails. - -**Line 643** — "For `$$KEEP` write `f.rawPath('$KEEP')`." - -One dollar sign in, two dollar signs out. Nothing says why. I assumed a typo, then assumed `rawPath` prepends a `$`, then gave up. If `rawPath` adds a `$`, say so in the sentence. - -**Line 644** — "Every `fn.*` call returns a wrapper. `.node` is the raw expression inside it. `match()` takes the wrapper through `expr(...)`. `fn.cond()`, `redact()`, and the option-object stages take `.node`." - -Four sentences, four new concepts, no names I recognise ("wrapper", "node", "raw expression"). I cannot restate this. What I actually need is a rule I can apply: which argument positions need `.node`. Even the example at line 673 has to re-explain it in a code comment, which tells me the prose did not land. - -**Line 692** — "Build a raw expression with the `fn.*` helpers and pass its `.node` property, or name a field with `MongoAggFieldRef.of('price')`." - -`price` is not a field in the example schema. I looked for it. Use a real field. - -## 2. Words and phrases I had to guess - -- **"compiles to"** (line 11) — guessed: is translated into, at build time or at `build()` time. I do not know which, and the page later says nothing runs until you execute, so "compiles" may mean neither. -- **"plan"** (line 121, 127) — the page calls it "a built query" and then names the variable `plan`. I guessed these are the same thing. Two names for one object on the same line is avoidable. -- **"built query"** vs **"pipeline"** vs **"chain"** vs **"builder"** — four words circulating. I guessed they are stages of the same object. -- **"root"** (lines 93, 101, 357, 454) — the parameter is called `root` but every example passes a collection name positionally and never types the word `root`. The error message says `Valid roots: ...`. I guessed root = collection. Line 454's example `db.query.from(root).unwind('items')` literally tells me to type `root`, which is not valid code. -- **"There is no `find` and no `distinct`"** (line 11) — guessed this refers to the MongoDB driver's methods. If you have not used the driver, this sentence names nothing. -- **`@@discriminator(kind)`, `@@base(Post, "article")`** (lines 59, 67, 74) — never explained anywhere on the page. Guessed: `Article` and `Tutorial` are subsets of `posts` selected by `kind`. That is Prisma 8 vocabulary I have never met. -- **"Leaf fields"** (line 159) — guessed: scalar fields, as opposed to embedded documents. Why not say "scalar fields", which the same page uses at line 158? -- **"system variables"** (line 642) — guessed: MongoDB's `$$`-prefixed names. Fine if you know MongoDB, opaque if you do not. -- **"expression helper" / "accumulator" / "raw expression" / "document-shaped expression" / "key expression"** — five kinds of expression, no definitions on this page. I guessed from the examples. -- **"decoded"** (line 136) — guessed: converted from driver types to my types. -- **"granularity"** (line 683) — passed through with no explanation at all. - -## 3. Places I asked "so what do I actually type?" and got no answer - -- **`graphLookup`, `setWindowFields`, `densify`, `fill`, `geoNear`, `bucketAuto`** (lines 683–688). Each gets one sentence and an options list. `output` in `setWindowFields` and `fill` is an object of *what*? `range` in `densify` is what shape? There is not one example among the six. I could not write any of these calls. -- **`startWith` / `groupBy` / `partitionBy` / `near` are "a raw expression"** — said four times before line 692 tells me how to build one. And line 692's two options (`fn.*` + `.node`, or `MongoAggFieldRef.of`) are not the same thing, so I do not know which applies where. -- **The facet example imports `MongoFieldFilter`** (line 707) but `MongoFieldFilter` is not in the list of importable classes at line 693, and nothing documents `MongoFieldFilter.eq('kind', 'tutorial')`. I would have copied the example and had no idea how to change it — e.g. to a `gt` filter. -- **Constructor signatures.** Line 693 lists 28 classes and says "Import them". `new MongoCountStage('count')`, `new MongoSortStage({createdAt: -1})`, `new MongoLimitStage(2)` — I can copy those three. For the other 25 I have nothing. -- **`for await` over it** (line 133). No example. Over what expression? `for await (const doc of runtime.query(plan))`? I guessed, and `await runtime.query(plan)` returning an array makes me unsure the same call can be iterated. -- **Dropping a field with `project()`.** Line 398 says "keeping, dropping, or computing each field" but no example shows `0`, and I would not have known dropping used `0` if the table at line 405 had not listed the type. -- **`unwind('tags')`** takes a plain string while every other stage takes `(f) => f.something`. Nothing says why, and the one example (line 460) explicitly does not work against the example schema. So there is no runnable example of `unwind` on this page. -- **`contract.json` import** (line 116): `import contractJson from './contract.json' with { type: 'json' }`. This needs specific Node and TypeScript settings. Nothing says which. -- **`mongo({ contractJson, url, dbName })`** (line 118) — `dbName: 'app'`. Where does that come from? Is it required? The page sends me to another page for `mongo()` but still shows a half-explained call I must copy. -- **`search(config, index?)`** — what happens if I omit `index`? -- **`acc.*`** is used at line 480 and 501 and defined nowhere on this page. It says "See part 2" nowhere either. - -## 4. Places that explain how the tool works inside, when I only wanted to know what to do - -- **Line 644**, the wrapper/`.node` paragraph. This is the internal object model. What I need is a rule: "pass `.node` when the argument is X". The code comment at line 673 does a better job than the reference text. -- **Line 158** — "Prisma ORM sends your value to MongoDB unchanged. It does not turn a string into an `ObjectId`, a date, or anything else." This is implementation behaviour. The thing I need is the consequence, and it is in the warning at line 199 anyway. -- **Line 136** — the whole decoding paragraph is framed as "what Prisma does to values" when what I need is "results are normal after these stages; after anything else you get `ObjectId`s and driver dates." -- **Line 131** — "Call `build()`. `aggregate()` is the same method under another name." Knowing there is a second name for the method I should not use is not useful. Say "Call `build()`" and stop. -- **Line 691** — "`geoNear`, `densify`, and `fill` have not been verified against a running MongoDB." This is your internal test status. As a reader I do not know what to do with it — use them or not? -- **Line 199** — "The hex-string form passes TypeScript, so it compiles cleanly and then returns zero documents." The compiler detail is background; the fact I need is "returns zero documents, always", which is already in the first sentence. - -## 5. Contradictions I noticed and could not resolve - -- **Line 136** lists `vectorSearch()` as a stage that preserves decoding. **Line 745** says `vectorSearch()` is Atlas-only. Reading in order, I met the claim before I knew what the stage was. -- **Line 158** says Prisma sends values unchanged, which is why `_id` filters fail. **Line 203** shows `db.orm.posts.where({ _id: postId })` working with a plain hex string. So the ORM client *does* convert and the pipeline builder does not. Nothing says that, and after one reading I believed the two pages behaved the same way. -- **Line 11** promises "This page documents every stage, accumulator, expression helper, and write method." The page ends at line 760 with no accumulators, no expression helpers, no write methods. I would have scrolled back looking for what I missed. - -## Could I do what the page is for, after one reading? - -Partly. I could write and run the simple stages: `match`, `sort`, `limit`, `skip`, `sample`, `project`, `addFields`, `count`, `sortByCount`, `group`, `lookup`, `replaceRoot`. Those all have complete, copyable examples against the real schema, and that is the bulk of what I would actually use. - -I could not do these things: - -1. **Set up the project.** `contract emit`, `mongo(...)` options, and the JSON import are shown but not explained. I would guess and fail at least once. -2. **Write any of the six option-object stages** — `bucketAuto`, `geoNear`, `graphLookup`, `setWindowFields`, `densify`, `fill`. No examples, no shapes for `output` or `range`. -3. **Know when to write `.node`.** The rule at line 644 did not survive one reading. I would trial-and-error against the type checker. -4. **Use `facet` or `unionWith` with anything but the exact stages shown**, because `MongoFieldFilter` is used and never documented, and 25 of the 28 stage classes have no constructor shown. -5. **Use `unwind`**, because the only example needs a field the schema does not have. -6. **Predict when results come back decoded.** I do not know whether the rule is "the whole pipeline" or "up to the first non-listed stage". -7. **Explain `@@base`, `@@discriminator`, or `@@type("mongo/string@1")`** to a colleague, even though they are in the schema every example runs against. - -The biggest single gap is the setup paragraph at line 84. Every code block on the page depends on `db`, `runtime`, and `contract.json`, and all three are handled in four sentences that also contain a file rename. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part1.round4.md b/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part1.round4.md deleted file mode 100644 index 05dba115389..00000000000 --- a/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part1.round4.md +++ /dev/null @@ -1,133 +0,0 @@ -I read the file once, top to bottom, as a Prisma ORM 7 user seeing this API for the first time. Findings below. - -## Sentences I could not restate after one reading - -**1. Line 11, the "use the pipeline builder when..." sentence.** -> "Use the ORM client (`db.orm`) for everyday reads and writes across models and relations, and the pipeline builder when you need a stage the ORM client doesn't expose: grouping, `$lookup` joins with post-processing, computed projections, multi-stage transformations, or stages that write the results into a collection (`$out` and `$merge`)." - -Too many ideas in one sentence: two tools, five use cases, three new terms. "Post-processing" of what, by what? "Computed projections" means nothing to me yet. Plainer: split it into two sentences and drop the list to three concrete items. - -**2. Line 19.** -> "`Article` and `Tutorial` are variants of `Post`. They are stored in the same `posts` collection and told apart by the value of the `kind` field." - -"Variant" is a new word the page uses as if I know it. The schema then shows `@@discriminator(kind)` and `@@base(Post, "article")`, neither of which is explained anywhere on this page. I can guess, but I had to. - -**3. Line 95.** -> "Passing an unknown collection name throws right away, with an error whose `code` is `ORM.MODEL_UNKNOWN` and whose message is `Unknown root: "". Valid roots: ...`." - -"Root" appears here for the first and only time and is never defined. The page has spent a paragraph telling me the argument is a *collection name*, then shows an error that calls it a *root*. I could not tell whether "root" is a third concept. - -**4. Line 136.** -> "**After `lookup()` or `group()`, compare ids with `String(a._id) === String(b._id)`**, where `a` is a looked-up sub-document and `b` is the parent document it came from." - -This sits in "Building and executing a pipeline", before `lookup()` or `group()` have been introduced. I do not yet know what a "looked-up sub-document" is. The letters `a` and `b` are not in any example on the page, so I had to invent the situation the rule applies to. - -**5. Line 137.** -> "**Read results are decoded.** If every stage in your pipeline is one of `match()`, `sort()`, `limit()`, `skip()`, `sample()`, `project()`, `addFields()`, and `vectorSearch()`, results are decoded: `_id` is a hex string and dates are your contract's date type. If any stage is not in that list, including `lookup()` and `group()`, every document comes back raw." - -I understood the rule, but not "your contract's date type" — my contract does not declare a date type anywhere I can see, and `createdAt DateTime` in the schema does not tell me what I get back. This is also the most consequential rule on the page and it is a bullet in the middle of a list. - -**6. Line 159.** -> "Neither a hex string nor a driver `ObjectId` matches in `match()`. The builder sends the value as a plain object, so an `ObjectId` arrives as its bytes." - -Two problems. "The driver" is never introduced — I inferred the MongoDB Node driver. And the second sentence explains the tool's insides to justify a rule; I only need the rule. - -**7. Line 161.** -> "To compare computed values rather than a field against a constant, wrap the comparison in `expr(...)`... `fn` holds the helpers that build expressions, and `expr()` wraps one so `match()` accepts it. The `fn.*` helpers take expressions, not plain values, so wrap every constant in `fn.literal()`." - -Three new things at once (`fn`, `expr`, `fn.literal`) inside a remark about `match()`. I could copy the example but I could not have written a second one. I still cannot state the rule for when I need `expr()` versus when I do not. - -**8. Lines 642–646, the "Call site / What you pass" table.** - -This table is dropped inside the `redact()` section, under `redact()`'s remarks, with no heading, no introductory sentence, and no subject. It is plainly about `fn.*` values in general, not about `redact()`. I did not know what it was describing until I had read all three rows. Worse, it is the first mention of `.node`, and `.node` is never defined on this page — the table tells me when to type it but not what it is. - -**9. Line 683.** -> "`granularity` picks the bucket size from one of MongoDB's preferred-number series." - -"Preferred-number series" is a term I do not know, and the page gives no values. I have no idea what to type. - -**10. Line 686.** -> "`setWindowFields({ partitionBy?, sortBy?, output })` computes a value for each document from the documents around it, such as a running total. ... `output` is an object mapping each new field name to `{ operator, window? }`, where `operator` is the accumulator or expression to compute." - -I understand the purpose sentence. I cannot picture the `output` object. `{ operator }` — is `operator` a string, an `acc.*` value, an `fn.*` value, a `.node`? No example. - -**11. Line 692.** -> "Each stage class takes the same arguments as the MongoDB stage it builds." - -Twenty-eight class names are listed, then this sentence tells me to go work out the arguments myself from MongoDB's docs. Only three are then documented. This is a list, not an explanation. - -**12. Line 750.** -> "These stages need MongoDB Atlas. The query builds, and MongoDB rejects it when it runs." - -I had to read twice. Plainer: "Nothing stops you from building these against a non-Atlas database. The error only appears when you run the query." - -## Words and phrases I had to guess - -- **"variants"** (line 19) — guessed: subtypes of one model stored in one collection. -- **"root"** (line 95) — guessed: the same thing as the collection you pass to `from()`. -- **"the driver"** (lines 136, 137, 159) — guessed: the official MongoDB Node.js driver, underneath Prisma. -- **"decoded" / "raw"** (line 137) — guessed: converted to friendly JS types versus handed back as the driver produced them. -- **"your contract's date type"** (line 137) — guessed: whatever `DateTime` maps to, probably `Date`. -- **"plan"** (line 123 onwards) — guessed: just the variable name for what `build()` returns. The page says as much at line 129, but the name appears three sections earlier. -- **"raw expression"** (lines 682, 683, 684, 685, 686, 688) — used six times before line 691 tells me how to make one. Guessed: a MongoDB expression object, not a typed builder value. -- **`.node`** (lines 646, 670, 691) — guessed: the plain MongoDB object inside an `fn.*` wrapper. -- **"document-shaped expression"** (line 539) — guessed: an expression that evaluates to an object. -- **"option-object stages"** (line 646) — used in the table before the section of that name exists, at line 676. -- **`@@type("mongo/string@1")`** (line 27) — guessed: how the enum is stored. No idea what `@1` means. -- **"post-processing"** (line 11) — guessed: more stages after the join. - -## Places I asked "so what do I actually type?" - -1. **Line 97, using the builder without a client.** - > "You can build the same pipelines without a client. `import { mongoQuery } from '@prisma/orm-mongo/query-builder'`, then `mongoQuery({ contractJson }).from('posts')`." - - It tells me how to *build* a pipeline with no client, and never tells me how to *run* one. If there is no client there is no `runtime.query`. So what do I do with the result? No example, no code block, one line in a bullet. - -2. **`build()` itself.** Line 9 says I "finish the chain with the call that builds the query". Every table says "then finish with `build()`". But `build()` has no entry of its own, no options table, no return-type table, unlike every other method on the page. What does it return? Can I inspect it? Can I log the pipeline it produced? - -3. **Line 686, `setWindowFields`.** No example anywhere. I cannot type this. - -4. **Lines 687–688, `densify` and `fill`.** `{ method }` — which methods? No values given. `bounds` gets its three values spelled out; `method` gets nothing. - -5. **Line 683, `granularity`.** No values. - -6. **`search()` and `vectorSearch()`.** Lines 751–753 describe the arguments in prose; there is no code example for any of the three Atlas stages. The `search()` call only appears squeezed into a table cell at line 759. - -7. **Line 691 contradicts the `bucket()` example.** The bullet says "Build a raw expression with the `fn.*` helpers and pass its `.node` property, or name a field with `MongoAggFieldRef.of('duration')`." The example at line 735 passes `MongoAggFieldRef.of('duration')` with no `.node`. So does `MongoAggFieldRef` need `.node` or not? I would have guessed wrong. - -8. **Line 459, `unwind()`.** - > "This example needs a `tags String[]` field on `Post`, which the example schema does not have." - - The one stage where I most wanted to copy a working example is the one I cannot run. Add `tags` to the schema. - -9. **Example output comments assume data I never saw.** Line 629 says "two buckets, each `{ _id: , count: 1 }`" and line 722 says "facets.totalCount is `[{ count: 2 }]`". The page shows a schema but never shows the documents. I cannot check my results against these. - -10. **Three different import paths** — `@prisma/orm-mongo/runtime`, `@prisma/orm-mongo/query-builder`, `@prisma/orm-mongo/query-ast/execution` — with no statement of which things come from which. I had to scan every code block to work out that `fn`, `acc`, `expr` are in one and the `Mongo*Stage` classes in another. - -11. **`count()` collides with `acc.count()`.** The page documents a `count()` stage at line 574 and an `acc.count()` at line 505 without ever saying they are different things. I would have typed the wrong one. - -12. **`lookup()` with no match.** Line 360 says the joined documents arrive in an array. It does not say what happens when nothing matches — empty array, or missing field? - -## Places that explain the insides when I only wanted the instruction - -- **Line 159.** "The builder sends the value as a plain object, so an `ObjectId` arrives as its bytes." I do not need to know how the value is serialized. The rule I need is in the warning at line 200. -- **Line 639.** "It prefixes `$` to the string you give it, so `f.rawPath('$KEEP')` reaches MongoDB as `$$KEEP`." Here the internal detail is unavoidable, because the string I type does not look like the string MongoDB sees. But the surrounding sentences — "There is no expression helper for a `$$` variable, and `fn.literal(...)` cannot stand in. The server rejects both `fn.literal('KEEP')` and `fn.literal('$$KEEP')`" — tell me about two things that do not work before telling me the one that does. -- **Line 692.** The full list of 28 internal class names is the implementation surfacing into a reference page. I need the ones I can use in `facet` and `unionWith`, with their arguments. -- **Line 137.** The decode rule is stated as a fact about the library's behaviour with no instruction attached. What do I do about it? Change my comparison code? Convert manually? The page tells me the mechanism and leaves the action to me. - -## Could I do what the page is for, after one reading? - -Partly. I could write and run a pipeline that filters, sorts, limits, projects, groups, and joins — those five have clear examples I could adapt, and the `groupBy` diff at line 526 is the single most useful thing on the page for someone coming from version 7. - -What I would still not know: - -- What `.node` is, and therefore when to type it. The table at line 642 gives me three rules for a thing the page never names. -- How to run a pipeline built with `mongoQuery` and no client. -- How to write `setWindowFields`, `densify`, `fill`, `graphLookup`, `geoNear`, `search`, `searchMeta`, or `vectorSearch`. Eight stages are described only in prose bullets with no code. -- What arguments any of the 28 stage classes take, beyond the three used in one example. -- Whether `MongoAggFieldRef.of(...)` needs `.node`. -- What "your contract's date type" is, so I do not know what my dates will actually be. -- What to do about the decode rule beyond being warned it exists. -- What `@@discriminator`, `@@base`, and `@@type("mongo/string@1")` mean in the schema I am told all examples run against. - -The biggest single defect is the table at lines 642–646. It is general guidance about `fn.*` values, filed under `redact()`, introducing `.node` with no definition and referring forward to a section that does not exist yet. I would have missed it entirely if I had jumped to the stage I needed, which is how people read reference pages. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part2.round1.md b/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part2.round1.md deleted file mode 100644 index bcc884968f1..00000000000 --- a/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part2.round1.md +++ /dev/null @@ -1,124 +0,0 @@ -# Reader report: `pipeline-builder.part2.mdx` - -I have used Prisma ORM 7 for two years. I have never seen this builder. Here is what stopped me. - ---- - -## 1. Sentences I could not restate after one reading - -**Line 33** — "The examples below are the common accumulators, all executed against a live database. The `N`-variants (`firstN` shown), `top`/`bottom`, and the standard-deviation accumulators share the same call shapes; `firstN` is exercised here as a representative of the family." - -Stopped by: "executed against a live database" and "exercised here as a representative of the family". This is a note about how you tested the docs, not about how I use the thing. I do not care where you ran them. "Exercised" is test-suite vocabulary. Plainest wording: "`firstN` is shown below. `lastN`, `maxN`, and `minN` are called the same way." And drop the live-database clause entirely. - -**Line 33, second half** — "In every signature, `sortBy` is a plain `{ field: 1 | -1 }` object and `n` is an expression, so pass `fn.literal(2)` rather than `2`." - -Stopped by: "`n` is an expression". At this point in the page "expression" has not been defined — expression helpers start 80 lines later. I only learned from the trailing example what I actually type. Plainest wording: "`n` must be wrapped: write `n: fn.literal(2)`, not `n: 2`." Also `1 | -1` is never spelled out; I guessed ascending/descending. - -**Line 120** — "The `fn.*` namespace covers 73 of MongoDB's aggregation expression operators. It is not all of them: there is no `fn.and`, `fn.or`, `fn.not`, `fn.switch`, `fn.ifNull`, `fn.map`, `fn.reduce`, or `fn.filter`, and arithmetic stops at `add`, `subtract`, `multiply`, and `divide`." - -Too many ideas in one paragraph, and it tells me what is missing without ever telling me what is there. The table below it is headed "Helpers (examples)" — so eight helpers are named out of 73 and the other 65 are unlisted. I cannot use this page to find out whether `fn.substr` or `fn.dateAdd` exists. This is the biggest gap on the page. - -**Line 127** — "Return a boolean expression; use `.node` where a raw expression is required." - -Stopped by `.node`. It is never defined anywhere on the page. It is used at lines 136, 180, 407, 428 and I still do not know what it is or when I need it, beyond "`fn.cond` wants it and `match()` does not". Plainest wording: say what `.node` gives you and give me a one-line rule for when to add it. - -**Line 136-138** — the `fn.cond` / `expr()` remarks. - -The rule is buried under an internal crash message: "throws `TypeError: visitor.expr is not a function`". That error names an internal thing called a "visitor" that I have no way to connect to anything. Plainest wording: "Inside `fn.cond()`, pass `fn.eq(a, b).node`. Inside `match()`, pass `expr(fn.eq(a, b))`." - -**Line 138** — "`fn.toObjectId(fn.literal(id))` throws (`Cannot read properties of undefined (reading 'codecId')`), because `fn.literal()` has no field type information." - -"codecId" is your internals leaking. So is "field type information". And the fix — "Give `fn.toObjectId()` an expression taken from the `f` argument instead" — has no example, so I still do not know what to type when the ObjectId I want to convert is a string variable from my own code and not a field on the document. That is the normal case and the page does not cover it. - -**Line 207** — "It keeps the row type you already had; `pipe(stage)` lets you declare a new one. Whatever the row type says, the rows are no longer decoded, because a stage the builder does not recognise drops the result shape." - -Three ideas, one sentence, and the key word "decoded" is never defined. It appears again at lines 259, 322, 466, 468. I guessed it means "converted from raw BSON into the model's types", but the page never says so. Plainest wording: "After `pipe()`, the rows come back exactly as MongoDB returns them — dates and ids are raw driver values, whatever the TypeScript type claims." - -**Line 247** — "A bare (non-array) operation throws as soon as you call the write method: `Error: Unreachable: items.length > 0 but first is undefined`. This is an internal consistency guard, not a friendly validation message..." - -You are apologising for a bug in the middle of a reference. I do not need the crash text or the words "internal consistency guard". The rule is one sentence: "Always return an array." Then: "(Some older internal material shows the bare form. That is incorrect.)" — I cannot resolve "older internal material". I have never seen any internal material. Delete it or name the thing. - -**Line 249** — "You cannot mix operator-form ops and pipeline-form (`f.stage.*`) ops in one updater." - -Neither form is defined until line 400, 150 lines later. On first reading this sentence is meaningless. - -**Line 260** — "The builder does not check it against the model, and its values are stored as you pass them, the same way [`match()`](#match) filter values are." - -"stored as you pass them" — as opposed to what? The contrast is never stated, so the warning has no teeth. What actually goes wrong if I pass a string where the contract says Date? The page does not say. - -**Line 322** — "Both disappear from the type after a stage MongoDB's `findAndModify` command cannot carry, such as `skip()`; forced past the type system they throw an error whose `code` is `ORM.OPERATION_UNSUPPORTED`." - -The worst sentence on the page. "Disappear from the type" plus "a stage `findAndModify` cannot carry" plus "forced past the type system" — three unfamiliar ideas stacked. Plainest wording: "You cannot call `findOneAndUpdate()` or `findOneAndDelete()` after `skip()`. TypeScript will not offer them there." That is all I need. - -**Line 386** — "If you force it past the type system, the unknown key is silently ignored and the default (`'after'`) applies." - -"Force it past the type system" appears three times on the page (322, 386, and implied at 138) and is never explained. I guessed it means casting with `as any`. Say that, or drop the whole clause — TypeScript already rejects it, so this is an essay about what happens if I defeat my own compiler. - -**Line 466** — "`db.query.rawCommand(command)` packages a command node (for example `new RawAggregateCommand(collection, pipeline)`) into a plan, without translating it." - -"command node", "without translating it", and (line 462) "bypassing the typed AST". AST is internal architecture. I only want to know: raw pipeline in, untyped rows out. - ---- - -## 2. Words and phrases I had to guess at - -| Quote | What I guessed | -|---|---| -| `.node` | Some unwrapped inner value. Still not sure. | -| "decoded" / "undecoded" | Converted from raw driver values to the model's types. Never stated. | -| "result shape" | The TypeScript type of a returned row. | -| "the typed AST" | Your internal query representation. | -| "expression" | Anything built from `fn.*` or taken off `f`. Never defined on this page. | -| "plan" vs "Built query" (line 227) | The same thing under two names. The return-type table says "Built query", the prose everywhere says "plan". | -| "terminals" (anchor `#read-terminals`, and `#pipeline-write-terminals-out-and-merge`) | The last call in a chain. It only shows in anchors, but I saw it. | -| "escape hatch" (208, 467) | A way out of the typed API. Used twice, never unpacked. | -| `_id: null` in every `group()` example | "Put everything in one group." Never said. | -| `1 | -1` in `sortBy` | Ascending / descending. | -| `output` in `acc.top({ output, sortBy })` | The fields to return. Never explained and never shown. | -| `$redact` with `$$KEEP` / `$$PRUNE` (467) | Raw MongoDB features. Nothing on this page tells me what they are or why they are named here. | -| "field path" (line 130, `fn.literal` description) | A reference to a document field, as opposed to a constant. | - ---- - -## 3. "So what do I actually type?" — places the page does not say - -1. **`pipe()` has no example at all.** It is the escape hatch and it shows zero code. "`new MongoMatchStage(...)`" — with what arguments? What does the import line look like? I cannot use `pipe()` after reading this. -2. **The 65 unlisted `fn.*` helpers.** I need `fn.substr` or a date-add. The page gives me a naming rule (drop the `$`, camelCase) and five exceptions, but no list, so I have to guess and hope the compiler agrees. -3. **The 14 operator-form operators at line 406** — `inc`, `push`, `pull`, `pullAll`, `pop`, `currentDate`, `setOnInsert`, `rename`. Every example on the page uses `set` and nothing else. What are the arguments to `f.tags.pull(...)`? To `f.views.inc(...)`? To `f.updatedAt.currentDate(...)`? Unanswered. -4. **`merge()`'s `whenMatched` and `whenNotMatched`** (line 440). Named, no values listed, no example. I cannot write a merge that does anything other than the default. -5. **`out()`'s second argument** (line 439): "A second argument names a different database." A bare string? An object? No example. -6. **`acc.top` / `bottom` / `topN` / `bottomN`.** Line 33 says they "share the same call shapes" with `firstN`, but they do not — they take `output` and `sortBy`, and `firstN` takes `input`. So the one worked example does not transfer, and `output` is never shown. -7. **Converting a string id to an ObjectId.** Line 138 tells me the obvious approach throws, and points me at "an expression taken from the `f` argument" — which is not what I have when the id came from a URL parameter. The answer turns out to be `rawCommand()` 330 lines later, but the two are never connected at the point where I hit the problem. -8. **`durationSum: acc.sum(fn.literal(1))`** (line 44). The variable is called "durationSum" but it sums the constant 1, which just counts. I do not learn how to sum an actual field. Show `acc.sum(f.views)`. -9. **`findOneAndUpdate`'s second argument** — is it optional? Both examples pass it. The default is stated, so presumably yes, but the signature is never given. -10. **What `acc.first()` / `last()` mean without a `sort()`.** The example sorts first. The table says "first document in the group" with no warning that this is meaningless unsorted. - ---- - -## 4. Places that explain how the tool works inside, when I only wanted to know what to do - -- Line 136: `TypeError: visitor.expr is not a function` — a "visitor" is your code, not mine. -- Line 138: `Cannot read properties of undefined (reading 'codecId')` and "because `fn.literal()` has no field type information" — internal cause for an internal crash. -- Line 207: "because a stage the builder does not recognise drops the result shape" — the reason is internal; the consequence is all I need. -- Line 247: `Error: Unreachable: items.length > 0 but first is undefined` plus "This is an internal consistency guard, not a friendly validation message". -- Line 407: "(which emits an `$addFields`-style stage)" — I do not need to know what it compiles to. -- Line 462/466/467: "bypassing the typed AST", "packages a command node", "without translating it". -- Lines 33, 120: "executed against a live database", "exercised by the test suite", "exercised here as a representative" — three references to your test suite. Your tests are not my concern, and "the ones exercised by the test suite" tells me the table's contents were chosen by what you happened to test, which makes me trust the table less. - ---- - -## 5. Could I do what the page is for, after one reading? - -Partly. I could write a `group()` with `count`, `sum`, `avg`, `min`, `max`, `first`, `last`, `push`, `addToSet`. I could write a simple `updateMany` with `set`, a `deleteMany`, an `insertOne`, and a `findOneAndUpdate` with `returnDocument`. Those have real, complete, copyable examples. - -I could **not**: - -- Use any `fn.*` helper not in the eight-row table, because I have no list of the other 65 and no way to check whether the one I want exists. -- Use any update operator other than `set` — that is 13 of the 14 listed. -- Use `pipe()`, because there is no example and no import. -- Use `acc.top`, `bottom`, `topN`, or `bottomN`, because `output` is undefined and unshown. -- Configure `merge()` beyond the default, because `whenMatched` and `whenNotMatched` have no values listed. -- Convert a string id from my application into an ObjectId for a filter, which is something I do constantly in ORM 7. -- Explain to a colleague what `.node` is, or when a value is "decoded" and when it is not — and both of those change what my code gets back at run time, so not knowing them is a real risk, not a cosmetic one. - -The page reads as if it were assembled from a test suite: what was tested got an example, what was not got a name in a table or nothing. The reference sections that matter most for day-to-day work — the full `fn.*` list and the update operators — are the ones reduced to a name. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part2.round2.md b/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part2.round2.md deleted file mode 100644 index 85087cce09b..00000000000 --- a/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part2.round2.md +++ /dev/null @@ -1,119 +0,0 @@ -Read once, top to bottom, as an ORM 7 user. - -## 1. Sentences I could not restate after one reading - -**Line 132** — the `.node` rule. This is the worst sentence block on the page. -> "Every `fn.*` call returns a wrapper object. Its `.node` property is the raw expression inside that wrapper. `match()` takes the wrapper itself, through `expr(...)`. `fn.cond()` and the stages that take an options object take `.node`." - -What stopped me: three ideas about three different call sites, plus an unresolvable reference. "the stages that take an options object" — which stages are those? I cannot name one. Is `project()` one? The example at line 151 passes a bare `fn.add(...)` into `project()`, with no `.node`, so apparently not — but I had to infer that from an example instead of from the rule. There are three cases (bare wrapper, `.node`, `expr(...)`) and the page states two. - -Plainest wording I'd have understood: a table. "Pass the `fn.*` value as-is inside `project()`, `addFields()` and accumulators. Pass `.node` as the first argument of `fn.cond()` and inside `f.stage.set()`. Wrap it in `expr(...)` only inside `match()`." - -**Line 137 vs the example at line 178** — these contradict each other. -> "**Inside `fn.cond()`, pass `.node`.** Write `fn.cond(fn.eq(a, b).node, thenExpr, elseExpr)`." - -The example writes `fn.cond(fn.eq(...).node, fn.literal('is-tutorial'), fn.literal('not-tutorial'))`. Only the first argument gets `.node`; the other two do not. So "inside `fn.cond()`, pass `.node`" is false as written. I read it twice and still had to work the rule out from the code. Say: "The condition argument of `fn.cond()` takes `.node`. The then and else arguments do not." - -**Line 117** — -> "Five have a different name because the plain one was taken" - -Taken by what? `fn.first` does not appear anywhere. `acc.first` exists, but that is a different import on a different object. I cannot resolve "taken", so I cannot check my own guesses about names. - -**Line 117, second half** — -> "Each helper is the camelCase name of the MongoDB aggregation operator without its `$` prefix, so `$toUpper` is `fn.toUpper`" - -This only helps someone who already knows MongoDB's aggregation operators by name. I have used Prisma 7 for two years and have never written `$dateTrunc`. The table at 121–130 is names only, with no argument list, so for anything except the four or five obvious ones I still do not know what to type. - -**Line 136** — -> "and the rest of the date, regex, replace, and object helpers." - -I cannot restate this because it does not tell me anything. Which helpers, and what named arguments does each take? `fn.dateDiff` takes an object — with what keys? The page never says. - -**Line 33** — -> "Wherever a signature takes `n`, wrap the number: write `n: fn.literal(2)`, not `n: 2`." - -`fn` has not been introduced yet. It is defined 80 lines later, at line 111. Same problem at line 59, where `fn.year()` appears in an accumulator example before `fn` exists. Move the `fn` section above accumulators, or say "`fn.literal` comes from the expression helpers below". - -**Line 235** — -> "Pipeline form calls `f.stage.*` and can read other fields of the same document." - -`f.stage` is not explained until line 382. On first reading I did not know whether `stage` was a literal property name or a placeholder for something. It is literal, which I only learned later. - -**Line 381** — -> "There are fourteen, listed under the bullets." - -I looked for a bulleted list. There is a table. "Listed in the table below" is what I needed. - -**Line 246** — -> "The builder does not check it against your contract, and each value is stored exactly as you pass it, with no conversion to the type your contract gives that field." - -I understand the words. I do not understand the consequence, which is the only part I care about. My contract says a field is a date. What do I pass? A JS `Date`? An ISO string? If I pass the string, does the read side then break because "decoded" expects a real date? The page states the rule and stops exactly where my question starts. Same for line 246's tacked-on "`match()` filter values follow the same rule." - -**Line 303** — -> "its `_id` is a raw `ObjectId` even though the TypeScript type says otherwise." - -Says otherwise how? I guessed the type claims `string`. The page never says what the type claims, so I cannot predict what will break. - -**Line 191** — -> "Dates and ids are raw driver values." - -"Raw driver value" is not defined. I guessed: a `Date` object and an `ObjectId` object from the `mongodb` package. If that guess is right, say it in those words. - -**Line 33, last sentence** — -> "`acc.first()` and `acc.last()` only mean something when a `sort()` comes before the `group()`." - -What happens if I forget? An error? A silently arbitrary value? "Mean something" is doing a lot of work. I would have understood: "Without a `sort()` before the `group()`, the document MongoDB calls first is arbitrary and can change between runs." - -## 2. Words and phrases I had to guess - -- **`f.kind`** (lines 86, 178, 203) — guessed it is the field that says whether a `Post` is an `Article` or a `Tutorial`. Part 1 may have said so; the field just appears here. -- **`_id: null`** in every `group()` example — guessed it means "one group containing every document". Never stated on this page. -- **"the plain one was taken"** — guessed there is an internal name clash I cannot see. -- **"streams the pipeline output"** (line 436) — guessed "streams" means nothing operationally different from "writes", and is just describing `$merge`. -- **"result objects, not documents"** (line 245) — guessed that `runtime.query()` on a write returns a one-element array holding that object. I only worked this out because every example destructures with `const [result] =`. The page never says why a single write result comes back inside an array. -- **`acc.bottom`, "the reverse sort order"** (line 27) — guessed it means the reverse of the `sortBy` I supply. Could also have meant a fixed descending default. -- **`MongoFieldFilter.eq('kind', 'tutorial')`** (line 203) — guessed it builds a filter for a raw stage. Nothing on the page explains this class. -- **"Top `n` values by value"** for `maxN` (line 24) — "by value" reads like a typo or a tautology. Guessed it means the n largest. -- **"terminals"** in the anchors `#read-terminals` and `#pipeline-write-terminals-out-and-merge` — internal vocabulary that leaks into URLs. Readers will see these in a link. - -## 3. Places I asked "so what do I actually type?" and got no answer - -1. **`expr()` is never imported.** It is named at lines 111, 132, 137, 138 as the thing that makes `match()` work with computed values, and no example on the page uses it, imports it, or shows its shape. This is one of the page's three main rules and it has zero runnable code. -2. **Argument shapes for most `fn.*` helpers.** The table gives 70-odd names. Five get an argument shape at line 136. For `fn.dateDiff`, `fn.dateTrunc`, `fn.slice`, `fn.zip`, `fn.range`, `fn.getField`, `fn.convert`'s `to` values, and the rest, I have a name and nothing else. -3. **`insertOne` and required fields.** Line 255 passes `bio: null` and `address: null` explicitly. Must I list every field, including nullable ones? Must I pass `_id`? The page shows one shape and states no rule. -4. **Dates and ObjectIds on insert.** Following from line 246 — what literally goes in a date field or a relation id field? This is the first thing anyone hits. -5. **Building raw stages for `pipe()`.** Line 190 says "such as `new MongoMatchStage(...)`". If I want a raw `$lookup` or `$redact` stage, what class do I construct and with what arguments? The one example is the one stage I would never need `pipe()` for, since `match()` is typed. -6. **`pipe(stage)`** — what do I put in `NewShape`? An interface? No example. -7. **`fn.toObjectId()` on a real field.** Line 139 says to "give `fn.toObjectId()` an expression taken from the `f` argument instead". No example shows it. The only id example (line 488) sidesteps into `rawCommand()`. -8. **`whenMatched` as "an array of update stages"** (line 437) — what does one of those look like? Is it `f.stage.*`, or raw `$set` objects? Not shown. -9. **`acc.top` return shape.** The table says "Single document". The example comment says "output is what you get back" and names the variable `newestTitle`, so it returns a title string, not a document. Which is it? These two lines in the same page disagree. - -## 4. Places the page explains internals when I only wanted to know what to do - -- **Line 132, "Every `fn.*` call returns a wrapper object. Its `.node` property is the raw expression inside that wrapper."** I do not want to know the builder has wrapper objects with a raw expression inside. I want a rule for where to type `.node`. The internals are offered in place of the rule. -- **Line 139, "because a literal carries no field type to convert from."** This explains why the library cannot do it. It does not help me, and the sentence before it already told me it throws. -- **Line 437, "The builder passes both to MongoDB unchecked."** What I need is the consequence: a misspelled `whenMatched` value fails at the database, not in TypeScript. -- **Line 361, "the unknown key is ignored and the default `'after'` applies"** — this one earns its keep, because it tells me the failure is silent. Contrast with the two above. -- **Line 218, "`aggregate()` is an alias: it returns the identical query and runs identically."** Fine, but I am left asking why both exist and which one I should write. The page does not recommend one. - -## 5. Structure problem - -`### pipe()` (line 184) sits at heading level 3 under `## Expression helpers` (line 109). `pipe()` is a pipeline stage, not an expression helper. In a rendered sidebar it will appear nested under expression helpers. It belongs beside the other stages, which are in part 1. - -Also, `## Accumulators` jumps straight to `#### Examples` at line 35, skipping level 3. - -## After one reading, could I do what the page is for? - -Partly. I could write a `group()` with `count`, `sum`, `avg`, `min`, `max`, `first`, `last`, `push`, `addToSet`. I could write a basic `updateMany`, `deleteMany`, `insertOne`, `out()`, and `merge()`, and copy the `rawCommand()` `_id` filter. Those have complete examples and I would type them correctly. - -What I still would not know: - -- Where `expr()` comes from, or how to write a single `match()` filter that uses a computed value. This is presented as core behaviour and has no code at all. -- Whether to write a bare `fn.*` value, `.node`, or `expr(...)` at any call site not shown in an example. The stated rule and the shown examples do not agree. -- What arguments any date, regex, array, set, or conversion helper takes. I have names only. -- What to pass for a date or an id when inserting, which is the first thing I would do. -- How to build any raw stage for `pipe()` other than the match stage shown. -- Whether `acc.top` gives me back a document or a single value. -- How to convert a string id from my own code without dropping to `rawCommand()`. The page says an expression from `f` works and never shows it. - -The write-methods section (lines 227–457) is the strongest part: the array rule at 231–236 is stated once, firmly, with the error codes, and every method has a runnable example. The expression-helpers section is the weakest: it is a name list plus a rule I cannot apply. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part2.round3.md b/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part2.round3.md deleted file mode 100644 index 834e45eb40f..00000000000 --- a/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part2.round3.md +++ /dev/null @@ -1,59 +0,0 @@ -# Reader review: `pipeline-builder.part2.mdx` - -## Sentences I could not restate after one reading - -**Line 33 — the paragraph after the accumulator table.** This is seven separate rules in one block: which helpers share a signature, what `output` means, what `sortBy` means, the `n: fn.literal(2)` rule, where `fn.literal` comes from, and the `sort()`-before-`group()` rule. I had to reread it three times and I still could not tell you from memory which of `top`/`bottom`/`topN`/`bottomN` take `n`. Split it. The `sort()` rule in particular is a real correctness warning buried as the last sentence of a housekeeping paragraph. - -**Line 109 — "Five have a different name: `in` and `typeof` are reserved words in JavaScript, `toString` is already a method on every JavaScript object, and `$first` and `$last` get the suffix `Elem`."** Three reasons, five names, and the reasons are in a different order than the renames that follow. I counted on my fingers. Plainer: give a two-column table — MongoDB operator, helper name — with a one-line reason each. - -**Line 124–130 — the `.node` table.** I do not know what `.node` is. The page never says. It reads as: "sometimes you pass the thing, sometimes you pass a property of the thing, and there is no rule, just memorize this list." Line 129 is a single table cell holding five function names and five option names. I cannot hold that. What I actually wanted: one sentence saying *why* these differ, so I can predict the next case instead of looking it up every time. - -**Line 145 — "So does every other helper not named above, in the order MongoDB documents for that operator."** This tells me the reference is incomplete on purpose and sends me to MongoDB's docs, with no link. To use `fn.substrBytes` I have to leave the page. - -**Line 238 — "Pipeline form calls `f.stage.*`, where `stage` is a literal property name, and can read other fields of the same document."** "Literal property name" stopped me. I guessed it means "you literally type `.stage`, it is not a placeholder for a stage name" — which is worth saying, because everything else in `f.*` is a field name. Say that directly: "type `f.stage` exactly; `stage` is not a placeholder." - -**Line 249 — the `insertOne` remark.** Six ideas in one paragraph: the record is plain, TypeScript checks nothing, `_id` is optional, values are stored raw with no conversion, pass real `Date`/`ObjectId`, `match()` follows the same rule, and `insertMany([])` throws. The "no conversion" point contradicts the "decoded" promise I was given in part 1 and deserves its own callout, not sentence four of seven. - -**Line 399 — "`pop(direction)` | `1` removes the last element, `-1` the first. Optional, and defaults to `1`."** Ambiguous: is the *argument* optional, or the operation? I guessed the argument. - -## Words and phrases I had to guess - -- **`.node`** — guessed: some internal wrapper object, and `.node` unwraps it. Never defined. -- **"built query"** vs the variable named `plan` — guessed these are the same thing. The prose says "built query", every example says `plan`, and neither the writes section nor `out()`/`merge()` calls `.build()`. I eventually worked out that write methods return the built query directly, but only because line 247 says so in passing. -- **"expression"** — used throughout (`sum(expression)`, "written as an expression"). I guessed it means "either an `f.something` or an `fn.something(...)`, never a plain JavaScript value." Line 145 half-confirms this. But `f.bio.set('x')` and `MongoFieldFilter.eq('kind', 'tutorial')` both take plain values, so the rule clearly does not hold everywhere and the page never says where the boundary is. -- **"a plain string, names a different database"** (line 436) — guessed "plain" just means "a string, not an options object." -- **"Root-level writes"** — guessed: called on `from(...)` before any stage. -- **"an `N`-variant"** (line 85 heading) — guessed: the family of accumulators ending in `N`. Coined jargon in a heading. - -## Where I asked "so what do I actually type?" and got no answer - -1. **`$and`, `$or`, `$not`, `$switch`, `$ifNull`, `$map`, `$reduce`, `$filter` have no helper** (line 109). This is the most common thing I would need — an OR condition — and the page stops at "no helper." Do I use `pipe()`? `rawCommand()`? Nest `fn.cond`? Not said. This is the single biggest gap on the page. -2. **The 28 stage classes in line 190.** Only `MongoMatchStage`, `MongoCountStage`, and `MongoFieldFilter.eq` get a shape. I have no idea what `new MongoLookupStage(...)` takes. Same for `MongoFieldFilter` — `eq` is the only method shown; I do not know if `gt` exists. -3. **Getting an id back as a string.** Lines 192 and 304 both tell me the TypeScript type lies and I get an `ObjectId`. Neither tells me what to do about it. -4. **Filtering by `_id`.** Line 467 says the typed builder cannot do `_id` equality filters. That is a severe limitation mentioned as a subclause. The fix is the last example, which is good, but I only found it after reading the whole page. -5. **`fn.convert`'s `to` values** (line 144) — `'int'` and `'objectId'` are shown as examples. The full list is elsewhere, unlinked. -6. **`merge()`'s `on`** (line 437) — "the field or fields to match on." String, or array of strings? The example shows a string only. -7. **`out()` and `merge()` "produce no rows of their own"** (line 439). So what does `await runtime.query(plan)` return? Empty array? Undefined? The example discards it, which does not tell me. -8. **`upsertOne` on the root** (line 290) takes two callbacks with no labels. I inferred filter-then-updater from the heading text, not from the code. - -## Where the page explains internals when I only wanted to know what to do - -- **Line 190's 28-class list.** I wanted "here is how to add a raw stage." I got the module's whole export list. One example plus a pointer would do. -- **The `.node` table (129).** This is a description of how the builder represents expressions internally, presented as a rule I must memorize. -- **Line 222 — "`aggregate()` is an alias that returns the identical query. Write `build()`."** If `build()` is the answer, the alias is trivia. -- **Line 224–228 — the "Return type" table** has one row and restates the sentence above it. -- **Line 362 — "the unknown key is ignored and the default `'after'` applies."** Useful warning, but the mechanism explanation ("a typo produces a wrong but plausible result") is longer than the instruction. - -## Could I do what the page is for, after one reading? - -Partly. I could write a `group()` with `count`, `sum`, `avg`, `min`, `max`, `first`, `last`, `push`, `addToSet`. I could write a simple `project()` with arithmetic or string helpers. I could do `insertOne`, `insertMany`, `updateMany` with operator form, `deleteMany`, and `findOneAndUpdate` with `returnDocument`. The write sections are the strongest part of the page: the examples match the prose and each shows the result shape. - -What I would still not know: - -- Whether my `fn.*` value needs `.node`, plain, or `expr(...)` at any call site I have not memorized — and I would get it wrong, because the page gives me a list, not a rule. -- How to express OR, AND, or a null fallback. I would be stuck the first time I needed `$or`. -- How to build any raw stage other than `MongoMatchStage` and `MongoCountStage`. -- Which helpers take positional arguments in which order, without opening MongoDB's docs. -- What `.node` actually is. -- How to get a string `_id` out of a `findOneAndUpdate()` result. -- Whether values I pass to `insert*` and `match()` are converted (line 249 says no; part 1's "decoded" promise says the read direction is converted). I would have shipped a bug here. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part2.round4.md b/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part2.round4.md deleted file mode 100644 index 6417be71541..00000000000 --- a/docs/orm-docs-audit/c21-orm-reference/pipeline-builder.part2.round4.md +++ /dev/null @@ -1,112 +0,0 @@ -Read once, top to bottom, as a Prisma 7 user who has never seen this builder. - -## Sentences I could not restate after one reading - -**1. Line 108, the rename table's last row.** -> `| $first` and `$last` | `fn.firstElem` and `fn.lastElem` | none | - -The column is headed "Why". The answer is "none". So two helpers were given different names for no reason? That reads as a mistake or a joke. The other four rows all give a real reason. I'd guess the real reason is a collision with `acc.first`/`acc.last`, but the page will not say. Plainest wording: say the actual reason, or drop the row's "Why" cell rather than writing "none". - -**2. Line 95, the definition of an expression.** -> "An expression is an `f.field` value or an `fn.*` value, never a plain JavaScript value. Stage callbacks and these helpers take expressions. The update operators on the [write methods](#write-methods) and the `MongoFieldFilter` methods take plain JavaScript values instead." - -Three rules in three clauses, and the third names two things (`MongoFieldFilter`, write-method update operators) that have not appeared yet. I could not hold it. Plainest wording: "Inside a stage, write `f.title` or `fn.toUpper(...)` — never `'hello'` or `3` on their own; wrap constants in `fn.literal('hello')`. Update operators and `MongoFieldFilter` are the opposite: they take ordinary JavaScript values, so write `f.bio.set('hello')`, not `f.bio.set(fn.literal('hello'))`." - -**3. Line 125, the `.node` paragraph.** -> "Every `fn.*` helper returns a typed wrapper, and `.node` is the raw MongoDB expression inside that wrapper. Arguments typed as an expression take the wrapper. Arguments typed as a raw MongoDB expression take `.node`, and those are the condition of `fn.cond()`, `redact()`, and the option-object stages..." - -I cannot apply "Arguments typed as an expression take the wrapper" to code I am writing, because nothing on the page tells me how to look at an argument and see which type it is. I only ever learn it from the list that follows. The general rule is unusable; the list is the whole answer. Cut the general rule and keep the table. - -**4. Line 130, the `.node` table's second row.** -> "The condition argument of `fn.cond()`, and `f.stage.set()`, `f.stage.replaceRoot()`, `f.stage.replaceWith()`" - -`f.stage.*` is not defined until line 378, roughly 250 lines later. On first read this row is unresolvable. Also the row is ambiguous: does "the condition argument of" apply to all four items, or only to `fn.cond()`? (It turns out to be only `fn.cond()`, but I had to reach line 416 to be sure.) - -**5. Line 161, the named-arguments remark.** -> "Some helpers take one object of named arguments rather than positional ones: `fn.dateDiff({...})`, `fn.dateTrunc({...})`, `fn.dateAdd({...})`, ... and `fn.convert({ input, to, onError?, onNull? })`." - -Eleven signatures inside one sentence. I skimmed it and retained nothing. This is a table, not a sentence. - -**6. Line 162.** -> "So does every other helper not named above, in the order given for that operator in [MongoDB's aggregation operator reference]." - -I can restate this, but what it actually says is: for about sixty of the helpers in your table, this reference page will not tell me the arguments. I have to leave and read MongoDB's docs. That is worth saying plainly rather than burying it in a remark. - -**7. Line 163.** -> "To turn a string id from your own code into an `ObjectId`, build the `ObjectId` yourself and put it in a raw pipeline with [`rawCommand()`](#rawcommand). The last example on this page does exactly that." - -The last example does not do that. It does `new ObjectId(postId)` in a `$match`. It never converts an id inside a pipeline, which is what `fn.toObjectId` is for. So I could not tell whether the cross-reference was wrong or whether I had misunderstood `fn.toObjectId`. - -**8. Line 236.** -> "Write methods are available at three points: on the root collection, after a `match()` filter, and as the last call in a pipeline." - -"As the last call in a pipeline" meant nothing until I reached `out()` and `merge()` 180 lines later. The first two points name the position; the third does not name the methods. Plainest wording: "...and at the end of any pipeline, where `out()` and `merge()` write the output into a collection." - -**9. Line 242.** -> "Pipeline form calls `f.stage.*`, and can read other fields of the same document." - -"Can read other fields of the same document" is the entire reason to choose pipeline form, and it is one trailing clause. I did not understand what it bought me until I saw `f.stage.set({ bio: f.name.node })` at line 416. - -## Words and phrases I had to guess - -- **"option-object stages"** (line 125). Guessed: stages whose argument is a single options object rather than a callback. The page lists five of them (`bucket()`, `bucketAuto()`, `graphLookup()`, `setWindowFields()`, `fill()`), so I could work out the membership, but never the definition. -- **"typed wrapper"** (line 125). Guessed: an object the builder makes, whose real contents sit in `.node`. This is the tool's internals, not something I need. -- **"combinators"** (line 159). Guessed: the `eq`/`gt`/`in` filter methods. The word is used as if I already know it; the link is to a different page. -- **"`Mongo*Stage` classes, one per MongoDB stage"** (line 202). Guessed: `$unwind` becomes `MongoUnwindStage`. The page never lists them or states the naming rule outright, so I am guessing a class name and hoping the import resolves. -- **"read terminals"** / **"pipeline-write-terminals"** (anchors at lines 222 and 421). Guessed: "terminal" means the call that ends the chain. The word appears only in the anchors, never in the prose, but it is what shows up in the URL I share with a colleague. -- **"decoded"** applied to `pipe()** (line 204): "the rows come back exactly as MongoDB returns them, whatever the TypeScript type claims." Guessed: adding `pipe()` means the pipeline is no longer all-eight-approved-stages, so decoding stops for the whole query. The page does not connect this back to the eight-stage rule, and does not say whether the loss applies to the whole result or only to fields the raw stage touched. -- **"the driver"** (lines 204, 311, 312). Guessed: the `mongodb` npm package. Never introduced. -- **`f.name.node`** (line 416). The `.node` rules at lines 125-131 are all about `fn.*` values. Here `.node` is on a plain field reference. I guessed that field references also carry `.node`, but the rule as written does not cover this case. - -## Places where I asked "so what do I actually type?" and got no answer - -1. **Argument order for most helpers** (line 162). The table at 112-123 lists roughly a hundred helpers. Apart from the eleven object-argument ones and `slice`/`range`, the page tells me to go read MongoDB's docs. So this reference does not let me write `fn.dateDiff` — sorry, `fn.substr` — without leaving it. - -2. **OR/AND/NOT inside `project()` or `addFields()`** (line 160): "Write that stage yourself with `pipe()`, or write the whole command with `rawCommand()`." No example. I have a concrete need (`$or` inside a computed field) and two pointers, neither of which shows the code. - -3. **`MongoAndExpr`** (line 158) is named but never imported or shown. Only `MongoOrExpr` has an example. I assume the import path is the same, but I am assuming. - -4. **Which `Mongo*Stage` class do I want?** (line 202). Two are shown (`MongoMatchStage`, `MongoCountStage`). There is no list and no stated naming rule, so for any third stage I am typing a name and hoping. - -5. **`whenMatched` as an array of update stages** (line 429): "`MongoAddFieldsStage`, `MongoProjectStage`, and `MongoReplaceRootStage` objects". No constructor signatures, no example. I cannot type `new MongoAddFieldsStage(...)` from this. - -6. **Can I put `sort()` between `match()` and a write?** Line 311 tells me to "use `findOneAndUpdate()` after a `sort()` when the choice matters", but the section is titled "Writes after `match()`", every example is `match()` then write, and line 313 says `skip()` blocks these methods. I do not know whether the chain is `.match(...).sort(...).findOneAndUpdate(...)` or something else, and no example shows it. - -7. **What fields must an insert document have?** Line 253 says "TypeScript requires no particular field, and the builder does not check the record against your contract", yet the example at 265 passes `bio: null` and `address: null` explicitly. Can I leave them out? Should I? The page says the builder won't stop me and then models the opposite. - -8. **The two examples I cannot run.** Line 44 ("on a collection with a numeric `views` field") and line 409 (`f.views.inc(1)`, `f.tags.push('mongodb')`) both invent fields the example schema does not have. Every other example uses `users` and `posts`. For `inc`, `push`, `pop`, `pull`, `pullAll`, and `addToSet` — six of the fourteen operators — there is no runnable example at all. - -9. **Mixed argument conventions inside one accumulator call** (lines 35-36, 85-86). In `acc.top({ output: f.title, sortBy: { createdAt: -1 } })` I use the `f` argument for `output` and a bare string key for `sortBy`. In `acc.firstN({ input: f.title, n: fn.literal(2) })` I use `f` for `input` and `fn.literal` for a number. Three conventions in two lines. Line 36 flags the `n` rule but nothing explains why `sortBy` keys are bare strings when everything else goes through `f`. - -10. **`MongoFieldFilter.eq('kind', 'tutorial')`** (line 148) takes the field name as a string. Everywhere else I write `f.kind`. The page never says that building a filter by hand costs me the typed field reference and the spell-checking that comes with it. I would want that said out loud, because it is the trade I am making. - -11. **`neq` vs `ne`.** `MongoFieldFilter` has `neq` (line 159). `fn.*` has `ne` (line 118). Two spellings of the same idea, on the same page, uncommented. I will get this wrong. - -12. **`setOnInsert(value)`** (line 398): "A value written only when an upsert inserts a new document." So it only does anything inside `upsertOne`. The page does not say that, and the `upsertOne` examples do not use it. - -13. **`fn.toObjectId` throws** (line 163) but, unlike every other failure on the page, no error `code` is given. Elsewhere I get `ORM.MUTATION_DATA_MISSING`, `ORM.ARGUMENT_INVALID`, `ORM.OPERATION_UNSUPPORTED`. Here I just get "throws", so I cannot recognise it in a log. - -14. **Line 256** points at "the `match()` warning" for the `_id` rule, and line 457 does the same. Both links go to `#match`, which is in the half I do not have. Inside this page the rule appears twice as a pointer and once as an example, but never as a statement of what actually goes wrong if I try `f._id.eq(someString)`. - -## Places that explain the machinery when I only wanted the instruction - -- **Line 125**: "Every `fn.*` helper returns a typed wrapper, and `.node` is the raw MongoDB expression inside that wrapper." I do not need to know there is a wrapper object. I need to know: type `.node` in these four situations, and nowhere else. The table right below already does that job. -- **Line 125 again**: "Arguments typed as an expression take the wrapper. Arguments typed as a raw MongoDB expression take `.node`." This describes the type signatures the library authors wrote. It is not actionable, because I cannot see those signatures while typing. -- **Line 204**: "After a `pipe()` stage the rows come back exactly as MongoDB returns them, whatever the TypeScript type claims." The useful half is the second clause — the types lie to you. The framing is about how the builder's decoding step is implemented. -- **Line 311**: "Which document `updateOne` / `deleteOne` picks is up to the driver." Correct but unhelpful. What I need is: do not use `updateOne` when it matters which one; use `findOneAndUpdate` after a `sort()`. The sentence after does say that, so the driver sentence earns nothing. - -## Could I do what the page is for, after one reading? - -Partly. I could write a `group()` with the common accumulators, do a simple `project()` with `fn.concat`/`fn.toUpper`, and do all the basic writes — `insertOne`, `insertMany`, `updateMany`, `deleteMany`, `upsertOne`, `findOneAndUpdate`. Those have real, runnable, complete examples, and the "return an array" rule at line 238 is stated firmly enough that I would not get it wrong. - -What I still would not know: - -- The arguments to most of the ~100 expression helpers. The page hands that back to MongoDB's docs. -- Whether to write `.node`, in any situation not literally listed at lines 129-132. The rule is a list, not a principle, so the first unlisted case stops me. -- How to write an `$or` inside a `project()`. I know I must use `pipe()` or `rawCommand()`, and that is all I know. -- Which `Mongo*Stage` class to import for any stage other than match and count. -- How to build the update-stage objects `merge({ whenMatched: [...] })` accepts. -- Whether `sort()` is allowed between `match()` and `findOneAndUpdate()` — which line 311 tells me to do and no example shows. -- How to use `inc`, `push`, `pop`, `pull`, `pullAll`, or `addToSet` against the schema the page uses, since the only examples for them reference fields that do not exist. - -The two structural problems, if you only fix two things: line 161's eleven signatures should be a table, and line 162 should not outsource argument order for most of the helper list to another site. A reference page that cannot tell me the arguments is not yet a reference page. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/raw-queries-and-index.round1.md b/docs/orm-docs-audit/c21-orm-reference/raw-queries-and-index.round1.md deleted file mode 100644 index 9152608728a..00000000000 --- a/docs/orm-docs-audit/c21-orm-reference/raw-queries-and-index.round1.md +++ /dev/null @@ -1,63 +0,0 @@ -# Reader review, round 1: index.mdx and raw-queries.mdx - -## index.mdx - -### Sentences I could not restate - -1. "Prisma ORM has three query surfaces." "Query surface" is not a word I know; I guessed "three different APIs for writing queries". Say that. -2. "The pipeline builder gives you a typed way to build MongoDB aggregation pipelines through `db.query`." `db.query` appears here and nowhere else; the other two are named after the property (`db.orm`, `db.sql`), so I expected `db.pipeline`. Plainest: "The pipeline builder builds MongoDB aggregation pipelines. You reach it at `db.query`." -3. "The method reference pages document each method with the classic Remarks / Options / Return type / Examples structure (the raw queries page is deliberately more narrative)." "Classic" to whom? "More narrative": guessed "prose instead of tables". Why am I told this before reading anything? -4. "When a method's behavior differs between PostgreSQL and MongoDB, exists on only one database, or is type-checked but not enforced at runtime, the method's own Remarks call that out inline." Three conditions plus a promise. "Type-checked but not enforced at runtime" stopped me. Plainest: "If a method works differently on PostgreSQL and MongoDB, or exists on only one of them, its Remarks say so." - -### Words guessed -- "query surfaces": three separate query APIs. -- "model-level methods like `where()`, `create()`, and `include()`": in Prisma 7 these were arguments, not methods. Guessed they are chained calls now. The page never says this changed, and it is the biggest difference. -- "client lifecycle": connecting and disconnecting. -- "raw escape hatches": the way out when the typed API cannot do it. - -### So what do I type? -The whole page: not one line of code. One three-line snippet showing the same query in the ORM client and the SQL query builder would answer the page's own question. - -### Contradiction -"Reach for the SQL query builder when you need a join", but the ORM client has `include()`, which is how I did joins in Prisma 7. Which do I use? - -### Could I do what the page is for? -Partly: I can pick which page to open. I cannot tell whether my Prisma 7 queries map to the ORM client or the SQL query builder, and I do not know what `db.query` is. - -## raw-queries.mdx - -### Sentences I could not restate - -1. The page-level warning (lines 13 to 15): one block, six claims, `codecId` used four times before it is defined. "`.returnsRow(spec)` names a `codecId` for every column, so its rows are converted just like a builder query's." What is a codec, what does "converted" mean, what does a builder query's row look like? Plainest: "A `codecId` such as `pg/text@1` tells Prisma how to turn the database's bytes into a JavaScript value. `.returnsRow()` needs one per column, so you get real JavaScript values back." "A `fns.raw` fragment's `.returns(codecId)` only tells TypeScript what type to expect, so the value is whatever the driver returns." What is whatever the driver returns? The first example then shows `upperEmail: 'ALICE@EXAMPLE.COM'`, a normal string, so the warning scared me and the example contradicted it. -2. Line 23: "A whole raw statement is a complete SQL statement written with `db.raw.sql`, which you terminate with `.returnsRow(spec).build()` for a plan `runtime.query(...)` streams rows from, or `.affectedCount().build()` for a plan `runtime.execute(...)` reports statistics for. Both sizes end in a plan, so both go through the same middleware and the same runtime." Too much: two APIs, two terminators, two runtime methods, "plan" and "middleware" both new. "Streams rows from" made me expect an iterator; the example awaits an array. Plainest: "Write the statement with `db.raw.sql`. If it returns rows, end with `.returnsRow(spec).build()` and run it with `runtime.query(plan)`. If it does not, end with `.affectedCount().build()` and run it with `runtime.execute(plan)`." -3. Line 67: "Interpolating another `Expression` ... splices that expression's AST into the fragment. It is not a string concatenation of rendered SQL: the interpolated expression lowers to its own AST node, so it stays parameterized and type-checked." Internals. Needed: "Interpolated values are always sent as query parameters, never pasted into the SQL string." -4. Line 84: "A bare JavaScript value can be interpolated directly, and the adapter picks a `codecId` for it from its JavaScript type: `number` becomes `pg/int4@1`, `pg/int8number@1`, or `pg/float8@1` depending on its range". "The adapter": first mention. "Depending on its range" gives no ranges. If I pass 3000000000, which do I get? -5. Line 118: "`db.raw` is the client's raw SQL API ... Its one key, `sql`, is the same tagged template as `fns.raw`." Wrong by the page's own account: line 212 uses `db.raw.collection(...)`. -6. Line 171: "A row-returning raw query composes: interpolate one into another template and its SQL and its parameters are spliced into the outer statement, in order, which is how you build a subquery or a CTE. The outer template declares its own row spec, and a column the inner query already declared can be inherited from its `.returns` record rather than restated." Four ideas. Also `.returns` is now a property holding a record, after being a method in five earlier examples. -7. Line 198: "A statement terminated with `.affectedCount()` is a plan handle, not a row source, so it cannot be interpolated." Invented terms. Plainest: "You cannot interpolate an `.affectedCount()` statement, because it produces no rows. You can interpolate an `UPDATE ... RETURNING`, because it does." -8. Lines 202 to 208: "Interpolating anything else, such as a `Date`, throws synchronously with code `RUNTIME.RAW_SQL_UNSUPPORTED_INTERPOLATION`" then "TypeScript already rejects an unsupported value at compile time." If TypeScript rejects it, when does the runtime error fire? The quoted message has literal ellipses, so I cannot match it. -9. Line 313, `findOneAndUpdate()`: "The MongoDB command AST carries `sort` and `returnDocument` fields, but the public wrapper does not expose them ... a value that reaches the call untyped is dropped without a run-time error." Internals. Needed: "`findOneAndUpdate()` accepts only `{ upsert }`. To sort, or to get the document after the update, do X." It then names `new RawFindOneAndUpdateCommand(...)` with no code and no import. -10. Line 346: "runs a raw MongoDB aggregate command through the pipeline builder, bypassing the typed AST. Its rows come back as `unknown`; you type them yourself." How? `aggregate()` took a type parameter; does this? - -### Words guessed -"raw driver values" (values with no Prisma conversion); `codecId`; "a plan" (a prepared object you hand to the runtime); "middleware" (mentioned once, never explained); "a function bag `fns`"; "the adapter" (the PostgreSQL driver layer); `Expression`; "contract column" (`user.columns.id`); "the contract's lowercase plural root" (the collection name); "buildable command"; "return an image of it"; "row source" / "plan handle". - -### So what do I type? -1. Two ways to get a runtime, both unexplained: PostgreSQL `const runtime = await db.connect()`, MongoDB `(await db.runtime()).query(plan)`. How do they relate? Which do I use? -2. Where is the list of `codecId` values? I see eight; `pg/timestamptz-temporal@1` appears once, unexplained. For a uuid, jsonb, or numeric I have no idea and no link. -3. `new RawFindOneAndUpdateCommand(...)`: the only way to sort or get the post-update document, with no signature, import, or example. -4. Filtering by `_id` in a raw pipeline: line 356 says `rawCommand()` exists partly for `_id` equality filters and then sends me to another page. Four lines of code would do. -5. Undeclared variables in every example: `aliceId`, `bobId`, `limit`, `kind`, `minPosts`. On MongoDB `aliceId` is a hex string (`new ObjectId(aliceId)`); on PostgreSQL presumably a UUID. Never said. -6. Is `fns.raw` safe with user input? "Stays parameterized" is buried under AST language, and nothing is said about interpolating a column or table name from user input, which is the real danger. -7. Two forms of `select()` with no bridge: `.select('upperEmail', (f, fns) => ...)` and `.select((f) => ({ id: f.id, serverNow }))`. -8. MongoDB writes go through `query()`, not `execute()`, destructured out of an array; PostgreSQL writes go to `execute`. Never pointed out. -9. Can I run a raw query inside a transaction? Nothing. - -### Internals instead of instructions -Line 67 (AST); line 23 (middleware, plans); line 313 (command AST, wrapper); line 346 (typed AST); line 263 "`insertMany()`'s `insertedIds` is a plain array of `ObjectId`, reshaped from the native driver's index-keyed map". - -### Smaller things -`.returnsRow(spec)` is singular but returns many rows. Line 137's SQL writes `FROM "user"` with no schema while the page insists tables live under `db.sql.public`. The MongoDB section says `'users'`, `'posts'`, `db.orm.users`, while elsewhere models are `db.orm.public.User`; no sentence explains that MongoDB has no schema segment. - -### Could I do what the page is for? -PostgreSQL raw SQL: mostly yes. Still unknown: which `codecId` for types not in the short list; whether a fragment's value comes back usable; whether interpolating user input is safe. MongoDB raw commands: no for the two cases I would hit: sorting or post-update document from `findOneAndUpdate`, and filtering by `_id` in a `rawCommand`. diff --git a/docs/orm-docs-audit/c21-orm-reference/raw-queries-and-index.round2.md b/docs/orm-docs-audit/c21-orm-reference/raw-queries-and-index.round2.md deleted file mode 100644 index 00ad4b1e189..00000000000 --- a/docs/orm-docs-audit/c21-orm-reference/raw-queries-and-index.round2.md +++ /dev/null @@ -1,169 +0,0 @@ -# Reader review: `index.mdx` and `raw-queries.mdx` - -I read both once, top to bottom, as a Prisma 7 user with no knowledge of this version. - ---- - -## Page 1: `/orm/reference/index.mdx` - -### Sentences I could not restate after one reading - -**1.** "The **pipeline builder** is at `db.query`. It builds MongoDB aggregation pipelines." - -What stopped me: the name. `db.query` sounds like "run a query." Two lines later the page writes `runtime.query(plan)`, which is a *different* `query`. I now have `db.query` (build a pipeline) and `runtime.query` (run a plan) and the page never says they are unrelated. I guessed they were the same thing. - -Plainer: "The pipeline builder is at `db.query`. It builds MongoDB aggregation pipelines. It is not the same as `runtime.query()`, which runs a built query." - -**2.** "In Prisma ORM 7 you passed `findMany({ where, include })` arguments; those arguments are chained methods now, and [Coming from Prisma ORM 7] maps each one." - -What stopped me: "those arguments are chained methods now" compresses the single biggest change in the product into half a sentence, buried at the end of a six-sentence paragraph. This is the sentence I most needed and it is the one I nearly skipped. - -Plainer: "In Prisma ORM 7 you wrote `findMany({ where, include })`. Now each of those arguments is a method you chain: `.where(...).include(...)`. See [Coming from Prisma ORM 7]." - -**3.** "The raw queries page uses prose instead." - -Could not restate: "prose instead" of what, exactly — instead of all four parts, or instead of the headings? It also turns out to be wrong: the raw queries page *does* have a `Remarks` heading under `findOneAndUpdate()`. One page contradicting the other on my second click is bad. - -### Words I had to guess - -- **"a projection across two tables"** — I guessed "picking some columns from a join." The page uses "projection" as if I already write SQL that way. -- **"today it works on PostgreSQL only"** — I guessed "MongoDB support is planned." "Today" is doing work a plain "currently, PostgreSQL only" would do without the wink. -- **`db.orm.public.User`** vs **`db.sql.public.user`** — capital `User`, lowercase `user`, in two adjacent lines of the same code block. I guessed one is the model and one is the table. The page never flags the casing difference, and this is exactly the kind of thing that costs a reader twenty minutes. - -### "So what do I actually type?" - -**The code block does not run.** - -```ts -const rows = await runtime.query(plan); -``` - -`runtime` is never created. Nothing above it produces a `runtime`. I know `db.runtime()` exists, but the page does not use it, so I do not know whether to write `const runtime = db.runtime()`, `await db.runtime()`, or something else. This is the first code on the first page of the section and I cannot copy it. - -**Why does one line need `.build()` and the other does not?** - -```ts -const admins = await db.orm.public.User.where(...).all(); -const plan = db.sql.public.user.select(...).build(); -const rows = await runtime.query(plan); -``` - -The ORM client is awaited directly. The SQL builder needs `.build()` and then a separate run step. That is a large, load-bearing difference between the two APIs and the page shows it without naming it. I was left guessing whether `.build()` is optional. - -### Internals where I wanted instructions - -Nothing serious. This page mostly stays on "what to do." - -### Could I do what the page is for? - -Partly. I can pick which of the three APIs to use — that part works, and it is the page's main job. I could not copy the example and run it, and I would not know how to get a `runtime`. - ---- - -## Page 2: `/orm/reference/raw-queries.mdx` - -### Sentences I could not restate after one reading - -**1. The warning block (line 14).** This is one paragraph of twelve sentences covering: what a type id is, which API converts values, what `.returnsRow` does, what `.returns` does not do, why `numeric` is a string, why dates are not, and then the entire MongoDB BSON story. I read it before I knew what `fns.raw`, `db.raw.sql`, `.returns()`, or `.returnsRow()` were, because the page has not introduced any of them yet. - -I could restate no part of it after one reading. It also forward-references: "which is why the `upperEmail` example below is a plain string" — I have not seen `upperEmail`. - -This should be two short warnings, each placed next to the thing it warns about: one in the PostgreSQL section, one in the MongoDB section. - -**2.** "A type id such as `pg/text@1` tells Prisma ORM how to turn the database's bytes into a JavaScript value." - -Then later: `.returns(codecId)`, `param(value, { codecId })`, `'pg/text@1'`, and "codec" in the error message. **The page calls the same thing three names — "type id", "codecId", "codec" — and never says they are the same thing.** I guessed they were, but only because the values looked identical. Pick one word. - -**3.** "On a built raw query, `.returns` is a property: a record of the columns that query declared. That is a different thing from the `.returns(codecId)` method you call on a fragment." - -This contradicts its own example. In the code, `authorsWithPosts` ends at `.returnsRow({...})` — there is **no `.build()`** — and then `authorsWithPosts.returns.postCount` is read off it. So `.returns` is a property on something that is *not* built. Either the sentence is wrong or the example is. I could not tell which. - -**4.** "`where()`'s callback returns a boolean expression, the same thing the comparison helpers such as `fns.eq(...)` return. A raw fragment declared `.returns('pg/bool@1')` is one, so it satisfies `where()` on its own, with no `fns.eq(...)` wrapper." - -"is one" — one *what*? I had to go back two clauses to find "a boolean expression." - -Plainer: "A raw fragment that declares `.returns('pg/bool@1')` is itself a boolean expression, so you can pass it straight to `where()` without wrapping it in `fns.eq(...)`." - -**5.** "`select()` has three forms, and the two used on this page are the aliased form below and the object form further down." - -This tells me a count and two names, defines neither, and points at code I have not read. It is navigation instructions, not information. I would cut it and just link `select()`. - -**6.** "TypeScript rejects an options object that names `sort` or `returnDocument`, and an untyped value that names either is dropped with no error at run time." - -Two failure modes in one sentence, and "an untyped value" is not a thing I recognise. I guessed it means "an options object typed as `any`." - -Plainer: "TypeScript rejects `sort` or `returnDocument` in the options object. If you get past the type check — say, by casting to `any` — those keys are silently ignored at run time." - -**7.** "Two of those are `_id` equality filters and `$redact` with `$$KEEP` / `$$PRUNE`." - -"Two of those" — of what? The antecedent is "anything the typed pipeline builder can't express," two sentences back and phrased as a mass noun. Plainer: "Two examples: filtering by `_id` equality, and `$redact` with `$$KEEP` / `$$PRUNE`." - -### Words and phrases I had to guess - -- **"escape hatch"** — guessed: "the way out when the normal API can't do it." Used three times and never defined. It is jargon, but survivable. -- **"the short list below"** (line 52) — guessed it meant the list of inferred types in the `param()` section, which is *four sections later* and is about something else (what Prisma picks automatically), not a list of type ids I may write. -- **"relational-core"** in `@prisma/orm-postgres/relational-core/expression` — guessed this is an internal package name I just have to type. Same for **"query-ast"** in `@prisma/orm-mongo/query-ast/execution`. Both read like directories someone forgot to hide. -- **"a raw collection with nine methods"** — guessed "an object." I counted the nine myself to check. -- **"a write yields a single result object as its one row"** — guessed: "the result comes back wrapped in an array of length 1." Only the `const [result] = ...` in the example made this click. - -### "So what do I actually type?" - -**1. Where is the list of type ids?** The page shows me `pg/text@1`, `pg/bool@1`, `pg/int8@1`, and `pg/timestamptz-temporal@1`. That last one appears at line 125 with no introduction. There is no link to a complete list. When I need `numeric`, or `jsonb`, or `uuid`, I have nowhere to look. This is the single biggest gap on the page. - -**2. `.returns()` with a column instead of a string.** "For a type id that is not in the short list below, take the type from the column instead: `db.sql.public.user.columns.id`." Do I write `.returns(db.sql.public.user.columns.id)`? The page never shows it. `.returnsRow()` takes columns, so I assume `.returns()` does too — but I am guessing at a compile error. - -**3. How do I create `db` for MongoDB?** PostgreSQL gets a full setup block: import, `postgres({...})`, `await db.connect()`. The MongoDB half of the page gets nothing. I do not know the import path, the constructor call, or the options. Half the page is unrunnable. - -**4. `db.connect()` vs `db.runtime()`.** "On PostgreSQL you get the runtime from `await db.connect()`. On MongoDB you get it from `await db.runtime()` instead." So does `db.runtime()` work on PostgreSQL or not? Does `db.connect()` exist on MongoDB? The word "instead" implies they are alternatives for the same job, which means I now have two names for one thing and no rule for choosing. - -**5. Does `execute()` return a number or an object?** Line 158: "`runtime.execute(...)`, which answers with the number of rows the statement changed." Line 170: `// stats.affectedRows`. Those are two different return types. I cannot tell whether to write `if (stats > 0)` or `if (stats.affectedRows > 0)`. - -**6. Do I interpolate the built query or the unbuilt one?** "A raw query that returns rows can go inside another one." The example interpolates `authorsWithPosts`, which stops at `.returnsRow()`. Everywhere else on the page I am told to call `.build()` before using something. So: `${authorsWithPosts}` or `${authorsWithPosts.build()}`? The example says the former; the surrounding prose implies the latter. - -**7. What do I do when I need a dynamic column name?** "Do not build a column or table name from user input with a template; `fns.raw` cannot parameterize an identifier." Correct and important — and then the page stops. I have a real query with a user-chosen sort column. The page tells me what is forbidden and offers no alternative. - -**8. Are `RawFindOneAndUpdateCommand`'s trailing arguments optional?** `new RawFindOneAndUpdateCommand('users', filter, update, true, { count: -1 }, 'after')` is six positional arguments with a bare `true` in the middle. If I only want `returnDocument: 'after'` and no sort, do I pass `undefined` for sort? `null`? The page shows one call and no signature. - -**9. `tx` is undefined.** "You can also run a built raw query inside a transaction: `tx.query(...)` and `tx.execute(...)`." Where does `tx` come from? No example, no link on that sentence. - -**10. The upsert counter contradicts the MongoDB rule.** Line 216 says "Raw filters hold native BSON values, so the examples below build ids with the driver's `ObjectId` class." Then the counter example writes `const filter = { _id: 'pageViews' }` — a plain string `_id`, no `ObjectId`. Is that allowed, or a mistake? I could not tell. (The same example also names a page-view counter `db.raw.collection('users')`, which made me reread it twice looking for the point.) - -**11. Why does `_id` filtering need `rawCommand()`?** "Filtering by `_id` works here because a real `ObjectId` in a raw pipeline document matches." But `aggregate()` also takes raw pipeline documents. So why can't I filter `_id` there? The page implies a difference and does not name it. - -### Places the page explains internals when I wanted instructions - -**1. The whole type-inference paragraph (line 86).** - -> "A whole `number` within the 32-bit signed range, `-2147483648` to `2147483647`, becomes `pg/int4@1`. A larger whole number becomes `pg/int8number@1`, up to JavaScript's safe integer limit of `9007199254740991`. Every other number, including any fractional one, becomes `pg/float8@1`. A `bigint` becomes `pg/int8@1`, a `string` becomes `pg/text@1`, a `boolean` becomes `pg/bool@1`, and a `Uint8Array` becomes `pg/bytea@1`." - -Seven mappings and two magic constants in one prose block. This is a table, not a paragraph. And what I actually came for is one sentence: *"Interpolate plain numbers, strings, booleans, bigints, and `Uint8Array` directly. For anything else — a `Date`, a decimal you need as `numeric` — wrap it in `param(value, { codecId })`."* Give me that first, then the table for when I need it. - -**2.** "Writing `FROM "user"` with no schema works because PostgreSQL resolves the name through its search path; `public."user"` is the explicit form." - -This explains PostgreSQL to me, in the middle of an example about `.returnsRow()`. I did not ask. - -**3.** "Every example is transcribed from an executable test suite that runs against a live database." - -This is about how the docs are produced. It tells me nothing I can act on. Cut it. - -**4.** "Because `returnDocument` is never sent, the driver applies its own default, which is the document as it was before the update." - -I am being told the mechanism of a limitation. What I need is the rule: "`findOneAndUpdate()` always returns the document as it was *before* the update. To get the version after, use `db.query.rawCommand(...)`." The page does say this, just after two sentences of plumbing. - -**5.** "In plain JavaScript there is nothing to reject it, so the tagged template throws as soon as you call it." - -Borderline. The error code and message are useful. The explanation of *why* TypeScript can't help is not. - -### Could I do what the page is for? - -**PostgreSQL: mostly yes.** After one reading I could write a `fns.raw` fragment in a `select()`, use one as a `where()` predicate, and run a whole `db.raw.sql` statement with `.returnsRow()`. The examples are concrete and the shapes are clear. - -What I would still not know: -- Which type id to write for any column type the page did not happen to show. -- Whether `runtime.execute()` gives me a number or an object. -- Whether to `.build()` a subquery before interpolating it. -- What to do about a dynamic column name. - -**MongoDB: no.** I cannot create the client — the page never shows how. Every MongoDB example starts from a `db` that does not exist on this page. I could read the nine method signatures and understand what each does, but I could not run one. - -**The one thing that would help most:** the page assumes I read it in order, then repeatedly forward-references ("the short list below", "the counter below shows what that costs you", "the object form further down"). Reference pages get read by jumping to a heading. Every section should stand on its own. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/raw-queries-and-index.round3.md b/docs/orm-docs-audit/c21-orm-reference/raw-queries-and-index.round3.md deleted file mode 100644 index 5e350ec1f1f..00000000000 --- a/docs/orm-docs-audit/c21-orm-reference/raw-queries-and-index.round3.md +++ /dev/null @@ -1,119 +0,0 @@ -## Page 1 — `index.mdx` - -### Sentences I could not restate after one reading - -**"It is not the same as `runtime.query()`, which runs a built query."** -Stopped me because I had no reason to confuse them yet. `db.query` and `runtime.query()` are different objects; the page warns me off a mistake before I know enough to make it, and then doesn't say what `runtime` is. Plainer: put this after the code example, as "`db.query` builds MongoDB pipelines. The separate `runtime.query()` method, below, is what runs any built query." - -**"In the example below, the ORM client runs when you `await` it. The SQL query builder and the pipeline builder do not run on their own: each produces a built query with `.build()`, which you then run with `runtime.query(...)`."** -Three ideas in one place: that awaiting triggers execution, that builders are lazy, and that `.build()` plus `runtime.query()` is the two-step. I got it on the second read only. Splitting it into two sentences would have been enough. - -**The whole of paragraph one (lines 9–11) is two unbroken walls of text.** Nine sentences with no list and no break, covering the v7 migration, three API surfaces, when to use each, raw queries, and transactions. As a reader I lost the thread at "The **pipeline builder** is at `db.query`." A bulleted list of the three builders would have been readable at a glance. - -**"Prisma ORM gives you three ways to write a query."** -Then the next paragraph adds raw queries as a fourth, and the card list shows five pages. After one reading I did not know whether "three" was wrong or whether raw is a special case. Say "three typed ways, plus raw SQL when none of them fits." - -**"The raw queries page is shorter and has fewer tables."** -I can't do anything with this. It tells me about the documentation's formatting, not about the product. - -### Words and phrases I had to guess - -- **"built query"** — guessed: an object holding the SQL and its values that hasn't been sent to the database yet. Never defined. -- **`plan`** — the code names the built query `plan`. I guessed "plan" and "built query" are the same thing. The page never connects the two words. -- **`.all()`** — guessed: "run it and give me every matching row." Never explained. This matters because the page's whole framing is "in v7 you wrote `findMany({ where, include })`" — so I immediately want to know what replaced `findMany`, and `.all()` is the answer, silently. -- **`runtime`** — guessed: a connection object. `db.runtime()` appears in the code with no sentence introducing it. -- **"aggregate over a join"** — fine, I know SQL. -- **"Options, Return type"** as section names — fine. - -### "So what do I actually type?" - -- **`findMany` has no stated replacement.** The migration sentence promises the mapping is elsewhere, but the one example on this page uses `.all()`, which isn't one of the arguments being mapped. I'd have to leave the page to learn the most basic thing. -- **`db.runtime()` — do I call it once per app, once per query, or once per request?** The code makes it a top-level `const` and says nothing. -- **Nothing shows the pipeline builder** even though it's one of the three headline APIs. The example covers the ORM client and the SQL query builder only. - -### Internals where I only wanted to know what to do - -Nothing serious. "each produces a built query with `.build()`" is mechanism, but it's mechanism I have to type, so it earns its place. - -### Could I do what the page is for? - -Yes, mostly. It's an index, and I'd know which of the five pages to click. I would still not know what `.all()` is, what `runtime` is or when to create it, or why `db.query` and `runtime.query()` needed a warning. - ---- - -## Page 2 — `raw-queries.mdx` - -### Sentences I could not restate after one reading - -**"This page covers two of these ways out."** -"These" points backwards to a sentence that listed three *typed* APIs — the opposite of ways out. I re-read the previous paragraph looking for a list of ways out that isn't there. Plainer: "There are two kinds of raw query." - -**"A type id (`codecId` in the code) such as `pg/text@1` says how to turn the database's bytes into a JavaScript value."** -Two names for one thing in the first eight words, and then bytes, which I did not ask about. I wanted to know which string to type. Plainer: "A type id such as `pg/text@1` tells Prisma what JavaScript type a column should become. In code, the property is called `codecId`." - -**"A `numeric` column arrives as a string, and a date arrives as the driver's own value."** -"the driver's own value" is not a type. After one reading I do not know what variable type I will get for a date, so I cannot write the code that consumes it. Say the actual type. - -**"There is no way to send a column name or a table name as a parameter, so never build one from user input with a template."** -"build one" — build *what*? A column name? A template? A query? I had to reconstruct it. Plainer: "Never put user input into the SQL text itself, because a column or table name cannot be a parameter." - -**The CTE paragraph (line 170) — the worst on the page.** Seven sentences and roughly six separate rules: - -> "Interpolate the object that `.returnsRow(spec)` gives you, before you call `.build()` on it, and its SQL and its values are put into the outer statement in order. Only the outer statement is built. The outer statement declares its own columns. For a column the inner query already declared, read it back from the inner object's `.returns` property, which is a record of the columns that query declared, rather than writing the type id out again. That property is a different thing from the `.returns(typeId)` method you call on a fragment." - -I could not restate this after one reading. The hardest part is the last sentence: I now have `.returns()` the method, `.returns` the property, and `.returnsRow()` the method, and the page tells me they differ without giving me a way to keep them straight. "its SQL and its values are put into the outer statement in order" is also internals — I only need to know that it works. - -**"Pass the built query to `runtime.execute(...)`, which resolves to an object, `{ affectedRows }`, whose `affectedRows` is the number of rows the statement changed."** -The sentence names `affectedRows` twice and circles back on itself. Plainer: "`runtime.execute(...)` resolves to `{ affectedRows }` — how many rows the statement changed." - -**"Each entry is either a column taken from your contract, such as `db.sql.public.user.columns.id`, which brings its own conversion, nullability, and TypeScript type, or an explicit type id for a column your contract has no counterpart for."** -One sentence, two branches, four nouns in a subordinate clause, and "a column your contract has no counterpart for" is backwards English. Split it in two. - -**"Every one of these methods goes through `query()`, including the writes."** -"Goes through" is vague. I guessed it means "you run all of them with `runtime.query()`, even the inserts and deletes." Say that. - -### Words and phrases I had to guess - -- **"`codecId`"** — guessed it's simply the code's name for a type id. Confirmed by the parenthetical, but the page then uses both names interchangeably for the rest of the page and I had to keep translating. -- **`@@map`** — "unless the model sets `@@map`" — guessed: a contract annotation that overrides the table name. Never explained, and I'm coming from v7 where the file was `schema.prisma`. -- **`pg/int8number@1`** — guessed: a 64-bit integer that comes back as a JavaScript `number` rather than a `bigint`. Nothing on the page says so, and it matters, because the `.returnsRow` example later uses `pg/int8@1` and the comment says "row.postCount is a bigint". So the two spellings do different things, and I worked that out by inference. -- **"raw fragment"** vs **"whole raw statement"** — the definitions are clear. No complaint. -- **"tagged template"** — assumed knowledge, fair for a JS developer. -- **"native BSON values"** — guessed: whatever the MongoDB driver hands back, unconverted. -- **"the driver"** — guessed: the `mongodb` / `pg` npm package underneath. Never introduced. - -### "So what do I actually type?" - -- **Where is the full list of type ids?** The page gives seven auto-assigned ones plus "four more you are likely to write", then says "Those are the common ones, not the whole set." It never says where the whole set is. I have a `date` column, an enum, and a `text[]`. I am stuck, and there is no link to follow. -- **The link text lies.** "See the [type ids](#binding-a-bare-value-with-param) for the ones you are most likely to write" — the anchor lands on a section titled "Binding a bare value with `param()`". I'd assume I clicked the wrong link. -- **How do I bind a `Date`?** The page tells me to three separate times — "such as a `Date`", "To bind a `Date`, or any value whose type is not in the list, wrap it in `param(value, { codecId })`" — and never once shows it. The only `param()` example is `param(15, { codecId: 'pg/int4@1' })`, a plain number that the table above says is bound automatically. So the single worked example demonstrates the one case where `param()` is unnecessary, and the case it was written for is missing. -- **`.select((f) => ({ id: f.id, serverNow }))`** — this object-returning callback form appears with no introduction. Earlier the page showed `select('id')` and `select('name', callback)`. This is a third shape, used once, unexplained. -- **`db.query.rawCommand(command)` is passed to `runtime.query()` with no `.build()`.** Line 205 told me flatly: "Call `.build()` on it, then run it with `runtime.query(built)`." Then line 306 and line 340 don't call `.build()`. I would have typed `.build()`, got an error, and not known why. -- **How do I compare an `ObjectId`?** The warning says "compare one with `String(...)`", and the only demonstration is inside a code comment. I'd want `String(row._id) === aliceId` in actual code. -- **`$redact` with `$$KEEP` / `$$PRUNE`** is named as one of the two reasons to use `rawCommand()` and never shown. -- **`db.query.rawCommand` under a MongoDB heading, but its own `##` section.** After one reading I could not tell whether `rawCommand` is part of the raw API or the pipeline builder. The page says it "runs ... through the pipeline builder" and then sends me to the pipeline builder page "for the full treatment" — so which page is authoritative? - -### Contradictions I hit - -- **`aggregate()` vs `rawCommand()` on `_id` matching.** Line 226: `aggregate` sends "stages ... as you wrote them, so a filter on `_id` with a real `ObjectId` matches." Line 345: `rawCommand()` is "the way out when the typed pipeline builder can't express a query. Two examples: filtering by `_id` equality". Both claim to be the answer for `_id` filtering. After one reading I do not know which one to use, or whether the second sentence means "the *typed* builder can't, but `aggregate` also can" — in which case `rawCommand` has no advantage over `aggregate()`, which at least gives me types. -- **Update result shape.** The warning box (line 208) says the write methods give you `{ matchedCount, modifiedCount }`. Line 251 says `updateOne()`/`updateMany()` return `{ matchedCount, modifiedCount, upsertedCount, upsertedId }`. Two different answers, ten lines apart. - -### Internals where I only wanted to know what to do - -- **"says how to turn the database's bytes into a JavaScript value"** — I don't care about bytes. Tell me which string to type and what I get back. -- **"its SQL and its values are put into the outer statement in order. Only the outer statement is built."** — this is how the composition is implemented. I just want "you can nest a `.returnsRow()` query inside another one; call `.build()` on the outer one only." -- **"MongoDB has no schema segment, which is why there is no `public` here and none on `db.orm`."** — the "which is why" is an explanation of the API's shape. "MongoDB has no schemas, so there is no `public` segment" would do. -- **"Every one of these methods goes through `query()`, including the writes. A write comes back as an array holding one result object, which is why the examples read it with `const [result] = ...`."** — the "which is why" again explains the plumbing. The actionable part is only the second half: "A write returns a one-element array, so destructure it." -- **"The **first** call to an upsert counter returns an **empty** result. The upsert creates a brand-new document, so there is no earlier version to return, the driver returns `null`, and the query yields no row."** — a four-step causal chain about driver behaviour. The fact I need is the first sentence. The rest is why, and I'd have accepted "because there is no earlier version." - -### Other things that stopped me - -- **The upsert counter example uses the `users` collection as a page-view counter, with `_id: 'pageViews'`.** The page admits this: "The example schema has no counter collection, so `users` stands in for one here." Reading it, I spent time working out whether a users collection really was supposed to hold a counter document. Then a further sentence explains that a string `_id` is legal. Two sentences of apology around a four-line example. -- **`new RawFindOneAndUpdateCommand('users', { _id: ... }, { $inc: ... }, true, { count: -1 }, 'after')`** — six positional arguments. The list above it is clear, but reading the call itself, `true` means nothing without scrolling back up. -- **The whole PostgreSQL half assumes I've read the SQL query builder page**, and the whole MongoDB half assumes I've read the pipeline builder page, for the example schemas. Fine for a reference, but it means I can't run a single example on this page without opening two others. - -### Could I do what the page is for? - -**PostgreSQL: partly.** I could write a `fns.raw` fragment in a `select()` or a `where()`, and I could write a whole `db.raw.sql` statement with `.returnsRow()` or `.affectedCount()` — the examples are close enough to copy. I would still not know: how to bind a `Date` (the thing the page tells me three times to use `param()` for), where the full list of type ids lives, when `pg/int8@1` versus `pg/int8number@1` applies, or what type a date column actually arrives as. The CTE section I would have to read three more times. - -**MongoDB: partly, and I'd guess wrong at least once.** The nine collection methods are clear and copyable. But I would not know whether to use `aggregate()` or `rawCommand()`, because the page gives `_id` filtering as the reason for `rawCommand` and also says `aggregate` handles it. I would call `.build()` on a `rawCommand` because the page told me to, and it would break. And I'd have to pick one of the two conflicting answers about what `updateOne()` returns. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/raw-queries-and-index.round4.md b/docs/orm-docs-audit/c21-orm-reference/raw-queries-and-index.round4.md deleted file mode 100644 index ec9aef8ead1..00000000000 --- a/docs/orm-docs-audit/c21-orm-reference/raw-queries-and-index.round4.md +++ /dev/null @@ -1,146 +0,0 @@ -I read both pages once, top to bottom, as a Prisma ORM 7 user who has never seen version 8. - -# Page 1 — `index.mdx` (reference landing page) - -## Sentences I could not restate after one reading - -**1. The whole of line 15.** This is one paragraph carrying eight separate ideas: - -> "Plus raw SQL and raw commands when none of them fits. See [Raw queries]... For connecting and disconnecting the client, transactions, and prepared statements, see [Transactions and runtime]... In the example below, `db.runtime()` returns the connection that runs a built query. Create it once and reuse it. `.all()` runs the query and returns every row. It is what replaced `findMany`. The ORM client runs when you `await` it. The SQL query builder and the pipeline builder do not run on their own. Each produces a built query with `.build()`, which you then run with `runtime.query(...)`. Watch the casing: `User` is the model name and `user` is the table name." - -What stopped me: two link paragraphs, a definition of `runtime`, a definition of `.all()`, a migration note, an execution-model rule, and a naming warning are all in one block with no headings. I had to re-read it three times to find the one rule that matters, which is "the ORM client runs on `await`, the two builders need `.build()` then `runtime.query()`." - -Plainer: split it. One short paragraph for "when to use raw queries and transactions, with links." Then a small section titled "How a query runs" containing only: the ORM client runs when you `await` it; the SQL query builder and pipeline builder return a built query from `.build()`, which you run with `runtime.query(...)`; get `runtime` from `db.runtime()` once and reuse it. Then a one-line callout for the casing rule. - -**2.** > "it returns results that are not model rows, such as an aggregate over a join." - -"Model rows" is not defined anywhere on this page. I guessed it means "objects shaped like your model, the kind `db.orm` gives back." Plainer: "it returns whatever shape your `select()` asks for, not objects shaped like your models." - -**3.** > "`db.query` builds MongoDB pipelines. The separate `runtime.query()` method is what runs any built query." - -The word "separate" told me these two are being confused on purpose, but it did not tell me why I should care. I could not restate what problem this sentence is solving. Plainer: "Two different things are both named `query`. `db.query` builds a MongoDB pipeline. `runtime.query(...)` runs a query that has already been built." - -## Words and phrases I had to guess - -- **"built query"** — used three times before anything defines it. I guessed it means a plain object describing the query that has not been sent to the database yet. -- **`plan`** — the code names the variable `plan`, but the prose only ever says "built query." I guessed they are the same thing. The page never says so. -- **"the connection that runs a built query"** — is `runtime` a database connection, a connection pool, or a wrapper? "Create it once and reuse it" implies it is expensive, which implies a pool. I guessed pool. -- **`(u) => u.kind.eq('admin')` versus `(f, fns) => fns.eq(f.kind, 'admin')`** — two callback styles appear on consecutive lines in the same code block, and the page does not mention that they differ. I guessed the ORM client puts the comparison on the column and the SQL builder puts it in a helper object. I would not have been able to write either one from memory. -- **"typed"** in "three typed ways to write a query" — I guessed it means TypeScript checks the column names and result types. - -## Places I asked "so what do I actually type?" - -- **MongoDB.** The page says the SQL query builder is "PostgreSQL only" and that `db.query` "builds MongoDB aggregation pipelines," but the only code block is PostgreSQL. `const runtime = db.runtime();` is shown with no `await`. On MongoDB that line is wrong — I only found that out on the raw-queries page, at line 25. If I were a MongoDB user, I would have copied the broken line from the landing page. -- **Is `db.query` available on PostgreSQL?** The page says what it builds. It never says it does not exist on PostgreSQL, the way it does for `db.sql`. I do not know what happens if I type it. -- **`runtime.execute`** is never mentioned. The landing page implies `runtime.query(...)` runs everything. The raw-queries page then reveals a second method for statements that return no rows. The landing page's model of "how a query runs" is incomplete, and I did not know that until the next page. -- **`.all()`.** It is introduced as what replaced `findMany`. Do the SQL query builder and pipeline builder have `.all()` too? The example uses `.build()` for those instead, so I guessed no, but the page does not say. - -## Places it explains internals when I wanted instructions - -Only one, and it is mild: "The SQL query builder and the pipeline builder do not run on their own." That is a fact about the design. The instruction I needed is "call `.build()`, then `runtime.query(...)`," which arrives in the same sentence but second. - -## Could I do what the page is for, after one reading? - -Mostly. Its job is to send me to the right sub-page, and I can do that: everyday reads and writes go to `db.orm`, PostgreSQL joins and aggregates to `db.sql`, MongoDB pipelines to `db.query`. What I would still not know: how to get a `runtime` on MongoDB, that `runtime.execute` exists, and how to write either callback style without copying the example. - -# Page 2 — `raw-queries.mdx` - -## Sentences I could not restate after one reading - -**1.** > "A type id such as `pg/text@1` tells Prisma ORM what JavaScript type a column should become. In code, the property is called `codecId`." - -Two names for one thing, introduced in consecutive sentences, with no reason given. I still do not know what a "codec" is here or why the property is not called `typeId`. Plainer: pick one name and use it everywhere; if the property really is `codecId`, say "the property is named `codecId` for historical reasons" or just call the thing a codec id throughout. - -**2.** > "A date column arrives as whatever the `pg` package produces for it, with no conversion by Prisma ORM." - -This is the sentence I most needed an answer from, and it refuses to give one. I do not know what I get. A `Date`? A string? The page tells me to go read another project's documentation to find out. Plainer: say the actual type, for example "a `timestamptz` column arrives as a JavaScript `Date` in local time." - -**3.** > "Never put user input into the SQL text itself, because a column or table name cannot be a parameter." - -The reason does not support the rule. User input should never go into SQL text because of SQL injection. The fact that identifiers cannot be parameters is a *different* fact, and it is the reason you might be tempted to. Reading this once, I thought the page was saying user input is fine as long as it is not a column name. Plainer: "Interpolated values are sent as query parameters, so they are safe. Column and table names cannot be parameters, so they would be pasted into the SQL text. Never build an identifier from user input." - -**4.** > "A type id is the PostgreSQL type name plus a version, always `@1` today." - -This contradicts the table directly above it. `pg/int8number@1` and `pg/timestamptz-temporal@1` are not PostgreSQL type names. I could not restate the rule because the rule is wrong. Plainer: "A type id is `pg/`, a name, and a version. The name is usually the PostgreSQL type, sometimes with a suffix saying which JavaScript type you get back." - -**5.** > "You can nest a `.returnsRow()` query inside another. That is how you write a subquery or a CTE. Interpolate it before `.build()`, and call `.build()` on the outer one only. Its `.returns` property holds its declared columns, so `outer.returnsRow({ postCount: inner.returns.postCount })` reuses a declaration. Three names look alike. `.returns(typeId)` is a method on a fragment, `.returnsRow(spec)` is a method on a statement, and `.returns` is a property on the object `.returnsRow()` gives you. You cannot nest a statement that ends with `.affectedCount()`, because it produces no rows. You can nest an `UPDATE ... RETURNING`, because it does." - -Six unrelated rules in one paragraph. The "three names look alike" list is genuinely useful and is buried in the middle of it. "CTE" is never expanded. Plainer: make the three-names list its own callout, and give nesting its own short paragraph. - -**6.** > "Each of these nine methods builds a command. Call `.build()` on it, then run it with `runtime.query(built)`, the inserts, updates, and deletes included." - -The trailing clause reads like an afterthought and carries the surprise. On PostgreSQL the page just told me that statements returning no rows go through `runtime.execute(...)`. Here, deletes go through `runtime.query(...)`. Nothing explains why the rule flips. Plainer: "Run every one of these with `runtime.query(...)`, including the writes. `runtime.execute(...)` is PostgreSQL only." - -**7.** > "The write methods give you its own result objects, `{ insertedId }`, ... and you read those keys directly." - -"its" points back to "the `mongodb` package" two sentences earlier. On one reading I read "its" as pointing to "the write methods" and stalled. - -**8.** > "The **first** call to an upsert counter returns an **empty** result, because there is no earlier version. Read the counter value on the second call and later ones, each of which returns the version before its own increment." - -I had to read this three times. The behavior is simple but the wording is not. Plainer: "`findOneAndUpdate` returns the document as it was *before* your update. The first call creates the document, so there is nothing to return and you get `[]`. The second call returns `count: 1`, which is the value before that call added one." - -Also, the example undercuts itself: > "The example schema has no counter collection, so `users` stands in for one here, with the plain string `'pageViews'` as its `_id`." The warning two sections earlier told me to build ids with `ObjectId`. Now the example uses a plain string as `_id` with no comment. I could not tell whether that was allowed on purpose or an artifact of the stand-in. - -## Words and phrases I had to guess - -- **"spec"** in `.returnsRow(spec).build()` (line 19). Introduced with no definition. It is defined at line 136, over 100 lines later. On first reading I guessed it was a schema object. -- **"projection"** in the heading "`fns.raw` in a projection". Never used again on the page. I guessed it means the `select()` list. -- **`Contract`, `contract.d`, `contract.json`** — the code imports all three. I know what a contract is, but not that it compiles into two generated files, nor where they come from. I guessed a code-generation step I have not been shown. -- **`Temporal.Instant`** — described as "the standard JavaScript object for a point in time." It is used at line 99 with no import and no note about runtime support. I guessed my Node version has it. That guess may well be wrong, and the page gives me nothing to check against. -- **"codec"** — see above. Guessed: the thing that converts between a PostgreSQL wire value and a JavaScript value. -- **"CTE"** — I know it, but it is never expanded, and it is the only unexpanded acronym on the page. -- **"native BSON values"** — guessed: the raw objects the MongoDB driver produces, without any Prisma conversion. -- **`pg/int8number@1` versus `pg/int8@1`** — one reads back as `number`, the other as `bigint`. I guessed I choose based on whether I want to risk precision loss. The page never says when to pick which. - -## Places I asked "so what do I actually type?" - -1. **Finding a type id for a column that has no contract match.** > "The ids your contract uses are in `contract.json`." Where in it? What key? There is no path, no example fragment, no shape. This is the single most likely thing I will need, and the page points at a file and walks away. `db.sql.public.
.columns..codecId` is given for columns I *do* have, which is the easier case. - -2. **`Temporal`.** Do I import it? Enable a flag? Install a polyfill? The `param` and `ObjectId` imports are both spelled out. `Temporal` is not. - -3. **Reading documents raw on MongoDB.** There are nine methods and none of them is `find` or `findOne`. I wanted a plain raw read. I eventually worked out that `aggregate()` with a `$match` is the answer, but the page never says "there is no raw `find`; use `aggregate`." - -4. **What happens when a raw query fails.** The page documents exactly one error, `RUNTIME.RAW_SQL_UNSUPPORTED_INTERPOLATION`. Nothing about a SQL syntax error, a constraint violation, or a `.returnsRow()` spec that does not match the real columns. If my spec is wrong, do I get a run-time error or silently wrong values? I do not know. - -5. **Transactions on MongoDB.** PostgreSQL gets `tx.query(...)` and `tx.execute(...)` at line 157. The MongoDB half says nothing. I do not know whether raw MongoDB commands can run inside a transaction. - -6. **`.returnsRow()` when a column can be null.** Fragments get `{ codecId, nullable }` with its own section. For a whole statement's spec, entries are "either a column from your contract or a type id." A bare type id has no nullability. What do I type for a nullable column with no contract match, such as a `LEFT JOIN` column? Not covered. - -## Places it explains internals when I only wanted to know what to do - -1. > "If you get past the type check, say by casting the object to `any`, those keys are ignored at run time with no error." (line 289) - -This tells me how the implementation behaves when I defeat its own type checking. I did not want to know. The instruction I wanted was the next sentence: use `RawFindOneAndUpdateCommand`. - -2. > "runs a raw MongoDB aggregate command through the [pipeline builder], with no translation of the command you pass." (line 335) - -"Through the pipeline builder, with no translation" is about plumbing. What I need is the three practical facts, which are in the *rest* of the paragraph: it is already built so do not call `.build()`, rows come back as `unknown`, and you cast them. - -3. **`RawFindOneAndUpdateCommand` in general.** Being sent to `@prisma/orm-mongo/query-ast/execution` to hand-build a command object with six positional arguments, because the friendly method deliberately refuses `sort` and `returnDocument`, is the tool's internal shape showing through. The page even needs a `// collection, filter, update, upsert, sort, returnDocument` comment above the call so I can read my own code. I do not know whether that import path is supported or will break on the next release. - -4. > "`.returns(...)` only tells TypeScript what to expect. It does not cast the value in SQL and it does not change how the `pg` package reads it." - -Three sentences about what a method does *not* do, and no sentence about what to type when I actually want the conversion. The answer, use `.returnsRow()` instead, is somewhere else. - -5. > "`rawCommand()` is the way out when the typed pipeline builder can't express a query, such as filtering by `_id` equality." - -A significant limitation of a different API is disclosed in a subordinate clause on this page. If the pipeline builder cannot filter by `_id` equality, I want to know that in bold, on the pipeline builder page, not as an example here. - -## Could I do what the page is for, after one reading? - -**PostgreSQL: yes, for the common cases.** I could write a `fns.raw` fragment in a `select()`, use one as a `where()` predicate, and run a whole `db.raw.sql` statement with `.returnsRow()`. The examples are complete and I could copy them. - -**What I would still not know on PostgreSQL:** -- What JavaScript value I actually get for a date, a `numeric`, or a `json` column from a fragment's `.returns()`. The page tells me it does not convert and names no types. -- How to find a type id for a column my contract has no match for. I was pointed at `contract.json` with no instructions. -- Whether `Temporal` works in my runtime or needs anything installed. -- What happens when a `.returnsRow()` spec does not match the real result columns. -- How to declare a nullable column inside a `.returnsRow()` spec. - -**MongoDB: partly.** I could copy the insert, update, and delete examples and they would probably work. But I would not know: -- That there is no raw `find`, and that `aggregate` is the substitute. -- Why writes go through `runtime.query` here when they go through `runtime.execute` on PostgreSQL. -- Whether raw commands work inside a transaction. -- Whether a plain string `_id` is genuinely allowed, or just an artifact of the counter example borrowing the `users` collection. - -The two biggest structural problems, in order: the type-id system is introduced through its restrictions and its two names rather than through a straight answer to "what type do I get back," and the landing page's line 15 buries the one execution rule a reader needs under seven other ideas. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part1.round1.md b/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part1.round1.md deleted file mode 100644 index a19d2071c64..00000000000 --- a/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part1.round1.md +++ /dev/null @@ -1,81 +0,0 @@ -## Sentences I could not restate after one reading - -**Line 11** — "Where a method works only on some databases, or has a caveat that only shows up when the query runs, the Remarks call that out." -"the Remarks" arrives capitalised, as if I already know it. I don't meet a Remarks section until line 337. Plainer: "Some methods only work on some databases, or fail only when the query actually runs. Each method's **Remarks** section says so." - -**Line 131** — "You build queries from the client's `sql` property, which knows your contract and how your queries are run." -I do not know what "your contract" is. In Prisma 7 I had a schema and a generated client. The word never gets defined, but `contract.json` and `contract.d` show up in the very next code block as things I am supposed to already have. Plainer: say what a contract is and which command produces those two files, or link to the page that does. - -**Line 135** — "Create the client with `postgres(...)`, take a runtime from `db.runtime()`, and build queries off `db.sql.public`. On PostgreSQL `db.runtime()` is synchronous, so there is nothing to await." -Three new nouns in one breath: client, runtime, and `public`. And the second sentence implies runtimes are sometimes asynchronous without saying when or what I'd do then. I could not restate what a "runtime" *is* — only that I need one to run a query. - -**Line 149** — "`db.sql` holds the result of an internal `sql({ context, rawCodecInferer })` call, connected to the client's way of running queries and to the adapter's rule for how values are stored and read." -This defeated me. "internal", "context", "rawCodecInferer", "adapter", "rule for how values are stored and read" — five unknowns, one sentence, and it describes the inside of the tool. I wanted to know what to type. - -**Line 149, second half** — "a `rawCodecInferer` (a fallback rule for how to store and read raw expressions that have no column to take that from)" -"that have no column to take that from" — I read this three times. Take *what* from a column? I think it means: a raw SQL expression has no column, so the builder cannot work out its type, and this supplies the default. If that is the meaning, say that. - -**Line 199** — "Each name resolves against the current scope; an unknown name throws an error whose `code` is `ORM.COLUMN_UNKNOWN`." -"the current scope" is never defined. I only work out roughly what it means 140 lines later, from the `lateralJoin` remark about ambiguous names being dropped. Also: does it throw when I call `select()`, when I call `build()`, or when the query runs? The page draws that distinction at line 11 and then never tells me which side this falls on. - -**Line 341** — "`.as(...)` returns the wrong shape (a bare join source), which throws `subquery.getRowFields is not a function`." -This is an internal JavaScript error message pasted into a reference page. I can match it if I see it, but "bare join source" versus the join source `.as()` is documented to return at line 528 is a contradiction I cannot resolve. At 528 `.as()` produces exactly what joins want. Here the same value is "the wrong shape". Which is it? - -**Line 342** — "the merged scope **drops** the ambiguous names." -I had to guess that "merged scope" means the combined set of column names from both sides of the join. The sentence describes an internal bookkeeping step; what I need is the rule: after a lateral join, if both tables have a column of the same name, you must write `f.post.id`, never `f.id`. - -## Words and phrases I had to guess - -- **"contract"** — guessed: the new name for the generated schema types. Guessed `contract.json` and `contract.d` are generated by some CLI command. -- **"runtime"** — guessed: the thing that holds the database connection and executes queries. -- **"plan"** (the variable name, used in every example) — guessed: the compiled query object that `build()` hands to `runtime.query()`. The page never once says what `build()` returns or why it is called a plan. -- **"escape hatch"** (line 9) — guessed: a way out of the normal API for cases it doesn't cover. -- **"facet"** — appears only in the anchor `#the-dbsql-facet` (line 133) while the heading says "builder". Two names for one thing; I don't know if a facet is a distinct concept. -- **"function bag `fns`"** (line 187) — guessed: an object full of helper functions. "Bag" is not a word I've seen used this way. -- **"codec"** — never used in prose, but `'pg/text@1'` and `'pg/int4@1'` appear five times and `rawCodecInferer` names it. Guessed: a type tag telling Prisma how to encode and decode a Postgres value. -- **`Uuid`**, **`@@type("pg/text@1")`**, **`@@discriminator(type)`**, **`@@base(Task, "bug")`**, **`types { Embedding1536 = pgvector.Vector(1536) }`** — all new to me since v7. All guessed. The schema is presented as background, but it is the least familiar thing on the page. -- **"adapter"** (lines 149, 339) — guessed: the per-database driver. -- **"join source"** (lines 170, 528) — guessed: anything you're allowed to put on the right of a join. -- **"derived table"** (line 341, 348) — guessed: the SQL term for a subquery used as a table. - -## Places I asked "so what do I actually type?" - -1. **Where do `contract.json` and `contract.d` come from?** Every example depends on them. No command, no link, nothing. This is the first thing that blocks me from running a single line on the page. "This is a reference page, the project already exists" does not cover it — I have a v7 project and no contract. -2. **SQLite.** Line 14 says SQLite is supported. Every example is `postgres(...)` and `db.sql.public`. What do I import for SQLite? What replaces `public`, given SQLite has no schemas? The page raises SQLite four times (lines 14, 339, 453) and never shows one line of SQLite code. -3. **The codec strings.** `.returns('pg/int4@1')` is required for every raw expression, and there is no list of valid values and no link to one. I can copy `pg/text@1` and `pg/int4@1`. For a `numeric`, a `timestamptz`, a `bool`, I am stuck. -4. **`context`** in the line 154 example. The code is `sql({ context, ... })` with `context` appearing from nowhere. "Pass your `context` (from your database client)" — which client? I just created the client with `postgres(...)`, and that one has `db.sql` already. If this path is for people not using `postgres()`, I have no idea what object to pass. -5. **`limit(n)`** — "`number`, or a numeric expression" (line 491). No example of the expression form. What would I write? -6. **Does `distinctOn()` have to come after `orderBy()`?** The example puts it last (line 477), the prose says "according to the query's `orderBy()`" and "Pair it with an `orderBy()`", but never says whether call order matters. I would have to experiment. -7. **`.as()` on non-SELECT queries.** Line 516 says "a completed `SELECT` query". Is there a way to use an INSERT's returned rows as a source? Silence — possibly answered in part 2. -8. **`orderBy()` twice** — line 396 says calling it again adds secondary keys. Does calling `select()` twice add columns or replace them? Line 226 chains two `select()` calls and the result seems additive, but the prose at line 200 says "Add one computed column" only for the aliased form. For the column-name form at line 199 it says "Keep the named columns" — so a second call presumably replaces. Unclear. - -## Places that explain the inside when I wanted the outside - -- **Line 149**, the whole `sql({ context, rawCodecInferer })` paragraph. It tells me what `db.sql` *holds internally* and what it is *connected to*. I am reading a reference to write queries. This belongs somewhere else, or reduced to one sentence: "If you are wrapping your own database client instead of using `postgres()`, call `sql(...)` yourself — see X." -- **Line 135** — "On PostgreSQL `db.runtime()` is synchronous, so there is nothing to await." The useful half is "don't await it". The reason is implementation. -- **Line 339 and 453** — "TypeScript types `lateralJoin` as `never`, so the call does not compile". How the restriction is implemented. What I need: "Not available on SQLite; the call won't compile." -- **Line 341** — "`lateralJoin()`'s own first argument already names the derived table" plus the raw `subquery.getRowFields is not a function` error. The instruction "return the chain, don't call `.as()`" is enough. - -## A specific problem, not a comprehension one - -**Line 390** — "`options.nulls` | `'first' \| 'last'` | No | TypeScript accepts it, but nothing reads it: no `NULLS FIRST` or `NULLS LAST` reaches the SQL, so the database's own null ordering applies." - -Then **line 417** gives it a worked example titled "Control direction and null placement", whose comment says `// nulls adds nothing to the SQL`. The page documents a broken option and then demonstrates it. As a reader I assume the option works and I misread the caveat, or that the docs are papering over a bug. Either delete the option from the table and the example, or say plainly "`nulls` is currently ignored; do not use it." - -**Lines 135 and 161** are word-for-word identical from "A `select()`, `insert()`..." onward. On second reading I lost my place, thinking I had scrolled backwards. - -**Line 19** promises "The select and mutation examples use this `user` / `post` / `post_tag` / `tag` schema". The expanded schema also contains `Address`, `Task`, `Bug`, `Feature`, `user_type`, `Priority` and `Embedding1536`. Nothing in part 1 uses `Address`, `Task`, `Bug` or `Feature`, and they are the most unfamiliar syntax on the page. They made me stop and try to decode `@@base` and `@@discriminator` for no payoff. - -## Could I do what the page is for, after one reading? - -Partly. I could write a `select` with a `where`, an `innerJoin`, an `orderBy`, a `limit`, and a subquery join — the SQL concepts are ones I know and the examples are concrete and complete. The three-form table for `select()` (line 197) and the join examples are the strongest parts. - -What I still would not know: - -1. **How to get to line 1.** I cannot produce `contract.json` or `contract.d`, so I cannot run any example. That is a hard stop, and it is the page's own opening code block. -2. **What a "contract" or a "runtime" is**, beyond that I need both. -3. **How to use any of this on SQLite**, despite being told twice it is supported. -4. **Any raw expression whose type isn't text or int4**, because there is no codec list. -5. **When errors surface** — at `build()` or at `runtime.query()`. The page flags the distinction as important and then never applies it to `ORM.COLUMN_UNKNOWN` or `ORM.CAPABILITY_MISSING`. -6. **Whether `select()` accumulates or replaces** when called twice with column names. -7. **Whether the schema's new syntax matters to me.** `Uuid`, `@@type`, `@@discriminator`, `@@base` and the `types { }` block are all new since v7 and all unexplained. I do not know if I must adopt them to use `db.sql`, or if they are incidental to the examples. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part1.round2.md b/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part1.round2.md deleted file mode 100644 index 1246a2ea9e6..00000000000 --- a/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part1.round2.md +++ /dev/null @@ -1,97 +0,0 @@ -Read once, top to bottom, as a Prisma 7 user. - -## Sentences I could not restate after one reading - -**1. Line 131 (the whole "Entry points" paragraph).** It is one block of ~10 sentences that switches topic four times: how to build a query, what a contract is, what `prisma contract emit` produces, how table names are derived, and what `public` means. Quoting the worst of it: - -> "The client reads your contract, the `contract.prisma` file that replaced `schema.prisma`. Running `npx prisma contract emit` compiles the contract into the two files every example here imports: `contract.json`, read when your app runs, and `contract.d.ts`, read by TypeScript." - -What stopped me: "contract" is a brand-new word introduced mid-paragraph with no heading of its own, and the sentence explains what each file is *for internally* when all I need is "run this command before you build queries." Plainer: "Before you can build queries, run `npx prisma contract emit`. It writes `contract.json` and `contract.d.ts`, which the examples below import." - -**2. Line 131, last sentence.** - -> "`public` is the PostgreSQL schema, the namespace your tables live in unless you set another one." - -I can restate it, but "unless you set another one" is a dead end — the page never says where you set it or what you write instead. Plainer: name the frontmatter/schema setting, or drop the clause. - -**3. Line 135.** - -> "`db.runtime()` is the connection that runs a built query. On PostgreSQL it is synchronous, so do not await it." - -Two ideas, and the second is about the tool's internals. Then three lines later the examples `await runtime.query(plan)`. After one reading I was unsure whether "do not await" applied to `query()` too. Plainer: "`db.runtime()` returns the connection. Do not `await` it — only `runtime.query()` is awaited." - -**4. Lines 149–155.** - -> "The `sql(...)` function from `@prisma/orm-postgres/builder/runtime` builds the same query builder around your own database client instead, from that client's `db.context`" - -Three unexplained things in one sentence: what "your own database client" means when the example still passes `db.context` (so it *is* the Prisma client?), what `db.context` is, and `rawCodecInferer: { inferCodec: () => 'pg/text@1' }` — which appears in code and is never mentioned in prose at all. I could not restate any of this. I would cut the whole block or give it a section that says who needs it and why. - -**5. Line 187–189.** - -> "The first is `f`, which has one property per column the query can name at that point." - -"at that point" is doing a lot of work and the thing it refers to ("scope") is defined in the *next* paragraph. Plainer: put line 189 first, then define `f` in terms of it. - -**6. Line 207.** - -> "A computed expression's result type comes from `.returns(...)` on `fns.raw`, or from the operation's own declared return type. A type id such as `pg/int4@1` is the PostgreSQL type plus a version, always `@1` today. [Raw queries] shows the ones a bare JavaScript value gets, and you can take one from a column: `db.sql.public.user.columns.id`." - -Three sentences, four ideas, and the third one I genuinely could not parse. "shows the ones a bare JavaScript value gets" — the ones *what*? Type ids, presumably, but "a bare JavaScript value gets" a type id is backwards from how I think about it. And "you can take one from a column" — take a *type id* from a column? Does `db.sql.public.user.columns.id` evaluate to the string `'pg/uuid@1'`, or to something else I pass to `.returns()`? Unresolved. - -**7. Line 346.** - -> "After the join, a column name that both tables have must be written under its table, such as `f.post.id` or `f.post.createdAt`. A bare `select('id')` or `orderBy((f) => f.createdAt, ...)` throws, because a name both tables have is not in the query's scope on its own." - -The second sentence restates the first and then adds a reason that contradicts line 286, which told me that after *any* join I address columns by table name. So: is table-prefixing always required after a join, or only for names both tables share? The page says both. - -**8. Line 343.** - -> "Not available on SQLite, where the call does not compile. If you reach it anyway, it throws an error whose `code` is `ORM.CAPABILITY_MISSING`" - -"If you reach it anyway" — how? If it does not compile, what is the path where it runs? I guessed "if you ignore the TypeScript error or use JavaScript," but the page does not say. Same wording at line 448. - -## Words and phrases I had to guess - -- **"table-level, SQL-shaped methods"** (line 9) — guessed: "methods that look like SQL clauses and start from a table rather than a model." -- **"facet"** — in the anchor `[#the-dbsql-facet]` (line 133). The word appears nowhere in the prose. Guessed it is leftover internal vocabulary; it will show up in the URL people share. -- **"plan"** (line 145) — the variable name. `build()` is described as returning "the built query," but the variable is called `plan` everywhere. I guessed they are the same thing. `build()` itself is only defined in part 2, and I am told to call it ~20 times before that. -- **"Project a row down to"** (line 193) — guessed "reduce each row to." -- **"scope"** (line 189) — I guessed "the set of column names TypeScript will let you write," but I did not know whether violating it is a compile error or a runtime error until line 201 told me it is neither, it is a throw at call time. -- **`outerLeftJoin`** — guessed it is SQL's `LEFT OUTER JOIN`. The name is reversed from SQL, and the page never says so. -- **`rawCodecInferer` / `inferCodec`** (line 154) — no guess. I do not know what a codec is here. -- **`db.context`** (line 154) — guessed "some internal handle on the connection." -- **`SelectQuery`** — guessed it is a type name I never import, only see in tables. -- **`lateral`** (line 344) — "The callback receives a `lateral` builder." I guessed it is a table-less starting point that you point at a table with `.from()`. - -## Places I asked "so what do I actually type?" - -1. **Installing and importing.** Line 131 sends me to "Coming from Prisma ORM 7" for "the packages to install and the commands to run." On a reference page that opens with `import postgres from '@prisma/orm-postgres/runtime'`, I wanted one line naming the package. -2. **`import type { Contract } from './contract.d'`** — where do `contract.json` and `contract.d.ts` land relative to my source? The import path `'./contract.d'` (with the `.d`, no extension) is unusual enough that I would have copied it wrong. -3. **Setting a schema other than `public`.** Told it is possible, not told how. -4. **Getting a type id for `.returns()`.** I need `pg/int4@1` for every `fns.raw`. There is no list on this page and the pointer to it (line 207) is a sentence I could not parse. -5. **Ordering nulls.** Line 395 tells me `nulls` is accepted by TypeScript and silently does nothing. It does not tell me what to do instead — presumably `fns.raw` in `orderBy`, but the page does not say. -6. **`select()` replacing columns.** "Calling `select()` a second time never replaces the first call's columns." So how do I drop a column I already selected? Not answered. -7. **`db.sql.public.tag.insert([{ id, label: 'typescript' }])`** (line 165) — `id` is an undeclared variable. The `Uuid @id @default(uuid())` in the schema made me expect I could omit it. Do I have to generate ids myself with `db.sql`? -8. **Inside `lateralJoin`, where does `f.user.id` come from?** The example (line 371) uses `f.post.userId` and `f.user.id` in the same callback. The Remarks say "The subquery can filter on the outer row's columns," but never say that the outer table's columns appear on the same `f` under its own table name. I inferred it from the example, not the prose. -9. **Is a built query reusable?** Can I hold `plan` and run it twice, or with different parameters? Not addressed. - -## Places explaining internals when I wanted instructions - -- **Line 345:** "a chain with `.as(...)` on the end throws `subquery.getRowFields is not a function`." That is a JavaScript stack trace from inside the library. I only needed "do not call `.as()` here" — which the sentence already said twice before this. -- **Line 207:** "A type id such as `pg/int4@1` is the PostgreSQL type plus a version, always `@1` today." Tells me how the identifier is constructed; does not tell me where to get the right one. -- **Line 131:** the `contract.json` / `contract.d.ts` split — "read when your app runs" vs "read by TypeScript" is the library's internal division of labour. I need to run one command. -- **Line 448:** "`DISTINCT ON` is a PostgreSQL extension to SQL." True, and irrelevant to what I do. -- **Line 19:** the schema block carries `types { Embedding1536 = pgvector.Vector(1536) }`, `@@type("pg/text@1")`, `@@discriminator(type)`, and `@@base(Task, "bug")` — four features I have never seen, in the first code block, none of which any example uses. The page tells me to ignore `Address`, `Task`, `Bug`, and `Feature`, but not the syntax around them. - -## Could I do what the page is for, after one reading? - -Partly. I could write a `select` with `where`, `orderBy`, `limit`, `offset`, an `innerJoin`, an `outerLeftJoin`, and a `.as()` subquery, by copying the examples and changing table and column names. Those sections are clear and each has a runnable example. - -What I would still not know: - -- **How to get to a working `db` at all.** Everything on the page hangs off `db.sql.public`, and the one paragraph that creates `db` sends the install and setup steps to another page while spending its own words on what the two generated files are for. I would have stopped at line 131 and gone looking elsewhere. -- **What `build()` actually returns and whether I can reuse it.** The page's central verb is defined in a half I do not have. -- **How to write any `fns.raw` expression**, because I cannot get a type id for `.returns()` and every computed-column example needs one. -- **Whether table-prefixing after a join is always required or only for ambiguous names.** Lines 286 and 346 disagree. -- **What the second entry point (`sql(...)`, `rawCodecInferer`) is for,** or whether I am supposed to care. -- **The SQLite story.** The note promises SQLite is supported, one sentence says drop `public`, and then every example and two "PostgreSQL only" remarks are Postgres. I would not know which of the SELECT methods I can use on SQLite without testing each one. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part1.round3.md b/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part1.round3.md deleted file mode 100644 index 92c160920d6..00000000000 --- a/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part1.round3.md +++ /dev/null @@ -1,96 +0,0 @@ -I read the file once, straight through, as a Prisma 7 user who has never seen this version. - -## Sentences I could not restate after one reading - -**1.** "Before you can build queries, run `npx prisma contract emit`. It reads your contract, the `contract.prisma` file that replaced `schema.prisma`, and writes `contract.json` and `contract.d.ts` next to it." - -What stopped me: "your contract" is used as if I know what a contract is. I know what `schema.prisma` is. The sentence tells me a file was renamed but not what changed inside it, and "contract" then reappears everywhere (`contract emit`, `contract.json`, `Contract` type, "contract features", "Contract authoring") as a load-bearing concept I was never given. Plainest wording I'd have understood: "In Prisma 8 your `schema.prisma` is called `contract.prisma`. Run `npx prisma contract emit` to generate `contract.json` and `contract.d.ts` beside it; the examples import both." That at least stops "contract" from sounding like a new abstraction. - -**2.** "The `types` block, `@@type`, `@@discriminator`, and `@@base` are contract features documented under Contract authoring, and no example on this page uses them." - -Four unexplained names plus an unexplained category ("contract features") in one sentence, telling me to ignore all of it. I stopped and re-read to check I wasn't meant to do something. - -**3.** "`db.runtime()` returns the connection that runs a built query. Do not `await` it. Only `runtime.query()` is awaited." - -"Do not `await` it" reads as a rule with a reason I'm not given. I could not tell if awaiting it breaks, or is merely pointless. Plainer: "`db.runtime()` is synchronous — it returns immediately." - -**4.** "`db.sql` is ready to use as soon as you have a client, so most code needs nothing more. If you wrap your own database client, the `sql(...)` function from `@prisma/orm-postgres/builder/runtime` builds the same query builder around that client." - -"wrap your own database client" — I don't know what this means or when I'd be doing it. There's no example and no link. After one reading I could not tell whether this concerns me. - -**5.** "Not available on SQLite, where the call does not compile. Called from plain JavaScript, or after a cast, it throws an error whose `code` is `ORM.CAPABILITY_MISSING` at the moment you call `lateralJoin()`." - -Three situations packed together, and "after a cast" assumes I know why anyone would cast. This is describing how the type system fails to protect you rather than telling me what to do. Plainer: "PostgreSQL only. On SQLite TypeScript rejects the call; if you get past the types, it throws `ORM.CAPABILITY_MISSING`." - -**6.** "A computed expression's result type comes from `.returns(...)` on `fns.raw`, or from the operation's own declared return type." - -"the operation's own declared return type" — which operation? The `fns.*` helper? Something I declared? I could not resolve it. - -**7.** "A type id is the PostgreSQL type name plus `@1`." - -The rule is immediately broken by its own list: `pg/timestamptz-temporal@1` is not a PostgreSQL type name, and the real prefix `pg/` is not mentioned in the rule at all. I could not derive the type id for `numeric` or `jsonb` from this sentence, which is the only reason the sentence exists. - -**8.** "or read the id itself from `db.sql.public.user.columns.id.codecId`." - -"codec" appears once, undefined. I guessed it means the thing that converts a database value to a JavaScript value, but the page never says. - -**9.** "After the join, a column name that both tables have must be written under its table, such as `f.post.id` or `f.post.createdAt`. A bare `select('id')` or `orderBy((f) => f.createdAt, ...)` throws, because a name both tables have is not in the query's scope on its own." - -This is in `lateralJoin()`'s Remarks, but the example joins `user` to an alias called `latestPost`, so "both tables" and `f.post.*` don't match the example above it. I could not work out whether `f.post` or `f.latestPost` is correct after the join. The example uses `f.latestPost.id` outside and `f.post.id` inside, which I only spotted on a second pass. - -**10.** "Put the `distinctOn()` columns first in `orderBy()`, then the column that decides which row wins. Nothing checks this for you, and without it you cannot predict which row you get." - -Two rules and a warning in one breath. I had to look at the example to understand the ordering rule at all. - -**11.** "TypeScript also accepts a `nulls` option, but the builder ignores it and no `NULLS FIRST` or `NULLS LAST` reaches the SQL. Do not use it." - -I understood the sentence. What I could not restate is what I'm supposed to do instead — I sort by a nullable column fairly often. - -## Words and phrases I had to guess - -- **"contract"** — guessed: the new name for the schema, plus whatever is generated from it. -- **"facet"**, in the anchor `#the-dbsql-facet` — the word never appears in the text. Guessed: "the `sql` part of `db`". -- **"after a cast"** — guessed: `as any` to get past TypeScript. -- **"codec"** — guessed: the value-conversion layer; `codecId` is the type id string. -- **"type id"** / `'pg/int4@1'` — guessed `@1` is a version number for the encoding, but I don't know what changes at `@2`. -- **"plan"** — the page says "The examples name the built query `plan`", so I read it as just a variable name, but "plan" is suggestive enough that I wondered if it was a query plan in the database sense. -- **"scope"** — defined, and the definition held up. This one worked. -- **`pgvector.Vector(1536)`** and `Embedding1536` — guessed: pgvector extension type; unused here. -- **"numeric expression"** in `limit(n)` — guessed: something built from `fns`, but I have no idea what it looks like. - -## Places I asked "so what do I actually type?" - -1. **Getting a contract at all.** The page says `contract.prisma` "replaced `schema.prisma`". Do I rename mine? Does something convert it? "Coming from Prisma ORM 7" is linked once, for "the rest of the setup steps", after the `contract emit` instruction — so I don't know whether the migration is before or after the command I was just told to run. -2. **Every example variable.** `aliceId`, `carolId`, `helloWorldId`, `untaggedPostId`, `newestPostId` are used in nine examples and defined nowhere. I can infer they're UUIDs I'd have on hand, but no example is copy-runnable. -3. **`import type { Contract } from './contract.d'`** — importing from a path ending `.d` looked wrong to me. I'd have typed `'./contract.js'` and guessed. -4. **A schema that isn't `public`.** "`public` is the PostgreSQL schema your tables are in." Mine is `app`. Is it `db.sql.app.user`? The page never says whether the key is my actual schema name. -5. **`limit()` with an expression.** "or a numeric expression" with no example. -6. **Wrapping my own client** — `sql(...)` is named, with no signature and no example. -7. **`.returns()` when omitted.** Is it required on every `fns.raw`? What is the type if I skip it? Not stated. -8. **Chain order.** Examples show `select().where()`, `innerJoin().select().where()`, and `select().orderBy().distinctOn()`. The only statement about order is that `distinctOn()` and `orderBy()` are order-independent — which made me assume order matters everywhere else, and nothing tells me the rules. -9. **What `runtime.query()` returns and what I do with `runtime` afterwards.** Nothing about closing, pooling, or reuse. `db.runtime()` is created once at the top of the entry-points example and never mentioned again. -10. **Seeing the SQL.** The opening paragraph sells "direct control over the generated SQL", and the page never shows how to look at the SQL. (I accept this may be in the half I don't have.) -11. **Enum values.** The schema declares `Priority { Low = "low" ... }` and examples write `fns.eq(f.priority, 'high')`. So I pass the mapped string, not `Priority.High`. I guessed that; the page doesn't say. - -## Places that explain the tool's insides when I only wanted to know what to do - -- **The example-schema paragraph:** "They are here only so that this schema matches the one on the ORM client reference, and you can ignore them." That's an explanation of how the documentation was assembled. It cost me a paragraph to learn that four models don't matter. -- **`ORM.COLUMN_UNKNOWN` "at the moment you call `select()`"** and the same "at the moment you call" construction for `ORM.CAPABILITY_MISSING`. Twice I was told *when in the builder's lifecycle* an error fires. I only wanted to know the call is invalid. -- **"Not available on SQLite, where the call does not compile. Called from plain JavaScript, or after a cast..."** — this describes the layers of the type system rather than the rule, which is just "PostgreSQL only". -- **The `nulls` option paragraph** — a description of a defect in the builder. Useful as a warning, but it tells me what the builder does internally with my input rather than what to write. -- **"`SelectQuery` in the tables below is the type they return. You never import it."** — the second sentence is about the type system, and I'd already assumed it. - -## Could I do what the page is for, after one reading? - -Partly. I could write a `select`, a `where` with `fns.and`/`fns.or`, an `innerJoin`, an `orderBy`, a `limit`/`offset`, and a `.as()` subquery. The chaining shape is clear and the examples for those are good. - -What I would still not know: - -- How to get from my Prisma 7 project to a working `contract.prisma` and `contract.json`. This is the first instruction on the page and it is the one I'd be stuck on. Everything downstream depends on it. -- What "contract" means, which matters because the word is in the command, the file names, the type parameter, and a linked section. -- What to pass to `.returns()` for any type outside the seven listed, because the stated rule for building a type id contradicts its own examples. -- Whether my non-`public` schema works, and what to type if it does. -- Where the example variables come from, so I could not run a single example as written. -- After a `lateralJoin`, whether my columns are under `f.post` or `f.latestPost` — the Remarks and the example disagree on the face of it. -- How to sort nulls last, which the page tells me I cannot do but does not tell me how to work around. -- Whether chain order matters anywhere other than `distinctOn`/`orderBy`. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part1.round4.md b/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part1.round4.md deleted file mode 100644 index 2ea9e3314a4..00000000000 --- a/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part1.round4.md +++ /dev/null @@ -1,75 +0,0 @@ -Read once, top to bottom, as a Prisma 7 user. - -## Sentences I could not restate after one reading - -**Line 19** — "No example uses `Address`, `Task`, `Bug`, or `Feature`. No example on this page uses `types`, `@@type`, `@@discriminator`, or `@@base`." -What stopped me: not a word, but the point. You gave me a schema containing five things, then told me none of them are used. So why are they there? I spent time reading `types { Embedding1536 = pgvector.Vector(1536) }`, `@@discriminator(type)`, and `@@base(Task, "bug")` — all new to me — before being told to ignore them. Plainest version: cut the unused models from the example schema. If they must stay, say "the schema below is the full example schema used across the reference pages; this page only uses `user`, `post`, `post_tag`, and `tag`." - -**Line 137** — "`db.runtime()` returns the connection that runs a built query. `db.runtime()` is synchronous and returns immediately. Only `runtime.query()` is awaited. Hold the connection in a variable and reuse it. Write the type import path as `./contract.d`, exactly like that." -Five unrelated facts in five sentences, under a heading that says "The `db.sql` builder" but which is mostly about `db.runtime()`. "Write the type import path as `./contract.d`, exactly like that" told me *what* to type but gave me no reason, so I could not tell whether it is a real rule or a typo I should fix. Plainest version: split this into a short "connect and run" paragraph and a separate "build a query" paragraph. - -**Line 137** — "Finish it with [`build()`](#build), which returns the built query." -Circular. `build()` returns the built query. I still do not know what a built query *is* — an object? SQL text? Is it sent to the database at `build()` time or at `query()` time? Plainest version: "`build()` turns the chain into a query object. Nothing touches the database until you pass it to `runtime.query()`." - -**Line 172** — "Chain the methods in any order, except that a join must come before any call that names the joined table's columns, and `distinctOn()` needs its columns first in `orderBy()`." -Three rules in one sentence, and the third one I could not parse. "needs its columns first in `orderBy()`" — first in what sense? Does `distinctOn('userId')` have to be called before `orderBy`, or does `'userId'` have to be the leading `orderBy` key? I only worked it out from the example at line 465, where `distinctOn()` is called *last*. Plainest version: "The columns you pass to `distinctOn()` must be the first sort keys in `orderBy()`." - -**Line 176** — "After a join, a column name that both tables have is on `f` only under its table name, such as `f.post.id`." -I believed this, then the `innerJoin` example at line 305 writes `f.user.email`, and `email` exists only on `user`. Line 283 says a name only one table has "stays on `f` directly." So which is it — is `f.user.email` also legal, or is the example wrong? The page never says both forms work. This is the single thing I was least sure about, and it affects every joined query I would write. - -**Line 176** — "For an enum column you pass the value stored in the database, the string after `=` in the enum, so `High = \"high\"` is matched by `fns.eq(f.priority, 'high')`." -Works for `Priority`. But the schema's other enum, `user_type`, has no `=` at all — its members are bare `admin` and `user`. So what do I pass for `f.kind`? The rule as written has no answer. - -**Line 340** — "On SQLite TypeScript rejects the call. If you get past the types, it throws an error whose `code` is `ORM.CAPABILITY_MISSING`." -"If you get past the types" — I had to guess this means "if you cast to `any` or ignore the error." Say that. - -**Line 392** — "TypeScript also accepts a `nulls` option, but the builder ignores it and no `NULLS FIRST` or `NULLS LAST` reaches the SQL. Do not use it. To put nulls last, sort first by a computed value: an `fns.raw` fragment of the form `CASE WHEN IS NULL THEN 1 ELSE 0 END` that returns `pg/int4@1`. Then call `orderBy()` again for the column itself." -I understood that `nulls` is broken. I could not reliably reconstruct the workaround from prose. This is the one place on the page that describes a multi-step code change with no code block, and it is also the one place where I am most likely to get it wrong. - -## Words and phrases I had to guess - -- **"facet"** (line 135, in the anchor `#the-dbsql-facet`) — appears nowhere in the prose. I guessed it means "the `db.sql` part of the client." If the word is not in the text, it should not be in the anchor. -- **"caveat that shows up only when the query runs"** (line 11) — guessed: "fails at runtime, not at compile time." -- **"SQL-family builder"** (line 13) — guessed: "the builder for SQL databases, as opposed to MongoDB." -- **"finished `SELECT` query"** (line 164) — guessed: a chain you have stopped adding to but have *not* called `build()` on. The page uses "finished" and "built" for different things and never says so. Line 520 ("You cannot call `build()` on it") is what finally told me. -- **"contract"** (line 131) — guessed: the new name for the schema. The page says the file is renamed but never says what the word means or what `contract.json` and `contract.d.ts` each contain. -- **"`Uuid`"** as a field type (line 51) — guessed: a new built-in scalar replacing v7's `String @db.Uuid`. -- **"`@1`"** in `pg/int4@1` (line 194) — guessed: a version number on the type id. Never stated. -- **"codec"**, in `.codecId` (line 194) — guessed: the thing that converts between the database type and the JS value. Never defined. -- **"plan"** (line 137) — I guessed this is just the examples' variable name, not a Prisma concept. The page does say "The examples name the built query `plan`," which helped, but `plan` also reads like a query plan, which it is not. -- **"top-N-per-group"** (line 336) — I know this one, but it is SQL-blog jargon, not plain English. -- **"scope"** (line 174) — this one you actually defined, and I used the definition throughout. Keep it. - -## Where I asked "so what do I actually type?" - -1. **Line 131, "Install `@prisma/orm-postgres`."** No command. Every other instruction on this page is a literal command (`npx prisma contract emit`). Give me `npm install @prisma/orm-postgres`. -2. **Line 142, `import contractJson from './contract.json' with { type: 'json' };`** Import attributes need specific `tsconfig.json` and Node settings. The page shows the line and says nothing about what has to be true for it to compile. I have never typed `with { type: 'json' }` in a v7 project. -3. **Line 131, where do these files live?** "beside it" — beside `contract.prisma`. But my source is in `src/`. The import is `'./contract.d'`, so the emitted files must sit next to my TypeScript. The page never reconciles these. -4. **Line 392, the nulls-last workaround.** Described in words only. I wanted the five-line code block. -5. **Line 192, "To return fewer columns, start a new query instead."** No example of what that looks like. I understand the concept; I wanted to see the two-query version so I know there is no `reselect()` I am missing. -6. **Line 194, type ids.** "A few use a longer name, such as `pg/timestamptz-temporal@1` for `DateTime`." Which few? Where is the full list? If my `fns.raw` returns a `numeric` or a `jsonb`, I have to guess the id, and the page's rule ("`pg/` plus the PostgreSQL type name plus `@1`") has just told me the rule has exceptions it will not enumerate. -7. **Closing the connection.** "Hold the connection in a variable and reuse it." Do I ever close it? In v7 I called `$disconnect()`. The page does not say, and this is the entry-points section. -8. **`outerLateralJoin()`** is named three times (lines 151, 340) and has no section, no signature, and no example. Line 340 says "same arguments, same availability," which is something, but it is buried in another method's Remarks. - -## Where the page explains the internals when I only wanted to know what to do - -1. **Line 133** — "`public` is the PostgreSQL schema your tables are in, and Prisma ORM 8 puts every table in `public` today." If every table is always in `public`, telling me why does not help me type anything. I only need "type `db.sql.public.
`." -2. **Line 137** — "`db.runtime()` is synchronous and returns immediately. Only `runtime.query()` is awaited." This is a fact about the implementation. The example at line 145 already shows me `const runtime = db.runtime()` with no `await`, which is all I needed. -3. **Line 392** — explaining that TypeScript accepts `nulls` but the builder drops it, and that "no `NULLS FIRST` or `NULLS LAST` reaches the SQL," is an explanation of a bug's mechanism. "The `nulls` option does nothing. Do this instead: [code]" is what I needed. -4. **Line 343** — "because a name both tables have is not in the query's scope on its own" is the reason. The rule in the sentence before it was enough. - -## Could I do what the page is for, after one reading? - -Partly. I could write a single-table `select().where().orderBy().limit().build()` and run it. The chain shape is clear, the examples are runnable, and the `f`/`fns` split made sense on the first pass. - -What I still would not know: - -- **Whether my join code compiles.** The `f.user.email` contradiction means I do not know if I must qualify every column after a join or only the ambiguous ones. This blocks the main reason I would reach for this builder at all. -- **How to get from my working v7 project to the first line of this page's code.** The Entry points section assumes `contract.prisma` already exists, points at a migration page I have not read, and does not say where the emitted files go relative to my source. -- **What to pass for an enum with no `=` values**, such as `kind`. -- **What type id to use** for any Postgres type not in the seven-item list. -- **How to sort nulls last**, concretely. -- **What `outerLateralJoin()` looks like when typed out.** -- **Whether I ever close the connection.** - -One more thing that cost me confidence rather than comprehension: the schema declares `posts Post[]` on `Tag` and `tags Tag[]` on `Post`, *and* a separate explicit `PostTag` model. In v7 those are two different ways to model many-to-many and you pick one. The page then calls `post_tag` "the model for the `Post`/`Tag` join table" without explaining why both exist. I could not tell whether this is new v8 behaviour I need to learn or an error in the example schema. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part2.round1.md b/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part2.round1.md deleted file mode 100644 index b1b0866ca62..00000000000 --- a/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part2.round1.md +++ /dev/null @@ -1,94 +0,0 @@ -I read the file once, straight through, as the reader described. Here is what happened. - -## Sentences I could not restate after one reading - -**1. The aggregate note, line 38.** This is one sentence carrying six separate facts. - -> "For `count()` and integer `sum()`, a value outside the safe-integer range, ±(2^53 − 1), raises `RUNTIME.DECODE_FAILED` rather than answering with a rounded value (`avg()` is computed as `float8` and has no such guard), so reach for the lossless form when the magnitude is real: `countBigInt()` returns a `bigint`, `sumBigInt(field)` returns a `bigint`, and `avgDecimal(field)` returns an exact decimal string." - -What stopped me: the parenthetical about `avg()` interrupts the rule, `float8` is never explained, "reach for the lossless form when the magnitude is real" is a figure of speech I had to decode, and "answering with a rounded value" describes something that does *not* happen, so I had to hold a counterfactual in my head. Plainer: "If a `count()` or integer `sum()` result is larger than 2^53 − 1, the query fails with error code `RUNTIME.DECODE_FAILED`. Use `countBigInt()` or `sumBigInt()` instead when totals can get that large. `avg()` does not fail this way; it loses precision instead, so use `avgDecimal()` when you need an exact figure." - -**2. Line 366.** - -> "They come from the aggregate map in the contract written by `prisma contract emit`, which each target and extension contributes to, so which ones exist depends on your database." - -"Aggregate map", "target", and "extension" are all undefined here, and `prisma contract emit` appears for the first and only time on the page. I could not resolve any of them. Plainer: "Which aggregate functions you get depends on your database. PostgreSQL gives you these eight." - -**3. Line 379.** - -> "Any further function is registered dynamically: `ilike` is registered by the Postgres adapter for textual columns, while functions like `cosineDistance` come from an extension pack (pgvector) registered for your contract; without the matching adapter or extension pack, those functions are absent." - -Three unexplained terms ("registered dynamically", "adapter", "extension pack") and two ideas in one sentence. I do not know whether I have to do anything to get `ilike`, or whether it is just there. - -**4. Line 398.** - -> "A raw expression PostgreSQL computes as `numeric` still arrives as a string even when annotated `'pg/int4@1'`, because Prisma ORM does not convert the values a raw fragment returns." - -I read this three times. The page is telling me `.returns()` is a lie I tell TypeScript. That is a genuinely important warning and it is buried in a subordinate clause. It needs to be blunt: "`.returns()` only tells TypeScript what type to expect. It does not change the value you actually get back. If the real value is a string, your typed `number` will be a string at runtime." - -**5. Line 471, last sentence.** - -> "Since `8.0.0-rc.10` a buffered query hands its pooled connection back before you read the rows, so a paused loop no longer blocks other queries; a cursor stream keeps its connection until it finishes." - -"Buffered query" and "cursor stream" appear here for the first time. I do not know which one I get when I write `for await`, and the page never tells me how to ask for either. I could not restate this at all. - -**6. Line 471, "Each result is read once, one way."** Read once by whom? Does the array version stop me from also iterating? Does iterating twice throw? I guessed it means "you can `await` it or `for await` it, not both," but I am guessing. - -**7. Line 217.** - -> "On an adapter for a database without it, TypeScript types `returning` as `never`, so the call does not compile; calling it anyway throws an error whose `code` is `ORM.CAPABILITY_MISSING`." - -If it does not compile, how do I call it anyway? I could not work out the situation being described. - -## Words and phrases I had to guess - -- **`plan`** — every example names the variable `plan`, the prose calls it a "built query", and an anchor is `#executing-a-plan`. I guessed these are all the same thing. The page never says so. -- **`codecId`** — guessed: a string naming a database type. What `@1` means I have no idea; guessed a version number. -- **`'pg/int4@1'`, `'pg/text@1'`, `'pg/bool@1'`** — guessed these map to PostgreSQL `int4`, `text`, `boolean`. There is no list anywhere on the page, so I would not know what to write for a date, a UUID, or a numeric. -- **`1n`** (line 92) — guessed a JavaScript bigint literal. The page assumes I know, and it never explains why I would have reached for one. -- **"Gate the update with `where()`"** (lines 244, 289) — guessed "restrict which rows it touches". "Gate" is not a plain word for this. -- **"how it is stored and read comes from the target column"** (lines 182, 318) — guessed this means encoding and decoding. The phrase is repeated twice without ever being made concrete. -- **"function bag"** (line 352) — guessed "an object holding functions". -- **"family-agnostic utility"** (line 452) — guessed "works with more than just the SQL builder". "Family" is never defined. -- **"lossless form"** — guessed "the version that does not lose precision". Fine, but it appears before it is explained. -- **`distinctOn()`** (line 147) — appears once, in a list, never defined or shown. Guessed it is PostgreSQL `DISTINCT ON`. -- **"a `varchar` column widens to `text`"** (lines 376-377) — guessed this only matters for the TypeScript type. - -## Places I asked "so what do I actually type?" - -1. **Where does `runtime` come from?** Every single example calls `runtime.query(plan)`. I was told `db.runtime().query(...)` in the first half. No example on this page shows `const runtime = db.runtime()`. I would have copied an example and hit an undefined variable. -2. **Do I have to generate ids myself?** The schema says `id Uuid @id @default(uuid())`, but every insert example passes `crypto.randomUUID()` explicitly. So does `db.sql` apply the schema default or not? The page never says. This is the first thing I would try and the first thing that would break. -3. **What about insert conflicts?** No `ON CONFLICT`, no upsert, no mention that the topic exists. In ORM 7 I used upsert constantly. I do not know if it is unsupported, or somewhere else. -4. **Does `delete()` take arguments?** It is the only method with no Options section at all. I do not know if `delete()` is always bare. -5. **What happens if I `delete()` or `update()` without a `where()`?** Not stated. I would want that answered before I typed it. -6. **Which columns am I allowed to `select()` alongside a `groupBy()`?** SQL requires grouped or aggregated columns. The page never says what happens if I break that rule. -7. **Can I `groupBy` an alias?** In the computed-value example (lines 77-79) I have to retype the whole `EXTRACT(YEAR FROM ...)` expression in both `select()` and `groupBy()`. I would want to write `.groupBy('yearPlaced')`. The page does not say whether that works. -8. **`orderBy('totalAmount')` sorts by an aggregate alias** (line 158), but elsewhere `orderBy` takes column names. The page says "Sort by an aggregate alias to order groups" but does not say whether the alias must be one I selected, or what happens if it is not. -9. **When is `.returns()` not required?** The table says "Yes for a projected or compared value". I do not know what the third case is. -10. **Where do the imports come from?** `param` from `@prisma/orm-postgres/relational-core/expression`, `ResultType` from `@prisma/orm-postgres/components/runtime`. Two unrelated-looking subpaths of the same package, with no rule I can infer for a third import. -11. **How do I pick a `codecId` for a bare value?** Line 387 says "the builder picks the `codecId` from its JavaScript type" — but not which type maps to which codec, so I cannot predict when I need `param()`. -12. **`query` or `execute`?** Line 439 says `execute` is "for writes that return no rows". The insert-with-`returning` example uses `query`, the insert-without uses `execute`. So what do I call for an `update()` with no `returning()`, and what does `query` do if I use it on a write? Not stated. - -## Places the page explains internals when I only wanted to know what to do - -- **Line 366-379**, the whole explanation of contracts, targets, adapters, extension packs, and dynamic registration. I wanted a list of functions I can call. I got the tool's plugin architecture. -- **Line 176**: "Multiple rows are inserted in one `INSERT ... VALUES (...), (...)` statement, not one round-trip per row." This is the implementation. It happens to be reassuring, but it is not something I do. -- **Line 398**: "It does not cast the value in SQL or change how the driver decodes it." I wanted "you may get a string, check it." I got the mechanism. -- **Line 471**: pooled connections, paused loops, cursor streams. I wanted "use `for await` if the result set is big." -- **Line 92**: "PostgreSQL evaluates the comparison server-side." I wanted "pass a plain number." The reason why does not help me type anything. -- **Line 82-83**: the whole explanation of why `yearPlaced` is `'2024'` rather than `2024`. Correct and necessary, but delivered as a code comment about internals rather than as a warning I can act on. - -## Could I do what the page is for, after one reading? - -Partly. I could write a `groupBy` with a `count`, a `having` with a sum threshold, a multi-row insert, and an update with `returning`. Those four patterns are shown clearly enough to copy. - -I could not: - -- **Run any of it.** I do not have `runtime`. Every example uses it; none creates it. -- **Insert a row without guessing.** I do not know whether schema defaults apply, so I do not know whether `id` is mine to supply. -- **Write any `fns.raw` of my own beyond copy-paste.** I have three `codecId` strings from examples and no list, no naming rule, and no way to find one for a date or a numeric. -- **Predict what `.returns()` gets me.** After reading line 398 I know the annotation can be wrong, but I do not know which cases are safe, so I would have to test every raw expression by hand. -- **Handle insert conflicts at all.** The topic is absent. -- **Decide between `query` and `execute`, or between collecting and streaming**, with any confidence. The last section describes two internal modes I was never taught how to select. - -The single biggest problem is the missing `runtime` definition. The second is that `codecId` is required in six places and defined in none. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part2.round2.md b/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part2.round2.md deleted file mode 100644 index af0430a136c..00000000000 --- a/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part2.round2.md +++ /dev/null @@ -1,86 +0,0 @@ -Read once, top to bottom, as a Prisma 7 user seeing this version for the first time. - -## Sentences I could not restate after one reading - -**Line 38 (the aggregate note).** "`count()`, `sum()` over an integer column, and `avg()` over an integer column all return a JavaScript `number`. If a `count()` or integer `sum()` result is larger than 2^53 - 1, the query fails with an error whose `code` is `RUNTIME.DECODE_FAILED`. Use `countBigInt()` or `sumBigInt(field)` instead when totals can get that large. `avg()` does not fail this way. It is computed as a floating-point `number` and loses precision instead, so use `avgDecimal(field)` when you need an exact figure. `min()` and `max()` return the input column's own type." -Six rules in one paragraph before I have seen a single aggregate call. I stopped at "the exact form of" in the heading, "with exact variants beside them" — I did not know what a "variant" was until I hit the function table 300 lines later. Plainer: put this note *after* the first `count()` example, and split it into "these return `number`", "these overflow, use the BigInt version", "`avg` loses precision, use `avgDecimal`". - -**Line 62.** "Every example from here on calls `runtime`, and names the built query `plan`." -Two things stopped me. You do not "call `runtime`" — you call `runtime.query()`. And why is the built query named `plan`? Nothing on the page calls it a plan; the page calls it "the built query" everywhere else, and then the anchors give the game away: `[#executing-a-plan]`, `[#recover-the-row-type-from-a-plan]`. I guessed "plan" is an older name for the same thing and that I am seeing a half-finished rename. Plainer: "Every example below assumes `const runtime = db.runtime()`." - -**Line 86–87.** "`// rows === [{ yearPlaced: '2024', orderCount: 10 }]` / `// yearPlaced is the string '2024', not the number 2024. See `.returns()` below.`" -I annotated the expression `'pg/int4@1'` and got a string. I could restate the words but not the rule. Then line 403 confirms `.returns()` "does not change the value you actually get back" and points me *back* to this example. The two places point at each other and neither tells me how to get a number. - -**Line 399.** "Required: Yes for a selected value, a compared value, or a fragment used on its own as a `where()` predicate". -That is a list of every use of `fns.raw` I can think of. So when is `.returns()` *not* required? The page never says. Plainer: say "always required, except X". - -**Line 469.** "`postgres(...)` turns cursors off in the driver it builds, so every query loads every row into memory first." -`postgres(...)` never appears anywhere on this page. "turns cursors off in the driver it builds" is a sentence about the inside of the tool. I could not restate what I am supposed to *do* with it. - -**Line 471.** "Each result is read once, one way. Re-awaiting a result you already awaited is safe and gives you the same array." -These contradict each other on first read. "Read once" then "re-awaiting is safe". Plainer: "You can `await` a result as many times as you like. You cannot mix `await` and `for await` on the same result, and you cannot `for await` it twice." - -**Line 191.** "The column it goes into decides how it is stored and read, so you never annotate an insert value." -"Annotate" with what? I had not met `param()` or `.returns()` yet at this point in the page. - -**Line 3.** "Grouping starts with `groupBy()`, which turns a query into a `GroupedQuery`." -But in every example `groupBy()` comes *after* two `.select()` calls. Does the order in the chain matter? Not said. - -## Words and phrases I had to guess - -- **`plan`** — guessed: the object `.build()` returns, same thing the page elsewhere calls "the built query". Two names for one thing. -- **"These are fixed:"** (line 361) — guessed: these functions exist no matter which database you use, unlike the aggregates below. The word "fixed" does not say that. -- **"in scope"** (line 90, "neither a column in scope nor an alias you selected") — guessed: a column of the table I started from. -- **`codecId`** (line 331) vs "type id" (part 1) vs `.returns(codecId)` — guessed these are all the same thing. The page uses two names and never joins them up. -- **"typed to resolve to the returned rows"** (line 236) — guessed: TypeScript now knows the query gives back rows, so I should use `query` not `execute`. -- **`relational-core`** and **`components/runtime`** in the two import paths — guessed these are just package subpaths with no meaning I need. -- **`db.orm.public.User`** (line 450, capital U) against `db.sql.public.user` (lowercase) everywhere else — guessed the ORM side keys by model name and the SQL side by table name. The page does not say so, and the capital letter looked like a typo. -- **"exact form of"** (`countBigInt`, `sumBigInt`, `avgDecimal`) — guessed: the version that does not lose precision. - -## Places I asked "so what do I actually type?" and got no answer - -1. **`exists` and `notExists`.** Listed in the table at line 368. No signature, no example, nothing. Do they take a subquery? Another builder? I cannot write one. -2. **`in` and `notIn`.** Same. Listed, never shown. Do I pass an array? -3. **`ilike`** (line 384). Named, never shown. Is it `fns.ilike(f.email, '%@example.com')`? -4. **Getting a number out of a raw expression.** I now know `.returns('pg/int4@1')` lies to TypeScript and I still get `'2024'`. The page never shows the fix. Do I write `CAST(... AS int4)` in the raw SQL? `Number(row.yearPlaced)`? Say it. -5. **Choosing a `codecId`.** `param()` requires one and its only example is `'pg/text@1'`. What do I pass for a date, a uuid, a boolean, an array? The list is linked from `fns.raw` at line 392 but not from the `param()` section, which is the section that forces me to pick one. -6. **`having()` with an alias.** `orderBy()` takes the alias `'totalAmount'` (line 164). `having()` re-writes the whole `fns.sum(f.amount)` (line 121). Can `having()` take the alias too? Not said, so I would have retyped the aggregate and hoped. -7. **`update()` mixing forms.** The table gives a values object *or* an expression callback. Can I set one column to a literal and another to an expression in the same callback? Not said. -8. **`distinct()` and `distinctOn()` on a grouped query** (line 153). I do not know what distinct means once rows are already one-per-group, and there is no example. -9. **`execute` appears at line 205** in the first insert example, with no explanation. It is defined at line 435, 230 lines later. On first read I did not know why insert used `execute` and the `returning()` example used `query`. -10. **`1n`** (line 98). "Do not pass a `bigint` literal such as `1n`." What happens if I do — a type error, a wrong result, a crash? I would want to know whether this is a compile-time safety net or a trap. -11. **`sum(field)` over a non-integer column** (line 377): "the type PostgreSQL's own `sum` produces for it". So for a `numeric` column I get… a string? A `Decimal`? I would have to go read PostgreSQL docs to use my own schema. -12. **`build()` and defaults** (line 182): "The value is generated when you call `build()`." So if I build once and run twice, do I insert the same uuid twice? That is a real question the sentence raises and does not answer. - -## Places explaining the inside when I only wanted to know what to do - -- **Line 469**, the whole "turns cursors off in the driver it builds" paragraph. I wanted to know whether `for await` saves memory. The answer is no. Say that in one sentence and drop the driver. -- **Line 182**, "The value is generated when you call `build()`." I wanted to know that leaving a column out applies its default. -- **Line 224**, "On a database without it, TypeScript rejects the call. If you get past the type check, for example by casting, the call throws an error whose `code` is `ORM.CAPABILITY_MISSING`." I wanted "works on PostgreSQL and SQLite". The part about defeating your own type check is a curiosity, not instruction. It also never names the database that lacks `RETURNING`. -- **Line 90**, "Prisma ORM does not check that every other selected column is grouped or aggregated. PostgreSQL rejects the query when it runs." Useful in effect, but framed as a division of labour between two systems. Plainer: "If you select a column you did not group or aggregate, the query fails when it runs, not when you build it." -- **Line 450**, "Since `8.0.0-rc.10` it also works on ORM queries". A release note in the middle of a reference entry. -- **Line 425**, "There is no `build({ params })` form." Answers a question I did not ask, unless it is aimed at people migrating from something. - -## Two structural problems - -**The schema switches back without telling me.** Line 5 says "The examples below use a separate `customer` / `order` schema". Every mutation example from line 204 on is back to `db.sql.public.tag` and `db.sql.public.user` from part 1. Nothing marks the switch back, and I spent a moment looking for a `tag` model in the `customer`/`order` schema. - -**Line 147's comment describes code that is not there.** "`// avgAmount is 300, and avgDecimal(f.amount) would give the exact '300.0000000000000000'`" — the example uses `avg`, and the comment tells me about a function the example does not call. - -## Could I do what the page is for, after one reading? - -Partly. - -I could write a grouped query with a count or a sum, filter it with `having()`, sort by an aggregate alias, and take the top row. I could insert rows, update with a values object, delete with a `where()`, and add `returning()`. I could pick `query` versus `execute`. That is the bulk of the page and it works. - -What I still would not know how to do: - -- Write an `exists`, `notExists`, `in`, `notIn`, or `ilike` call. The page names five functions it never shows. -- Get a usable number back from a computed expression. I know `.returns()` will not do it and the page stops there. -- Pick a `codecId` for anything that is not text or `int4`. -- Decide whether `having()` accepts an alias, or whether I must retype the aggregate. -- Mix fixed values and expressions in one `update()`. -- Say what `distinct()` means on a grouped query. -- Tell whether rebuilding versus reusing a built `insert()` gives me a new uuid. - -And I would still be carrying the unexplained word "plan" through the whole page, including into two heading anchors. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part2.round3.md b/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part2.round3.md deleted file mode 100644 index c6a8aa26e5c..00000000000 --- a/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part2.round3.md +++ /dev/null @@ -1,85 +0,0 @@ -Read once, top to bottom, as a Prisma 7 user who has never seen this API. - -## Sentences I could not restate after one reading - -**Line 150** — "On a grouped query, `distinct()` and `distinctOn()` drop repeated rows from the groups you already have, which only matters when you select fewer columns than you grouped by." -Stopped by: I cannot picture the case. Grouping already makes one row per group, so "repeated rows" sounds impossible. "Select fewer columns than you grouped by" is a condition I have to reason out myself, and there is no example. Plainer: give the two-column group / one-column select example, or cut the sentence. - -**Line 179** — "The value is generated when you call `build()`, so build once and run once. Running the same built insert twice inserts the same generated value twice. Build it again for a new one." -Stopped by: three ideas and an apparent contradiction. If the id is `@default(uuid())` and it is the primary key, running twice would fail, not "insert the same value twice." I could not tell whether this is a warning about a crash or about duplicate data. Plainer: "`build()` generates default values once and freezes them. If you run the same built insert twice you write the same id twice, which fails on a unique column. Call `.build()` again for each run." - -**Line 256** — "Every value the callback returns must be an expression, so you cannot put a plain JavaScript value beside a computed one." -Stopped by: "beside" — beside in the same object? In the same statement? And the fix given ("write it as a `fns.raw` fragment with `.returns(...)`") has no example, so I do not know what that looks like. - -**Line 322** — "`codecId` is the type id, the name this page uses everywhere else." -Stopped by: I read this three times. It parses as "codecId is what the rest of the page calls type id," but the wording makes "the name" point at nothing clear. Plainer: "The option is called `codecId`, but it takes the same type id used everywhere else on this page." - -**Line 363 vs 365** — "`in(expr, values)`, `notIn(expr, values)`, where `values` is an array or a built subquery" against "`exists(subquery)`, `notExists(subquery)`, taking a select query you do not call `build()` on". -Stopped by: `in()` wants a **built** subquery, `exists()` wants an **unbuilt** one. That is a real trap and the page states it in passing, in two table cells, with no example of `in()` with a subquery. I would have got this wrong. - -**Line 408** — "A `fns.raw` fragment only becomes an expression once you call it, so there is nowhere in the builder you can use a fragment without it." -Stopped by: "once you call it" — call what? The fragment or `.returns()`? Two "it"s in one clause pointing at different things. Also this is an internal reason for a rule I already accepted from the bold sentence before it. Plainer: "Always call `.returns()`. Nothing in the builder accepts a fragment without it." - -**Line 409** — the whole `.returns()` remark. -Stopped by: it is one paragraph carrying five claims (it is TypeScript-only; `EXTRACT` gives `numeric`; `numeric` arrives as a string; `CAST` does not help; three separate workarounds). One of the workarounds, "or select a real column of that type," is not a workaround at all for a computed value — I cannot select a real column that holds `EXTRACT(YEAR ...)`. That broke my reading of the list. - -**Line 440** — the whole "Running a built query" paragraph. -Stopped by: six facts jammed together (query vs execute, the rule, `AsyncIterableResult`, `await` gives `Row[]`, `for await` gives one row, `execute` gives `{ affectedRows }`). This is the most important paragraph on the page for "how do I run it," and it is the least broken up. It needs a short table. - -**Line 469** — "`for await` does not save memory on PostgreSQL: the client loads every row first, and both `await` and `for await` read that same in-memory result. The connection goes back to the pool before you read the rows." -Stopped by: this tells me how the client works internally and then leaves me with the obvious question unanswered — if it saves nothing, why would I ever use `for await`? The connection-pool sentence is unconnected to anything I have to decide. - -## Words and phrases I had to guess - -- **"plan"** (line 55 and everywhere). I guessed "the object `.build()` returns." The page never says the word means that; line 55 says the examples "name the built query `plan`," which reads as a naming convention, but then the anchor `#executing-a-plan` and the phrase "Running a built query" use it as if it were a defined term. -- **`runtime.execute`** (line 202). First appears inside a code comment. I guessed it is the sibling of `query` for writes. It has no entry of its own anywhere on the page — only prose at line 440. -- **`AsyncIterableResult`** (line 440). Guessed: a thing you can both `await` and `for await`. The page links it to a different page instead of saying so. -- **`codecId` / "codec"** (line 329). Guessed: codec = the thing that encodes and decodes a value, so `codecId` = the type id. Never stated. -- **pgvector** (line 393). Guessed: a PostgreSQL extension for vector columns. Not explained, and `cosineDistance` is named with no signature at all — I do not know if it takes one argument or two. -- **"the contract's default"** (line 179). Guessed: the `@default(...)` in `contract.prisma`. -- **"per-row type is recoverable"** (line 436). Guessed: "you can get the TypeScript type of one row." -- **`ORM.COLUMN_UNKNOWN`, `RUNTIME.DECODE_FAILED`, `RUNTIME.ENCODE_FAILED`, `ORM.MUTATION_DATA_MISSING`, `RUNTIME.ITERATOR_CONSUMED`**. I guessed these are the `code` property on a thrown error object. The page never shows one error being caught, so I do not know what I actually write to check for them. - -## "So what do I actually type?" - -1. **Line 256** — setting a fixed value inside the `update()` callback form. Told to "write it as a `fns.raw` fragment with `.returns(...)`". No example. This is the only place on the page that names a workaround and then does not show it. -2. **Line 363** — `in()` with a built subquery. Stated in a table cell, never shown. -3. **Line 393** — `cosineDistance`. No arguments, no example, no return type. -4. **Line 366** — the `raw` row of the built-in table gives no signature, unlike every other row. -5. **Line 186** — "Every column is optional." So what happens if I leave out a required column that has no default? Not said. I would have to try it. -6. **`returning()`** — can I write `returning('*')`? Can I return a computed expression? Only "Column names (`string`)" is given, and I do not know whether that is a restriction or just the common case. -7. **Line 94** — "You can also group by an alias you gave a selected value, such as `.groupBy('yearPlaced')`." The example directly above (lines 85–87) writes the same eight-token raw fragment twice instead. The page shows me the worse way and mentions the better one afterwards, so I do not know which one I am supposed to type. -8. **Any error code** — I never see `try { ... } catch (e) { if (e.code === ...) }`. Five codes are named and none is shown in use. -9. **Line 94** — "If you select a column you did not group or aggregate, the query fails when it runs." Fails with what? Every other failure on the page comes with a code. This one does not. - -## Places explaining the inside when I wanted the outside - -- **Line 188, 321, 397** — "Each value is sent as a query parameter, never pasted into the SQL" appears three times. Once, near the top, is enough. What I need to know is only the consequence: do not call `.returns()` on insert values. -- **Line 408** — "A `fns.raw` fragment only becomes an expression once you call it." The rule ("always call `.returns()`") is all I need. -- **Line 469** — "the client loads every row first," "the connection goes back to the pool before you read the rows." Pure internals. Neither changes anything I type. -- **Line 219** — "PostgreSQL and SQLite, the two databases the builder supports today. Both support SQL's `RETURNING`." Telling me *why* it is available on both is not something I asked. -- **Line 3** — "Call it after your `select()` calls, because a grouped query has no `select()` of its own." The reason is implementation trivia; "call `groupBy()` after all your `select()` calls" is the instruction. - -## Other things that tripped me - -- **Line 55 against line 58.** "Every example below assumes `const runtime = db.runtime();`" and then the very next line writes `const runtime = db.runtime();` anyway. On first read I assumed the assumption did not hold and looked for the difference. -- **Line 66** — `// e.g. { customerId: ..., orderCount: 5 }` shows a bare object, but `rows` is an array. Line 90 shows it correctly as `[{ ... }]`. Inconsistent within four lines of code. -- **Line 203** — a markdown link inside a code comment: `// see [Running a built query](#executing-a-plan)`. In a code block that renders as literal brackets. It is also where `execute` is introduced for the first time, buried in a comment. -- **`param()` is filed under "Mutations."** It has nothing to do with mutations — its own example (line 345) is a `select` with a `where`. I would never have found it looking for it. -- **Line 91** — "yearPlaced is the string '2024'" immediately after I was told to write `.returns('pg/int4@1')`. The explanation is 300 lines later at line 409, reached by "See `.returns()` below." On first read this looks like a bug in the example. -- **Line 404** — "A fragment used as a `where()` predicate declares `'pg/bool@1'`." "Declares" reads as something the tool does. I want "write `.returns('pg/bool@1')`." -- **Two different deep import paths** (`@prisma/orm-postgres/relational-core/expression` and `@prisma/orm-postgres/components/runtime`) with no hint why they differ. I would guess wrong when reaching for a third import. - -## Could I do what the page is for, after one reading? - -Mostly yes for the plain cases: group by a column with a count, filter with `having()`, insert, update, delete, and get rows back with `returning()`. Those examples are complete and I could copy them. - -What I still would not know: - -- How to set a fixed value alongside a computed one in `update()`. The page tells me it is possible and does not show it. -- Whether to pass `in()` a built or unbuilt subquery — I would have got this backwards from the `exists()` example. -- What `execute` is, in full. It is never given its own entry, and I met it in a code comment. -- What to actually type to check for any of the five error codes. -- Whether my number is going to come back as a string. I would have shipped `yearPlaced` as a string and found out in production, because the explanation is far from the example that hits it. -- When `for await` is worth using. The page tells me it saves no memory and then leaves the choice to me. -- What `cosineDistance` takes. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part2.round4.md b/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part2.round4.md deleted file mode 100644 index b0223e8cbe2..00000000000 --- a/docs/orm-docs-audit/c21-orm-reference/sql-query-builder.part2.round4.md +++ /dev/null @@ -1,66 +0,0 @@ -Read once, top to bottom, as a Prisma 7 user. Findings below. - -## Sentences I could not restate after one reading - -1. Line 5: *"The examples below use a separate `order` schema, where `order.amount` is an integer column"* — "schema" collides with the meaning I was given in part 1, where `db.sql.public.user` is "schema then table" and `public` is the schema. So I read this as a PostgreSQL schema named `order`, then hit `db.sql.public.order` and had to reread. The word is used in three more senses on this page: "the aggregate example schema" (line 9, a `.prisma` model file) and "this page's main schema" (line 154, the example models). Plainest wording: "The examples below use an extra table, `order`, whose `amount` column is an integer." - -2. Line 3: *"A grouped query supports `having()` for group-level filtering, the aggregate functions, and the same `orderBy()`, `limit()`, `offset()`, and `distinct()` methods as a `SelectQuery`."* — "supports the aggregate functions" does not match the example. In the example the aggregate is written inside `.select()` **before** `.groupBy()`, and the same line tells me to "Call `groupBy()` after all your `select()` calls". So the grouped query does not support aggregates; the selects that precede it do. Plainer: "Write your aggregates in `select()` first, then call `groupBy()`. After that you can add `having()`, `orderBy()`, `limit()`, `offset()`, and `distinct()`." - -3. Line 75 / line 405, the `EXTRACT` comment: *"`.returns()` only tells TypeScript what type to expect. It does not convert the value, so yearPlaced is the string '2024'."* — I declared `'pg/int4@1'` and got a string. I could not work out what TypeScript now believes `row.yearPlaced` is. If `.returns('pg/int4@1')` types it as `number`, then the page's own advice, `Number(row.yearPlaced)`, is code TypeScript would flag, and the row is typed wrong at runtime. The page never says. This is the single sentence that cost me the most. - -4. Line 87: *"TypeScript does let you name a selected alias on the `f` argument here, but PostgreSQL does not accept an alias in `HAVING`, and the query then fails when it runs."* — three ideas in one sentence, and the practical upshot (the types offer you something that is guaranteed to break) arrives last. Plainer: "Do not use a selected alias in `having()`. TypeScript will let you, but PostgreSQL rejects it and the query fails at run time. Write the aggregate again instead." - -5. Line 163: *"`build()` generates default values once. Running the same built insert twice writes the same id twice, which fails on a unique column."* — This contradicts the sentence just before it, which says "the `@default(...)` in `contract.prisma` is applied". I could not tell who applies the default: PostgreSQL, or the builder on my machine. The answer matters and is only implied. Plainer: "Prisma generates `@default(uuid())` and `@default(now())` values when you call `build()`, not when the query runs. So the built query holds fixed values — reusing it inserts the same id twice. Call `.build()` again for each insert." - -6. Line 171 vs line 164: *"The rows to insert. Every column is optional."* against *"Leave out a required column that has no default and PostgreSQL rejects the insert when the query runs."* — Read together, these say the opposite things. I assume the first means "optional to TypeScript", but the table does not say so. - -7. Line 310 vs line 393: *"`param()` is for the values that have no such column"* against *"For a bare value, the builder picks the type id from its JavaScript type."* — If the builder already picks a type for a bare value in `fns.raw`, then the bare value *does* get a type without a column, and `param()`'s stated reason for existing disappears. After one reading I did not know when I actually need `param()`. - -8. Line 469: *"It does not reduce memory on PostgreSQL, because both `await` and `for await` read the same result the client has already loaded."* — I can restate the words, but not the point. The section exists to offer me `for await`, then tells me it buys nothing on my database. - -9. Line 471: *"You can `await` a result as many times as you like. You cannot mix `await` and `for await` on the same result."* — "a result" is a thing I never have. Every example on the page writes `await runtime.query(plan)` in one expression. Nothing shows the object you would hold and await twice. - -## Words and phrases I had to guess - -- **"plan"** — I guessed it is just the variable name the page chose for whatever `build()` returns, not a type. But line 434's anchor is `#executing-a-plan` while the heading reads "Running a built query", and line 456's anchor is `#recover-the-row-type-from-a-plan`. I guessed these are the same thing. -- **"contract"/"contract.prisma"** — carried over from part 1, fine. But line 389, *"so it is there only if your contract uses pgvector"*, made me guess that a contract can declare an extension. I do not know how. -- **`codecId`** (lines 311, 318, 400) — I guessed "codec" is Prisma's internal word for the thing that converts a database value to JavaScript. The page never says, it only apologises for the name. -- **`numeric`, `float8`, `varchar`, `int4`, `text`** (lines 382, 386, 405) — PostgreSQL type names. In my contract I write `Decimal`, `Float`, `String`, `Int`. I guessed at the mapping. The page never gives it, so I cannot tell which of *my* columns makes `sum()` return a string. -- **"relational-core"** in `@prisma/orm-postgres/relational-core/expression` (line 309) — I guessed it is an internal layer name I am not meant to think about. -- **"the version of `count` that does not overflow"** (line 381) — I guessed "overflow" means the 2^53 limit described at line 61, but those two places never point at each other. -- **`AsyncIterableResult`** (line 471) — guessed this is the type of the object `runtime.query()` returns, which would explain awaiting it twice. Only findable by following the link. - -## Places I asked "so what do I actually type?" and got no answer - -1. **Line 61**: *"To check for a code, catch the error and compare `error.code`."* No snippet. No `try { … } catch (e) { … }`. No word on whether `error.code` is typed, or whether I must narrow `unknown` first. This is the only error-handling instruction on the page and it has no code. -2. **Line 78**: *"You can also repeat the same `fns.raw` fragment inside `groupBy()`."* The Options table gives the signature `groupBy((f, fns) => expr)`, but there is no example. Do I have to call `.returns()` again on the repeated fragment? Do the two fragments have to match textually? Unanswered. -3. **Line 86**: `having()` rejects a `bigint` literal such as `1n`. But if my counts are big enough that I used `countBigInt()`, how do I filter on that count? No answer. -4. **Line 69**: nothing tells me which type id `EXTRACT(YEAR FROM …)` *should* have. The example passes `'pg/int4@1'` and then explains that the value is a string. Should I have written a `numeric` type id instead? I do not know what I am meant to type. -5. **Line 389**: *"it is there only if your contract uses pgvector"*. How do I make my contract use pgvector? No link, no line of schema. -6. **Line 204**: *"**Availability:** PostgreSQL and SQLite."* So on MySQL, what do I write instead of `returning()`? Not said, and not linked. -7. **Line 240**: can I mix the two `update()` forms — fixed values for some columns, expressions for others? Line 242 tells me to wrap a fixed value in `fns.raw` inside the callback, which implies no. It never says so plainly. -8. **Line 471**: what expression produces the "result" I can await repeatedly? `const result = runtime.query(plan)` with no `await`? Never shown. -9. **Line 333**: the one `param()` example binds a string with `'pg/text@1'` — exactly what line 393 says the builder infers on its own. So the example does not show a case where `param()` is needed. I finished the section without one. - -## Places that explain the inside when I only wanted the instruction - -- Line 173: *"Each value is sent as a query parameter, never pasted into the SQL. The column it goes into decides how it is stored and read."* I only needed the rule: "Never call `.returns()` or `param()` on an insert value." The mechanism is the whole first sentence. -- Line 469: *"because both `await` and `for await` read the same result the client has already loaded."* Driver-level detail. The instruction I wanted is "On PostgreSQL, use `await`." -- Line 309: *"`param` and `ResultType` are in different subpaths of the same package, so use the paths shown."* This explains the package layout to excuse itself. "Import each from the path shown" would do. -- Line 386–387: *"A `varchar` column is typed as `text`."* Both are `string` in JavaScript. I could not work out what this changes for me. -- Line 78: *"That failure comes back as PostgreSQL's own error, not a Prisma ORM code."* Useful only if the page then tells me what I *can* match on. It does not. - -## Could I do what the page is for, after one reading? - -Partly. I could write a grouped query with a count and a `having()` filter, an insert with `returning()`, an update with a values object, and a delete with a `where()`. Those four have clean, complete, copyable examples and I trust them. - -What I still would not know: - -- Whether `.returns()` makes TypeScript agree with the value I actually receive, and therefore whether my `Number(row.yearPlaced)` compiles. -- Which of my contract's column types (`Decimal`, `Float`, `Int`) make `sum()` and `avg()` hand back a string instead of a number. The table is written in PostgreSQL type names I do not use. -- How to catch and identify any of the six error codes the page names (`RUNTIME.DECODE_FAILED`, `ORM.COLUMN_UNKNOWN`, `RUNTIME.ENCODE_FAILED`, `ORM.MUTATION_DATA_MISSING`, `RUNTIME.ITERATOR_CONSUMED`), because there is no `try`/`catch` anywhere on the page. -- When `param()` is genuinely required, as opposed to redundant with the builder's own inference. -- How to group by a computed expression rather than by its alias, which the Options table offers and no example shows. -- How to insert repeatedly without rebuilding, or whether rebuilding is the only answer. - -One more thing that would bite me the day after reading: line 231 and line 281 both say an `update()` or `delete()` with no `where()` hits every row and "nothing warns you". That is the most dangerous fact on the page and it sits mid-paragraph in the prose, not in a `:::warning`, while the far less costly aggregate-overflow note gets its own callout box. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part1.round1.md b/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part1.round1.md deleted file mode 100644 index cd1d98358bd..00000000000 --- a/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part1.round1.md +++ /dev/null @@ -1,97 +0,0 @@ -Read once, top to bottom, as a Prisma 7 user. Blunt notes. - -## Sentences I could not restate after one reading - -1. **"Pass the contract with `contractJson` (a JSON contract you import) or `contract` (a contract value). Supply exactly one."** (line 23) - What stopped me: I do not know what a contract is. In Prisma 7 I had a schema and a generated client. The page never says what a contract is, where `./contract.json` and `./contract.d` come from, or what generates them. "A JSON contract you import" defines the word with the word. And I cannot tell when I would use `contract` instead of `contractJson` — the page gives the choice and no reason to pick. - Plainest wording I would have understood: "A contract is the generated description of your database shape. `prisma generate` writes `contract.json` and `contract.d.ts` into X. Import the JSON and pass it as `contractJson`. Pass `contract` instead only if Y." - -2. **"`verifyMarker` | `'onFirstUse'` or `false` | Whether the runtime reads the contract marker row on the first query and warns when it does not match."** (line 39) - What stopped me: "contract marker row". I have never heard of it. Which table is it in? Who writes it? What does "does not match" mean, and what do I do when I see the warning? This is the only mention on the page. - -3. **"If your contract uses types from an extension pack, pass the pack in `extensions` (for example `extensions: [pgvector]`)."** (line 26) - What stopped me: "extension pack" is undefined, and I do not know how to tell whether my contract uses one. `pgvector` is shown as a bare identifier with no import line. Where does it come from — `@prisma/...`? The `pgvector` npm package? - -4. **"`middleware` | Array of middleware | Middleware applied to every execution."** (line 38) - What stopped me: circular. It tells me middleware is middleware. No shape, no signature, no example, no link. "every execution" — execution of what, a query? - -5. **"`mode` | `'strict'` or `'permissive'` | The mode middleware reads on every execution. Defaults to `'strict'`."** (line 193) - What stopped me: this is the one MongoDB option that sounds like it changes behaviour, and the description explains it in terms of an internal mechanism ("the mode middleware reads") rather than telling me what strict does differently from permissive. After one reading I have no idea what setting this to `'permissive'` would let me do. - -6. **"Transactions are fully supported on PostgreSQL and not shipped on MongoDB."** (line 9) versus **"(see [Transactions (MongoDB)](#transactions-mongodb) ...)"** (line 260) - What stopped me: the intro says there are no MongoDB transactions, then a link points to a MongoDB transactions section. One of these is wrong, and I cannot tell which from here. Also "not shipped" — does that mean not yet, or never? I would say "MongoDB transactions are not supported in this release." - -7. **"Returns a `Promise`. The runtime is what runs built queries directly (`runtime.query(...)` for rows, `runtime.execute(...)` for writes that return none), opens connections (`runtime.connection()`), prepares statements (`runtime.prepare(...)`), and reports telemetry (`runtime.telemetry()`)."** (line 78) - What stopped me: five new APIs in one sentence, none of them explained or linked, plus "built queries" which is undefined at this point in the page. "opens connections (`runtime.connection()`)" is the worst of it — I just connected; what further connection is this and why would I want one? Split it into a list with a link per item. - -8. **"The first connection can happen implicitly, the moment any query builds the runtime, so a later explicit `connect()` collides the same way."** (line 233) - What stopped me: "the moment any query builds the runtime" reads backwards — I thought the runtime runs queries, not that queries build the runtime. Plainer: "Running any query connects the client. If you then call `connect()`, it fails, because the client is already connected." - -9. **"`connect()` accepts a binding, so you can defer the choice of database: `mongo({ contractJson })` then `await db.connect({ url, dbName })`."** (line 234) - What stopped me: "defer the choice of database" — why would I want to? No situation is given. Also this contradicts the `mongo(options)` Options table, which marks a binding as required ("One binding"). - -10. **"Returns an [`AsyncIterableResult`](#asynciterableresult): `await` it for an array, or `for await` to stream rows."** (line 283) - What stopped me: the type name. The link goes to part two, which I do not have, so at this point the page defines it only by how you consume it. I could guess, but I could not restate what it actually is. - -11. **"`runtime.execute(built)` is the sibling call for writes that return no rows; it resolves the statement's statistics."** (line 284) - What stopped me: "resolves the statement's statistics". What statistics? Row count? I would write "returns the number of rows affected" if that is what it is. - -12. **"`await using` requires `@types/node` (which augments `SymbolConstructor` with `asyncDispose`) or a `lib` that declares the symbol."** (line 155) - What stopped me: the parenthetical is TypeScript internals I do not need. And "or a `lib` that declares the symbol" does not tell me which lib value to set. Plainer: "`await using` needs `@types/node` installed, or `"lib": ["esnext.disposable"]` in your tsconfig." - -13. **"This is what lets you share one `MongoClient` between Prisma ORM and driver-level code."** (line 181) - What stopped me: "driver-level code" — I guessed it means code I write against the `mongodb` package directly, but the page never says so. - -## Words and phrases I had to guess - -- **"contract"** — guessed: the new name for the generated schema/client types. -- **"binding"** — guessed: the way you tell the client which database to talk to. The page uses it as a noun ("One binding" in the Required column) before defining it, and I had to read line 24 to work out that "Required: One binding" means "pass exactly one of these three rows". -- **"runtime"** — guessed: the connected thing that actually talks to the database, as opposed to the client object. -- **"built query" / "a built query" / `built`** — guessed: the object a builder returns when you call `.build()`. Never stated. -- **"extension pack"** — guessed: a plugin that adds support for a Postgres extension's types. -- **"contract marker row"** — guessed: a row Prisma writes somewhere to check the deployed database matches the contract. No idea if that is right. -- **"telemetry"** — guessed: query timings/metrics. -- **"pipeline builder"** — guessed: a MongoDB aggregation-pipeline API. Only appears as a link. -- **"`db.sql.public.tag`"** (line 95) — guessed: schema `public`, table `tag`. Never explained; it just appears mid-example. -- **"`.orm`, `.sql`, `.raw`, `.enums`, `.nativeEnums`, `.contract`, `.context`"** (line 45) — guessed at most of these. `.nativeEnums` versus `.enums` I could not guess at all. `.context` I could not guess at all. -- **"not shipped"** — guessed: not implemented in this release. -- **"no-op"** (line 181) — fine for me, but it is jargon sitting in the middle of the one paragraph about who closes what. - -## Places I asked "so what do I actually type?" - -1. **Where `contract.json` and `contract.d` come from.** Every example imports them. No example shows the command that produces them, and no link is offered. This is the first thing I would need and the page never says it. -2. **How to install.** `import postgres from '@prisma/orm-postgres/runtime'` — is that a package I install, and with what? Same for `@prisma/orm-mongo/runtime`. -3. **`middleware`.** I cannot write one. No signature, no example, no link. -4. **`extensions: [pgvector]`.** Where do I import `pgvector` from? -5. **`contract` (the non-JSON form).** No example anywhere on the page shows it being used. -6. **`runtime.connection()`, `runtime.prepare()`, `runtime.telemetry()`.** Named once in a sentence with no example and no link. -7. **`binding` as an object.** `{ kind: 'url' | 'pgPool' | 'pgClient', ... }` — the `...` is the part I need. What are the other fields for each kind? Same for MongoDB, where the page says "a `binding` object that names its kind" and never even lists the kinds. -8. **When to call `connect()` on PostgreSQL at all.** Line 27 says it is optional; line 94's example calls it. I do not know which one I should do. -9. **Can I call `db.runtime()` on PostgreSQL before connecting?** It is synchronous and the client connects lazily, so this is the obvious question. The page only tells me what happens after `close()`. -10. **What `close()` does to in-flight queries.** "Close the client and release its pool" — does it wait for running queries? -11. **`uri` versus `url` on MongoDB.** Both are connection strings. The only stated difference is that `dbName` is optional with one and required with the other. I would not know which to type, or why both exist. -12. **Catching the errors.** The page repeatedly says things reject "with an error whose `code` is `DRIVER.NOT_CONNECTED`". It never shows the import or the shape, so I cannot write the `catch` block. Is `DRIVER.NOT_CONNECTED` a string I compare against, or a constant I import? -13. **The MongoDB example's `db.query.from('posts')`** (line 297) — `'posts'` is a bare string with no explanation of where the name comes from, and `db.query` is described in the return table only as `.query`. - -## Places that explain the internals when I only wanted to know what to do - -- **"Whether the runtime reads the contract marker row on the first query"** (line 39) — tells me the mechanism, not whether I should ever turn it off. -- **"The mode middleware reads on every execution"** (line 193) — describes plumbing instead of the behaviour I would observe. -- **"which augments `SymbolConstructor` with `asyncDispose`"** (line 155) — compiler internals in place of the tsconfig line I need. -- **"The underlying driver connection is built lazily and asynchronously on first use."** (line 258) — this one earns its place, because it explains why I must `await`. But it is immediately followed by a repeat of the same point, and the whole remark says the PostgreSQL/MongoDB difference three times in one paragraph (line 258) after already saying it at line 104 and line 171. -- **"the moment any query builds the runtime"** (line 233) — internal ordering, where what I need is "running a query connects you". - -## Could I do what the page is for, after one reading? - -Partly. I could create a PostgreSQL client from a connection string, connect it, close it, and use `await using` — those four have clear examples. I could do the same on MongoDB, and I understood the ownership rule (if I pass my own pool or `MongoClient`, I close it; if I pass a URL, Prisma closes it). That rule is the clearest thing on the page. - -What I would still not know: - -- **What a contract is or how to get one.** This blocks every example. I cannot run a single code block on this page without already having `contract.json`, and the page does not tell me how to get it or link anywhere that does. The intro says the page "assumes a project that already exists", but a Prisma 7 project does not have a contract, and I am the exact reader who is upgrading. -- **How to run a query.** The only PostgreSQL query example is one line, `db.sql.public.tag.select('id', 'label').build()`, with no explanation of any part of it. I would not know whether to reach for `db.orm`, `db.sql`, `db.raw`, or `runtime.query`. -- **Whether MongoDB has transactions.** The page tells me both. -- **What `mode: 'permissive'` does.** -- **How to handle a connection error**, because I cannot write the check against `DRIVER.NOT_CONNECTED`. -- **What most of the client surface is.** Line 45 lists eleven members; the page documents five. - -One more thing that tripped me up specifically as a reader of both halves: the MongoDB **Return type is `MongoClient`** (line 199), and two lines below, an example imports a completely different `MongoClient` from the `mongodb` package (line 217). Two different things with the same name on the same screen. I read line 199 as "this returns the driver's client" and had to back up. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part1.round2.md b/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part1.round2.md deleted file mode 100644 index 186af5217b1..00000000000 --- a/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part1.round2.md +++ /dev/null @@ -1,89 +0,0 @@ -I read the page once, top to bottom, as a Prisma 7 user. - -## Sentences I could not restate after one reading - -**Line 23** — "Your contract is the `contract.prisma` file that replaced `schema.prisma`. Running `npx prisma contract emit` compiles it into two files: `contract.json`, read at run time, and `contract.d.ts`, read by TypeScript." - -This is the single most important fact on the page and it is buried in a bullet under `postgres(options)`. I stopped at "Your contract is" — the page says "the contract" seven times before this and never says what a contract is. Plainer: put this before the first heading, as its own short section: "Prisma 8 replaces `schema.prisma` with `contract.prisma`. Run `npx prisma contract emit` to generate `contract.json` and `contract.d.ts` in `src/prisma/`. Every client you create needs `contract.json`." - -**Line 25** — "A binding tells the client which database to use. Pass exactly one of `url`, `pg`, and `binding`, or leave all three out and pass one to [`connect()`](#connect). The `binding` form is the same three choices in one object: `{ kind: 'url', url }`, `{ kind: 'pgPool', pool }`, or `{ kind: 'pgClient', client }`." - -Three ideas in one bullet: what a binding is, when you may omit it, and a fourth option that repeats the first three. I could not work out why `binding` exists at all if `url` and `pg` already do the job. Plainer: "Tell the client which database to use in one of three ways: `url`, `pg`, or `binding`. Use `binding` when your code chooses between them at run time, because it is one object instead of three separate option names." - -**Line 28** — "Prisma ORM keeps a record in your database of which contract the database was set up for. With `verifyMarker: 'onFirstUse'`, the default, the first query checks that record, logs a warning if it is missing or does not match, and runs the query anyway." - -I do not know what "a record" is, where it lives, or what "the database was set up for" means. "Marker" appears only in the option name, never in the prose, so I cannot connect `verifyMarker` to "a record". The behaviour is also self-cancelling: it checks, warns, and runs anyway. So what do I do when I see the warning? Plainer: "Migrations write a marker row into your database recording which contract it matches. By default the first query compares that marker with your contract and logs a warning on a mismatch, then runs anyway. A warning usually means you have not run your migrations." - -**Line 96** — "Every error carries a `code`: to handle one, catch the error and compare `error.code` with the code's string." - -"the code's string" stopped me. Which string? Compare with what? Plainer: "Every error has a `code` property. Catch the error and compare `error.code` with the value shown here, for example `if (error.code === 'DRIVER.NOT_CONNECTED')`." Show the line of code. - -**Line 270** — "On MongoDB, `runtime()` returns a **`Promise`**: you must `await` it. The connection to MongoDB is opened on first use, which is why this call is asynchronous." - -The reason does not hold. Line 29 says PostgreSQL also opens its connection on first use, and line 121 says PostgreSQL's `runtime()` is synchronous anyway. So the stated reason explains nothing, and I now distrust both sentences. Either give the real reason or drop it and just state the difference. - -**Line 285** — "`(await db.runtime()).query(built)` runs any built MongoDB query, including one built by the [pipeline builder](/orm/reference/pipeline-builder) (`db.query`). The client itself has no `execute()` method." - -Two unrelated facts. The second one names something that does not exist, without saying what to use instead in the same sentence. I had to read line 287 to learn `runtime.execute` is the answer. - -**Line 187** — "That is what lets you use one `MongoClient` both through Prisma ORM and in code you write against the `mongodb` package directly." - -Follows three sentences about who closes what. By the time I reached "That is what lets you", I had lost which fact "that" points back to. - -## Words and phrases I had to guess - -- **"runtime"** — I guessed "the connected object you actually run queries on". The page confirms this at line 89, which is after the word has been used eleven times including in the page title. Define it in the intro. -- **"binding"** — I guessed "the database connection details". Never stated plainly. -- **"built query"** / **"`.build()`"** (line 90) — I guessed you chain builder calls and finish with `.build()` to get a plain object. No example of a chain appears before it is used at line 112. -- **"`postgres`"** — I guessed `Contract` is the exported type from `contract.d` and the generic is what makes `db.sql.public.tag` typed. The page never says the generic is required or what happens if you leave it out. -- **"`.nativeEnums` holds the enum types defined in PostgreSQL itself"** (line 54) — I could not guess how this differs from `.enums`, or when I would use it. No example. -- **"`.context` is internal: the query builders read it, and you do not call it"** — I guessed it is an implementation detail I should ignore. Then why is it in a reference list I am reading? -- **"`mode`: `'strict'` or `'permissive'`. Passed through to your middleware, which decides what to do with it"** (line 202) — I guessed nothing. This option does literally nothing on its own and the description says so. It tells me neither what to pass nor why. -- **"`middleware`: Array of middleware"** (lines 43, 201) — the type restates the name. I still do not know the shape of one item. -- **"marker"** — see above. -- **"`.sql` is the SQL query builder, keyed by table name"** vs **"`.orm` holds your models by model name"** — I guessed `Tag` the model becomes `tag` the table. Only a code comment at line 111 tells me. On MongoDB the same rule is in another code comment at line 300. A naming rule this important should not live in comments. - -## Where I asked "so what do I actually type?" - -1. **`connect()` with a binding.** Lines 95 and 246 both say "`connect()` accepts a binding, so a client you created without one can be given its database here." There is no example anywhere on the page. Is it `db.connect({ url })` or `db.connect({ kind: 'url', url })`? This is the only way to use the documented "leave all three out" path, and it is never shown. -2. **Middleware.** Both `middleware` rows link away. One two-line example of a middleware function would cost less than the link. -3. **`mode`.** No example, no meaning, no guidance. -4. **`binding` on MongoDB.** Line 184 says the choices are `{ kind: 'url', url, dbName }` or `{ kind: 'mongoClient', client, dbName }`, but the text right above offers `uri` as a separate option. If I am on the `uri` path, what do I put in `binding`? Unanswerable from the page. -5. **`url` plus `dbName` on MongoDB.** Line 192 says "Pass exactly one of `url`, `uri` plus `dbName`, `mongoClient` plus `dbName`, and `binding`." The very next example (line 223) passes `url` **and** `dbName`. I read the rule as forbidding that. Line 185 explains the override, but the rule and the example contradict each other on the page as written. -6. **`extensions`.** Line 27 shows the import and the array. It does not say to run `npm install @prisma/orm-extension-pgvector` first. I would have hit a missing-module error. -7. **Closing the Mongo runtime.** Line 253 says `MongoRuntime` is "(`query`, `execute`, and `close` only)". The PostgreSQL runtime has five calls and none is `close`. So on MongoDB I have `db.close()` and `runtime.close()` and the page never says which one I should call, or what the other does. -8. **`runtime.connection()`.** Line 92 says it "takes one connection out of the pool". It does not say how I give it back, or whether it does that for me. The link points to a section I do not have, but the one-line summary invites the question and leaves it open. -9. **What replaces `prisma.user.findMany()`.** As a v7 user this is my first question on any v8 page. The link at line 23 is the right answer, but it sits at the end of a bullet about file names. Put it in the intro. - -## Where the page explains the machinery when I only wanted to know what to do - -- **Line 28, `verifyMarker`.** I do not need to know Prisma writes a record in my database. I need to know: leave the default on, and if you see the warning, run your migrations. -- **Line 55, `.context` is internal: the query builders read it, and you do not call it.** Telling me how the builders talk to each other. Cut it, or cut the whole entry. -- **Line 270, "which is why this call is asynchronous."** An internal reason that contradicts line 29 and line 121. -- **Line 208, "Prisma ORM's `MongoClient` type, not the driver's."** This is a real trap, so it belongs somewhere — but the example at lines 228 to 235 imports the driver's `MongoClient` and assigns Prisma's to `db`, with both names live in one ten-line file, and never comments on it. Either name the type differently or warn me in the example. -- **Line 26, pool defaults.** "The pool it creates uses `connectionTimeoutMillis: 20000` and `idleTimeoutMillis: 30000`." The numbers are useful in the options table, where they already are. In the prose they are just internals. - -## Other things I noticed - -- **Line 297.** The heading is "Run a query built by the pipeline builder" but the anchor is `#run-a-pipeline-builder-plan`. The word "plan" does not appear anywhere on the page. Anyone who lands on that link expects a different section. -- **Line 210 vs line 300.** Line 210 says `.orm` "holds your models by model name" and shows `db.orm.users`. Line 300 says the collection name is "your model name with a lowercase first letter". If my model is `User`, is the ORM key `user` or `users`? The example says `users`, the rule says `user`. One of these is wrong, and I cannot tell which. -- **Line 9.** "Transactions are fully supported on PostgreSQL. They are not available on MongoDB." As a v7 user who uses `$transaction` on MongoDB today, this is alarming and the page defers the whole answer to a section I do not have. One sentence naming the replacement would stop me panicking. - -## Could I do what the page is for, after one reading? - -Partly. - -**I could:** create a PostgreSQL client from a connection string, run one `select`, and close it. The four examples at lines 62, 109, 147 and 166 are clear, complete, and copy-pasteable. Same for the basic MongoDB client. This is the page's main job and it does it. - -**I could not:** - -- Create a client without a binding and supply the database later at `connect()`. No example exists. -- Write a middleware, or decide anything about `mode`. -- Work out whether `db.orm` keys are singular or plural, because the page says both. -- Decide whether to call `runtime.close()` or `db.close()` on MongoDB. -- Work out what `binding` is for on MongoDB when I am using `uri`. -- Know whether `postgres` is required or optional. -- Know what to do when the `verifyMarker` warning fires. -- Know how `.nativeEnums` differs from `.enums` in practice. - -The largest single fix is moving the contract explanation from line 23 into the introduction. Everything on the page depends on `contractJson`, and a reader who does not already know what a contract is spends the whole first section not understanding the first argument of the first function. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part1.round3.md b/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part1.round3.md deleted file mode 100644 index c682749f4f5..00000000000 --- a/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part1.round3.md +++ /dev/null @@ -1,93 +0,0 @@ -Read once, top to bottom, as a Prisma 7 user. Report below. - -## Sentences I could not restate after one reading - -1. **Line 9:** "Prisma ORM 8 replaces `schema.prisma` with `contract.prisma`. Run `npx prisma contract emit` to compile that file into `contract.json`, which your app reads at run time, and `contract.d.ts`, which TypeScript reads." - Four new things in two sentences: a renamed schema file, a new command, and two generated files with different consumers. I had to re-read to work out which file goes where. Plainer: "Prisma ORM 8 renames `schema.prisma` to `contract.prisma`. Run `npx prisma contract emit`. It writes two files next to it: `contract.json`, which your app imports, and `contract.d.ts`, which gives you the TypeScript types." - -2. **Line 11:** "The runtime is the connected object you run queries on. `db.runtime()` returns it." - `db` has not appeared yet. I do not know what `db` is at this point in the page. I guessed it was a client, which turned out right nine paragraphs later. - -3. **Line 13:** "Availability is stated per method: where the two databases differ, or where a method exists on only one of them, the Remarks say so." - "the Remarks" is capitalised as if it is a thing I already know. I have not seen a Remarks section yet. Plainer: "Each method below has a Remarks list. If a method behaves differently on PostgreSQL and MongoDB, or only exists on one, that list says so." - -4. **Line 13:** "To run one there, share one `MongoClient` between Prisma ORM and your own `mongodb` driver code and use a driver session." - Three instructions compressed into one sentence, and I cannot act on any of them. What is "a driver session"? How do I "share" a client — is that the `mongoClient` option? The page does not say here, and the link goes to a section I do not have. - -5. **Line 27:** "Pass `contract` instead only when you already hold a contract object in your own code, because Prisma ORM converts it to the same JSON." - I do not know what "a contract object" is or how I would come to hold one. Nothing on the page produces one. I could not restate this except as "ignore this option." - -6. **Line 59:** "In `.nativeEnums` each member's name is its own value, because a PostgreSQL enum type has values and no separate names." - I read this three times. "each member's name is its own value" is circular phrasing. Plainer: "`.nativeEnums` maps each value to itself — `db.nativeEnums.public.AalLevel.aal1` is `'aal1'` — because PostgreSQL enums have no separate names." - -7. **Line 140:** "It waits for a connection already being opened, then releases the pool it created. After `close()`, `connect()` rejects and `runtime()` throws an error whose `code` is `DRIVER.NOT_CONNECTED` (`Postgres client is closed`). When you bound the client with `pg` (your own pool or client), close that instance yourself, because the client only owns pools it created from a `url`." - One bullet carrying four separate facts: in-flight connect handling, post-close behaviour of two different methods, and ownership rules. "waits for a connection already being opened" is also the only mention of a race I did not know existed, and it is buried mid-sentence. - -8. **Line 208:** "`mode` | `'strict'` or `'permissive'` | No built-in behaviour. Your own middleware can read it." - I could not restate the point of this option. If it does nothing, why is it in the table, and how does middleware read it? Neither is answered. - -9. **Line 252:** "Running any query connects the client too, so if you then call `connect()`, it fails for the same reason. PostgreSQL raises the same code from the same situation." - The second sentence contradicts what I took from line 34 ("`connect()` is optional: call it to open the connection up front and fail early"). Now I learn that on PostgreSQL too, a query followed by `connect()` throws. That is a trap, stated as an aside in the MongoDB section, in a page where the PostgreSQL section came first. - -10. **Line 309:** "`runtime.close()` closes only the connection Prisma ORM opened and does not mark the client closed, so the client then looks usable and is not." - Useful warning, badly phrased. "looks usable and is not" made me stop. Plainer: "After `runtime.close()` the client will accept further calls and then fail." - -## Words and phrases I had to guess - -- **"contract"** — guessed: the new name for the schema, and also for the compiled artefact, and also for a TypeScript type (``), and also for a runtime object (`.contract`, `contract:` option). Four uses of one word, never distinguished. -- **"runtime"** — guessed: a connection handle. The word also names the import path (`@prisma/orm-postgres/runtime`), which is not the same thing and is never flagged. -- **"binding"** — guessed: a wrapper object so you can pick the connection source with an `if`. The word itself told me nothing. -- **"marker"** in `verifyMarker` — guessed: the row the migration writes. The option name only makes sense after reading the internals note. -- **"built query" / `.build()`** — guessed: a builder produces a plain object you hand to `runtime.query()`. Line 95 defines it, but it is used at line 95 before I have ever seen a builder chain. -- **"middleware"** — guessed from Express. The page says "an object with a name and one or more hooks" but never lists which hooks exist. I know `beforeQuery` only because it is in the example. -- **"telemetry"** — guessed: timings? Row counts? "reports how the most recent query went" is not a definition. -- **"extension packages"** — guessed: npm packages you import and pass in an array. The pgvector example carried this, not the table row. -- **`Symbol.asyncDispose` / `await using`** — I have not used these. The page assumes I have. -- **`AsyncIterableResult`** — guessed from the name; the definition is in the half I do not have. - -## Places I asked "so what do I actually type?" - -- **Line 9, "what replaces `prisma.user.findMany()`"** — the page raises the exact question I came with and sends me elsewhere. One line showing the new call would have answered it. -- **Line 27, the `contract` option** — no example anywhere on the page. If I have a contract object, what does the call look like, and where do I get `Contract` from without a `.d.ts`? -- **Line 33, "so run them"** — which command? `npx prisma migrate dev` still? Prisma 8 renamed the schema file, so I do not assume the migrate commands survived. -- **Line 32, middleware** — `isForbidden` is undefined, `query` has no shape, and no hook besides `beforeQuery` is named. I cannot write a middleware from this. -- **Line 46, `poolOptions`** — no example. I can guess from the type, but it is the one option with defaults worth seeing in context. -- **Line 13, MongoDB transactions** — no code at all. This is the loudest gap on the page: it tells me transactions are unavailable, tells me there is a workaround, and shows me none of it. -- **Line 70, `import contractJson from './contract.json' with { type: 'json' }`** — this syntax fails in most setups I have. What `module`/`target` in `tsconfig.json`, what Node version? Not said, while the far less risky `await using` does get a tsconfig note. -- **Line 69, `from './contract.d'`** — importing from a path ending in `.d` looks wrong. Is it? The page never says. -- **Line 72 vs line 241** — `url: process.env.DATABASE_URL` with no `!`, but `new MongoClient(process.env.MONGODB_URL!)` with one. One of these does not typecheck. Which? -- **Line 9 says the files live in `src/prisma/`, every example imports `./contract.json`** — so what is the real import path from my `src/index.ts`? -- **Line 97, `runtime.connection()`** — "Call `connection.release()` when you are done." No example, and the link points into the half I do not have. -- **PostgreSQL `runtime.execute()`** — mentioned at line 96, never shown. Every PostgreSQL example is a read. -- **Line 117, `db.sql.public.tag`** — line 58 told me `.sql` is "keyed by table name". It is not; there is a schema segment first. The schema segment appears only in a code comment. Same for `db.nativeEnums.public['AalLevel']` — bracket notation here, dot notation for `.enums.public.Role`, with no explanation of why. -- **Line 61 vs line 98** — line 61 lists `prepare()` as a member of the *client*; line 98 describes `runtime.prepare(...)`. Which object do I call it on? -- **Line 95 links to `#runtimequery`** — the only `runtime.query()` section on this page is under MongoDB. Following that link from the PostgreSQL section lands me on MongoDB documentation. -- **Line 101, "Every error has a `code` property"** — where is the list of codes? I see three. -- **Line 216** — "The [example schema] sets `@@map("users")` on `User`, which is why the key here is `users`." I cannot see that schema, so the sentence explains nothing to me. - -## Places explaining internals when I wanted instructions - -- **Line 33, `verifyMarker`:** "Your migrations write a row into your database recording which contract that database matches. With `verifyMarker: 'onFirstUse'`, the default, the first query compares that row with your contract, logs a warning if the row is missing or does not match, and runs the query anyway." I wanted: leave this alone; if you see the warning, run your migrations. The row, the comparison, and the timing are machinery. -- **Line 27:** "because Prisma ORM converts it to the same JSON" — internal detail justifying a recommendation I would have taken anyway. -- **Line 29 and 189:** "because it is one object instead of three separate option names" — that is a description of the API shape, not a reason to pick it. -- **Line 309:** the explanation of *why* `runtime.close()` is wrong ("does not mark the client closed") is longer than the instruction ("Call `db.close()`, not `runtime.close()`"). -- **Line 214:** "Prisma ORM's `MongoClient` type, not the driver's." True and important, but it reads as an implementation note where I expected to be told what to do about the name clash. - -## Could I do what the page is for? - -Partly. After one reading I could create a PostgreSQL client from a connection string, run a read query, and close it, because that exact path is shown three times. The same is true for creating a MongoDB client. - -What I would still not know: - -- How to write anything to either database. There is no `execute()` example on the page. -- How to run a transaction on MongoDB, which line 13 promises and never delivers. -- Whether to call `connect()` at all. Line 34 says it is optional; line 252 says calling it after a query throws. I do not know the safe pattern. -- How to write a middleware, or which hooks exist. -- What `mode` does. -- What `telemetry()` returns. -- Whether `db.sql` is keyed by table or by schema-then-table. The prose and the examples disagree. -- Whether `prepare()` is on the client or the runtime. -- What tsconfig I need for the JSON import in every single example. -- What `prisma.user.findMany()` becomes — the question the page opens with and never answers. - -The biggest structural problem: the page defines `runtime`, `built query`, `contract object`, and `Remarks` *after* it first uses them, and it puts the PostgreSQL/MongoDB `connect()` contradiction in the second section only. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part1.round4.md b/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part1.round4.md deleted file mode 100644 index 428013c01d4..00000000000 --- a/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part1.round4.md +++ /dev/null @@ -1,83 +0,0 @@ -I read it once, top to bottom, as a Prisma 7 user. - -## Sentences I could not restate after one reading - -**Line 11:** "`db` is the client `postgres(...)` or `mongo(...)` returns. The runtime is the connected object you run queries on, and `db.runtime()` returns it. The import path `@prisma/orm-postgres/runtime` is a package name and is not that object." - -Three different things called "runtime" in three sentences, and the third one is a warning about a confusion I did not yet have. It made me think I had missed something. Plainer: "`db` is the client. `db.runtime()` gives you the object that actually runs queries. (The `/runtime` in the import path is unrelated — it is just where the package lives.)" - -**Line 13:** The whole paragraph. It is one block containing: what the page covers, that each method has a Remarks list, that Remarks note database differences, that transactions work on Postgres, that they do not work on Mongo, the workaround for Mongo, and four outbound links. I cannot hold that. Split it: a sentence on scope, a sentence on how to read the Remarks lists, then a short "Transactions are PostgreSQL-only" note. - -**Line 26:** "The `` type argument is what types `db.orm` and `db.sql`. Pass it yourself whenever you pass `contractJson`, because TypeScript cannot read a type out of a JSON file. It is inferred only on the `contract` path." - -"the `contract` path" — I had to reread twice to work out this means "when you use the `contract` option instead of `contractJson`". I first read "path" as a file path, which is exactly the wrong reading on a line about JSON files. Say "It is inferred only when you use the `contract` option." - -**Line 27:** "A `binding` is one object that names the source and carries it: `{ kind: 'url', url }`, `{ kind: 'pgPool', pool }`, or `{ kind: 'pgClient', client }`." - -"names the source and carries it" is a riddle. I understood it only from the code. Plainer: "A `binding` wraps any of the three choices in one object, with a `kind` field saying which it is." - -**Line 31:** "`verifyMarker: 'onFirstUse'`, the default, makes the first query check that the database was set up for your contract." - -"marker" is never explained. I do not know what a marker is, where it lives, or how it got there. I can guess it is a row or table Prisma writes during migration, but the page never says. Also the option name says "verify marker" while the description says "check that the database was set up" — those do not obviously connect. - -**Line 57:** "A PostgreSQL enum type has values and no separate names, so each value is its own name: `db.nativeEnums.public['AalLevel'].nameOf('aal1')` is `'aal1'`." - -This documents a method whose entire purpose is to return its own argument. After one reading I could not tell why it exists or when I would call it. If it is only there for API symmetry with `.enums`, say that. - -**Line 72:** "The line `import contractJson from './contract.json' with { type: 'json' }` is what `prisma orm init` writes, and it needs Node.js 22.18 or newer and TypeScript set up as `prisma orm init` sets it up." - -"TypeScript set up as `prisma orm init` sets it up" tells me nothing actionable. I have an existing project; `prisma orm init` already ran, or did not. I do not know which `tsconfig.json` settings are required. Name the settings. - -**Line 208:** "`mode` | `'strict'` or `'permissive'` | No | Defaults to `'strict'`. Only your own middleware reads it." - -I stopped dead. If only my own middleware reads it, what does Prisma do with it, and what do the two words mean? This reads like an option that does nothing, which cannot be right. Plainer: "Prisma ignores this value. It is passed through to your middleware so your own code can branch on it." - -**Line 309:** "Call `db.close()`, not `runtime.close()`. `db.close()` marks the client closed and then closes the `MongoClient` Prisma ORM created. After `runtime.close()` the client still accepts calls and then fails." - -Two ideas jammed together: an instruction, and an explanation of internal bookkeeping. The last sentence describes a bug-shaped behaviour without telling me what to do about it. "Always call `db.close()`. `runtime.close()` leaves the client looking open, and later calls fail." would be enough. - -## Words and phrases I had to guess - -- **"contract"** — I guessed it is what `schema.prisma` used to be, renamed. The page tells me the file was renamed but never says what a contract *is*. -- **"marker"** (line 31) — guessed: something Prisma writes into the database during migration to record which schema version is applied. -- **"binding"** — guessed: a discriminated union wrapping the connection-source options. -- **"middleware"** — line 30 gives an example, so I got the shape. I guessed the eight hook names from their words alone. I do not know when `beforeCompile` fires versus `beforeQuery`, what `interceptQuery` intercepts, or whether `onRow` runs per row (I assume yes). -- **"built query"** (line 94) — guessed: the object `.build()` returns. The page does say this, but the link text next to it says "Running a built query" while the anchor is `#executing-a-plan`, and the Mongo heading anchor is `[#run-a-pipeline-builder-plan]`. So "built query" and "plan" appear to be the same thing under two names, and I had to guess that. -- **"the `contract` path"** — guessed: "the `contract` option". -- **"`.context` is internal"** (lines 58, 218) — guessed: do not touch. Then why list it? -- **"`AsyncIterableResult`"** (line 284) — guessed from the name plus the sentence. The link goes to part 2, which I do not have. - -## Places I asked "so what do I actually type?" - -1. **`runtime.connection()`** (line 96). "Call `connection.release()` when you are done" — I have no code shape. Do I `const connection = await runtime.connection()`? Does it return a promise? Do I run queries on `connection.query(...)`? The link points to a section in the half I do not have, but this is the only lifecycle call on the page with no example and an unknown signature. -2. **`runtime.prepare(...)`** (line 97). Literally written with an ellipsis. I do not know what goes inside. -3. **A plain read.** Line 9 promises `db.orm.public.User.where(...).all()`, and then every single example on the page uses `db.sql.public.tag.select(...).build()`. Not one example uses `.orm`. After reading the whole client-lifecycle section I still cannot write the Prisma 7 `findMany` I came for. -4. **The `Contract` type.** Examples write `import type { Contract } from './contract.d'`. The page never says `contract.d.ts` exports a type named `Contract`, so I do not know whether that name is fixed, configurable, or my own alias. -5. **Closing a pool you supplied.** Lines 28, 84, 141 all tell me I close it myself. No example shows the order — do I call `db.close()` first, then `pool.end()`? Does `db.close()` on a supplied pool even do anything? Same gap on Mongo (lines 193, 243). -6. **Running a Mongo write.** Line 285 says use `runtime.execute(built)` and that there is no `execute()` on the client. No example. -7. **The `tsconfig.json` for `await using`** (line 166). "`@types/node` installed, **or** `"lib": ["esnext.disposable"]`" — I cannot tell if the second replaces my existing `lib` array or adds to it. -8. **Extensions** (line 29, 45). The remark says import from `@prisma/orm-extension-pgvector/runtime` and pass `extensions: [pgvector]`. The table calls the type "Array of extension packages". So do I pass the package, or the thing its `/runtime` subpath default-exports? Those are different, and the table contradicts the example. - -## Places that explain the internals when I only wanted to know what to do - -- **Line 11**, the whole "the import path is a package name and is not that object" aside. -- **Line 31**, "Prisma ORM logs a warning and runs the query anyway" — plus the concept of a marker at all, when what I need is "run `npx prisma db update`". -- **Line 57**, `nameOf` returning its own argument, and the reasoning about PostgreSQL enums having no separate names. -- **Lines 58 and 218**, "`.context` is internal." Do not document it, then. -- **Line 139**, "If a connection is being opened when you call it, `close()` waits for that to finish, then releases the pool it created." I only needed "`close()` is safe to call at any time." -- **Line 309**, "marks the client closed and then closes the `MongoClient` Prisma ORM created." -- **Line 192**, "That is the only difference between `uri` and `url`." The page spends a long remark on a distinction that reads like a historical accident. What I want is a one-line rule: "Use `url`. Use `uri` only if you must pass `dbName` separately." - -## Could I do what the page is for, after one reading? - -Partly. I could create a Postgres client from a connection string, connect it, close it, and use `await using`. The Postgres examples are concrete and I could copy them. - -What I still could not do: - -- **Write an actual query.** Every example builds a SQL builder query and calls `.build()`. The page never shows a completed read through `db.orm`, which is the thing line 9 told me I came for. -- **Use a pooled connection or a prepared statement.** Both are listed as runtime calls with no signature I can type. -- **Run a Mongo write**, or a Mongo transaction. Line 13 tells me to "pass your own `MongoClient` with the `mongoClient` option and use the driver's `session.withTransaction(...)`" and sends me elsewhere for the code. That is the single most likely reason a Mongo user opens a page called "Transactions and runtime", and it is a link. -- **Decide about `verifyMarker` or `mode`,** because I do not know what either one controls. -- **Set up a new project.** The page assumes `prisma orm init` already ran and refers to its output three times ("what `prisma orm init` writes", "TypeScript set up as `prisma orm init` sets it up", "the same way `prisma orm init` does") without saying what any of it is. That is fair for a reference page, but then it should link to the setup page from line 72, not just name the command. - -One more thing. The heading structure repeats — `connect()`, `runtime()`, `close()`, and `await using` all appear twice, once per database — and the only way I can tell which one a link points at is a trailing `-1` in the anchor (`#connect-1`, `#runtime-1`, `#close-1`). Clicking `[Transactions (MongoDB)](#transactions-mongodb)` from line 269 is fine, but `#connect-1` gives me no clue it means the MongoDB one. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part2.round1.md b/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part2.round1.md deleted file mode 100644 index d73e3bc78d6..00000000000 --- a/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part2.round1.md +++ /dev/null @@ -1,92 +0,0 @@ -Read once, top to bottom, as a Prisma 7 user. - -## Sentences I could not restate after one reading - -1. `"- Inside the callback, query through the `tx` handle, not `db`: `tx.orm` for models, `tx.sql` with `tx.query(...)` (rows) or `tx.execute(...)` (writes that return none) for queries built with the SQL builder, and `tx.enums` and `tx.nativeEnums` for enum members."` - Five ideas in one sentence, and it introduces `tx.enums` and `tx.nativeEnums` with no explanation of what either is or how they differ. "writes that return none" made me stop: none of what? Plainer: "Query through `tx`, not `db`. `tx.orm` holds your models. Build SQL with `tx.sql`, then run it with `tx.query(...)` to get rows back, or `tx.execute(...)` when the write returns no rows." Then define the enum properties separately, or drop them. - -2. `"`connection.destroy(reason)` evicts that one connection from the pool instead of reusing it: Prisma ORM passes a truthy error to `pg`'s `PoolClient.release(err)`, which tells the pool to drop the client."` - I wanted to know what `destroy` does to me. Instead I got the node-postgres call Prisma makes internally. Plainer: "`destroy(reason)` throws the connection away instead of returning it to the pool." - -3. `"`reason` is optional and advisory: the pool only reads whether it is set."` - I read this three times. I think it means the error I pass is never looked at, only its presence. But then why does the example pass `new Error('connection no longer trusted')`? So what do I actually type — `destroy()` or `destroy(someError)`? - -4. `"As designed, it will require a MongoDB replica set and must not silently degrade to non-transactional execution on a standalone server."` - "must not silently degrade" is a requirement written for whoever builds this, not for me. I cannot act on it. The whole paragraph at line 219 describes software that does not exist; the note above it already told me that. - -5. `"Same result as `runtime.prepare(...)`, but the callback receives **two** arguments, `(sql, params)`, because the client has no closed-over SQL builder for you to reach for."` - "closed-over" is a compiler word explaining why the API is shaped this way. I only needed: "off the client, the SQL builder is handed to you as the first argument." - -6. `"A raw query ending with `.affectedCount()` prepares into a `PreparedExecution` instead, consumed with `execute(target, params)`, which resolves the statement's statistics."` - Three undefined things at once: "raw query", `.affectedCount()`, and "the statement's statistics". What statistics? A row count? There is no example, no options table, and no return type for `PreparedExecution` anywhere on the page. - -7. `"The runtime also checks the signal between rows, and passes it into the calls that read each value."` - Internals. And the next clause tells me the phase can be `'stream'`, `'encode'`, or `'decode'` "depending on where it landed", but never says which is which. If I have to branch on `details.phase`, I still cannot. - -8. `"It is the label middleware reads to tell a top-level query from one inside a dedicated connection or a transaction."` - "middleware" appears here and in the options table and nowhere else on the page. I do not know what middleware is in this product, how to write one, or how it gets this label. Nothing tells me why I would ever set `scope` myself, and the sentence after says setting it does nothing except change the label. - -9. `"After a query, it returns an object of the shape `{ lane, target: 'postgres', fingerprint, outcome, durationMs? }`"` - `lane` and `fingerprint` are never explained, and they are two of the five fields. I cannot use this object. - -10. `"A result is consumed once per mode."` - "mode" is undefined. From the next clause I worked out that the two modes are `await` and `for await`, but the sentence alone told me nothing. - -11. `"On PostgreSQL, a query run through the client buffers its rows and hands its pooled connection back before you read them, so a result you hold or iterate slowly no longer keeps a connection checked out."` - One long sentence about pool mechanics. The part I care about is the last clause. "no longer" also implies a change from version 7 without saying so. - -## Words and phrases I had to guess - -- `pg/text@1`, `pg/int4@1` — I guessed these are Postgres type names with a version number. I have no idea what the id is for uuid, timestamp, numeric, or an array, and the page never links a list. -- "type id" — guessed it means a string naming a column type. -- `@prisma/orm-postgres/family-runtime` — "family-runtime" means nothing to me. Guessed it is just where lower-level helpers live. -- "a bare transaction context" — guessed: an object with only `query` and `execute`. -- "or a standalone SQL builder" — guessed there is some way to make a SQL builder without a client. The page never says how. -- `Promise` — guessed `R` is whatever my callback returns. -- "plan" — the anchor `#run-a-sql-builder-plan-with-txsql-and-txexecute`, `SqlQueryPlan`, and `(params) => plan` all say "plan", while the prose says "a built query". I guessed they are the same thing. Pick one word. -- `db.connect()` — appears at line 135 with no introduction. I already know `db.runtime()` from the first half. I guessed `connect()` opens the connection and returns the same runtime, mostly because line 287 says calling both is an error. -- "lane", "fingerprint" — no guess. I have nothing. -- `all()` and `createAll()` — assumed these came from the first half. `createAll()` being listed under "the calls that run a read query" still looks wrong to me. - -## Where I asked "so what do I actually type?" - -- **Isolation levels.** `"There are no `isolationLevel`, `timeout`, or `maxWait` options"`. In version 7 I set `isolationLevel: 'Serializable'` on a transaction. The page tells me the option is gone and then stops. Do I run `SET TRANSACTION ISOLATION LEVEL` myself through `tx.execute`? Through the manual connection? It does not say. -- **Timeouts.** Same sentence removes `timeout` and `maxWait`. Is `signal` the replacement? The page never connects them. -- **Where `runtime` comes from** in "Manual connection and transaction control". Line 182 starts `const connection = await runtime.connection();` with no preceding line defining `runtime`. Same in the `runtime.prepare` examples. -- **Type ids.** I cannot prepare a statement on anything but text without knowing the id, and there is no list or link. -- **`PreparedExecution`.** No example. I do not know what `execute(target, params)` resolves to, so I cannot write the line after it. -- **Nested transactions.** `"transactions do not nest"`. My version 7 code calls a service function that opens its own transaction. What do I do instead — pass `tx` down? The page names the limit and offers nothing. -- **Retries.** No mention of what to do when a transaction fails and should be retried. -- **`RuntimeExecuteOptions` reach.** `"Every runtime `query(...)` and `execute(...)` call accepts a `RuntimeExecuteOptions` object as its second argument."` Does `tx.query` take it? `connection.query`? `ps.query`'s second argument is already `params`, so how do I cancel a prepared statement? -- **Which transaction API to pick.** Three levels are announced, but nothing says when `withTransaction` beats `db.transaction`, or when manual control beats either. "A lower-level transaction helper you import directly" is not a reason to use it. -- **Error codes.** `RUNTIME.TRANSACTION_CLOSED`, `RUNTIME.PREPARE_UNUSED_PARAM`, `RUNTIME.ABORTED`, `RUNTIME.ITERATOR_CONSUMED`, `DRIVER.ALREADY_CONNECTED` all appear as bare strings. Nothing shows me how to read `code` off a caught error, and there is no link to a code list. - -## Where it explains the inside when I wanted the outside - -- The `pg` / `PoolClient.release(err)` explanation in `destroy`. -- "the client has no closed-over SQL builder for you to reach for". -- "The runtime also checks the signal between rows, and passes it into the calls that read each value." -- The buffering paragraph at line 434. -- The whole MongoDB planned-design paragraph at line 219. - -## Two things that look like mistakes - -- The heading `##### Run a SQL builder query with `tx.sql` and `tx.execute`` carries the anchor `#run-a-sql-builder-plan-with-txsql-and-txexecute`. Text and anchor disagree. -- The "Options" tables under "Manual connection and transaction control" list method names in a Name/Type/Required table with `—` in Required for every row. Those are not options, and the table gives me no information. -- The return type table says `ps.query` returns `AsyncIterableResult`, but the example does `await ps.query(...)` and then `inTx[0]!.id`. So an awaited result is an array. Nothing states that plainly. - -## Could I do what the page is for, after one reading? - -Partly. - -I could write a `db.transaction(async (tx) => ...)` and port my version 7 `$transaction([...])` array. The diff at line 91 is the clearest thing on the page and it told me exactly what to do. - -I could not: - -- Set an isolation level or a timeout. Version 7 code of mine does both. -- Restructure the nested transactions my service layer has today. -- Prepare a statement for any parameter type other than text. -- Use `PreparedExecution` at all. -- Do anything with `scope`, or with the telemetry object's `lane` and `fingerprint`. -- Decide between `db.transaction`, `withTransaction`, and manual connections. -- Handle the abort phases, because I cannot tell `'encode'` from `'decode'`. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part2.round2.md b/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part2.round2.md deleted file mode 100644 index 4ab9be491da..00000000000 --- a/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part2.round2.md +++ /dev/null @@ -1,88 +0,0 @@ -Read once, top to bottom, as a Prisma 7 user. - -## Sentences I could not restate after one reading - -**1. Line 3 + line 109 + line 114 — a direct contradiction.** -> "Use `withTransaction(...)` when you hold a runtime but no client." - -then - -> "Build queries with the client's `db.sql` and run them through `tx.query(...)`" - -Both examples use `db.sql`. So the case the section tells me it is for — no client — is the one case the examples show is impossible. I do not know when I would ever be holding a runtime and no `db`. Plainest version I would have understood: say the actual situation (a helper that receives a runtime as an argument?) and show it without `db`, or drop the claim. - -**2. Line 11 — too many ideas in one bullet.** -> "`tx.enums` holds the members of the enums declared in your contract, and `tx.nativeEnums` holds the members of the PostgreSQL enum types in your database." - -Two new APIs, never shown, never used again. I do not know what "holds the members" gives me — an array? an object of string constants? Why two, and which one do I want? No example anywhere on the page. - -**3. Line 14.** -> "When a function you call opens its own transaction today, pass `tx` down to that function instead." - -"today" made me stop — today as in "in your current Prisma 7 code"? Plainest: "If a helper you call starts its own transaction, change it to take `tx` as a parameter instead." And then the obvious follow-on is missing: what TypeScript type do I annotate that parameter with? Not stated. - -**4. Line 232.** -> "A type id is the `codecId` your contract gives that column, so read the ids out of your `contract.json`." - -Three stops. "codecId" is a word I have never seen. Part 1 told me the contract is `contract.prisma`; this says `contract.json` — I do not know what that file is, where it comes from, or whether I wrote it. And "read the ids out of" is not an instruction I can follow without seeing one line of that file. - -**5. Line 371.** -> "A signal that is **already aborted** ... rejecting with an error whose `code` is `RUNTIME.ABORTED` and whose `details.phase` is `'stream'`." - -If it "short-circuits before any query runs", why is the phase `'stream'`? That reads like a bug or an internal quirk, and I could not restate why. - -**6. Line 429.** -> "a query run through the client loads every row into memory first and hands its connection back before you read them" - -I can parse it, but it contradicts what I thought `for await` was for. If every row is already in memory, streaming buys me nothing on PostgreSQL. The page never says that plainly, and never warns me about large result sets. - -**7. Line 431.** -> "Each result is read once, one way. Re-`await`ing a result you already awaited is safe and returns the same array." - -First sentence says once; second says twice is fine. I had to read it three times to work out the rule is really "pick `await` or `for await` and never mix". - -## Words and phrases I had to guess - -- **`family-runtime`** (line 113, import path) — guessed it is just a subpath name with no meaning I need. Nothing tells me. -- **`SqlQueryPlan`** (lines 242, 288) — guessed this is the type of what `.build()` returns, i.e. "the built query". The page uses two names for one thing and never links them. -- **`R`** in `(tx) => Promise` (line 24) — guessed "whatever type your callback returns". -- **`kind: 'user'`** (line 99) — appears only in the migration diff, with no counterpart in the Prisma 7 code above it. Guessed it is a required column in some example model. It made me think the new API required a field I did not have. -- **`(f, fns)`** in `where((f, fns) => fns.eq(f.label, params.label))` — guessed fields and functions. Never said on this page. -- **`db.raw.sql`** and **`.affectedCount()`** (line 318) — guessed raw SQL, and that `.affectedCount()` marks it as a write. Both appear for the first time inside a code sample. -- **`all()` / `createAll()`** (line 427) — guessed model methods from the ORM page. They arrive with no introduction. -- **`lane`** (line 404) — guessed it means "which of the several query APIs you used". - -## Where I asked "so what do I actually type?" - -1. **The type id.** `'pg/text@1'` is the only one on the page. I have an `int`, a `timestamptz`, a `boolean`, a `uuid`. I cannot guess their ids and the page points me at a file it never shows. -2. **`db.connect()` vs `db.runtime()`.** Line 137 does `const runtime = await db.connect()`; line 184 does `const runtime = db.runtime()`. Line 281 says calling `connect()` after `prepare()` fails. Nowhere does the page say which one I should write in my own code, or why. -3. **Bounding a transaction with a timeout.** Line 13 tells me to pass a `signal` to every `tx.query` and `tx.execute`. No example on the page does it. I wanted one line showing `AbortSignal.timeout(...)` inside a `db.transaction`. -4. **Isolation level.** Line 12 says the options are gone. It does not say whether I can get `SERIALIZABLE` at all — e.g. by running `SET TRANSACTION ISOLATION LEVEL` as the first statement. I have production code that needs it and the page leaves me stuck. -5. **Passing `tx` to a helper.** Told to do it (line 14), never shown, and no type given. -6. **MongoDB.** Line 220: "share one `MongoClient` between Prisma ORM and your driver code and use a driver session". No code, and no hint how a `MongoClient` gets shared with the Prisma client. -7. **`tx.enums` / `tx.nativeEnums`.** Mentioned, never typed out. -8. **Line 146 example** uses `runtime` that is not declared in that snippet. I would have copied it and got an undefined variable. -9. **Retrying.** "call `db.transaction(...)` again yourself" — on which error codes? A serialization failure has a code; the page does not say which one, so I cannot write the retry it tells me to write. - -## Where it explains the insides when I only wanted to know what to do - -- **Line 167:** "`reason` is optional and the pool never reads its value, so plain `destroy()` is enough." Then the parameter should not be in the table (line 177) at all. Telling me an argument is ignored is internals. -- **Line 373 / 380:** `scope` is documented as an option, with a type, in the options table — and then "the runtime sets it for you, so do not set it yourself." If I must not pass it, it is not an option. -- **Line 404:** `fingerprint` is "a hash of the query text with the literal values replaced". That is how it is built, not what I do with it. Nothing on the page tells me why I would read telemetry at all, or when `durationMs` is missing. -- **Line 429:** the pooling/buffering explanation, above. -- **Line 372:** `details.phase` of `'encode'` / `'decode'` tells me where inside the driver the abort landed. I cannot act differently on any of them. - -## Could I do what the page is for, after one reading? - -Partly. I could write `db.transaction(async (tx) => ...)` and port a `$transaction([...])` array — the diff at line 93 is the clearest thing on the page, and line 105 explains the payoff well. - -I could not: - -- Write a prepared statement, because I cannot find a type id for any column I actually have. -- Decide whether to use `db.connect()` or `db.runtime()`. -- Port any transaction that relied on `isolationLevel` or `timeout`, which is most of mine. -- Write the retry loop the page tells me to write, because it never names the error code to retry on. -- Use `withTransaction` with any confidence, because its stated purpose contradicts both its examples. -- Use `tx.enums`, `tx.nativeEnums`, or `telemetry()` for anything, having been told they exist and nothing else. - -The single biggest hole is the type id. It is the one input I cannot invent, and the page answers it by naming a file it never shows. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part2.round3.md b/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part2.round3.md deleted file mode 100644 index bd6b36deb1e..00000000000 --- a/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part2.round3.md +++ /dev/null @@ -1,98 +0,0 @@ -Read once, top to bottom, as a Prisma 7 user. - -## Sentences I could not restate after one reading - -**1.** "Use `withTransaction(...)` in a helper or a library that is handed a runtime rather than a client." -Stopped by: I do not know why anything would be handed a runtime instead of the client, or how that happens. The page never shows a helper receiving one. Plainer: "If you are writing a function that takes a runtime as a parameter instead of the client, use `withTransaction(...)`." And then show that function signature. - -**2.** "Open a connection and drive the transaction yourself when you need the same connection between statements." -Stopped by: this contradicts the next section. Remark 1 under `db.transaction` says "Every call on `tx` uses the same transaction connection." So `db.transaction` already gives me the same connection between statements. I cannot tell what the manual path actually buys me. The same sentence repeats at line 164, so the contradiction is doubled, not clarified. - -**3.** "Nothing retries a failed transaction for you, so when PostgreSQL reports a serialization failure, call `db.transaction(...)` again yourself." -Stopped by: two ideas fighting. The sentence before says there is "no way to set the isolation level." Serialization failures are a serializable-isolation thing. So either the default isolation can produce them and the page should say so, or this advice is unreachable. I also do not know what error code a serialization failure arrives as — the page tells me to compare `error.code` with a string everywhere else, but not here. - -**4.** "Annotate that parameter with `TransactionContext`, imported from `@prisma/orm-postgres/family-runtime`, which covers `query` and `execute`." -Stopped by: "covers" — covers what? I assume it means the type only has those two methods, but that contradicts the `tx` I get from `db.transaction`, which has `orm` and `sql` too. So if I annotate my helper's parameter with `TransactionContext` and pass it my `tx`, do I lose `tx.orm` inside the helper? The page does not say. No code example. - -**5.** "`for await` does not save memory on PostgreSQL: every row is loaded first." -Stopped by: this tells me the one reason I would reach for `for await` does not apply, then gives a different upside about pooled connections that I cannot map to a decision. After one reading I do not know which one to use. - -**6.** "Same result as `runtime.prepare(...)`, but off the client the SQL builder is handed to you as the first argument." -Stopped by: "off the client" used as a preposition twice on the page ("Prepare a statement off a runtime", line 230) and I had to guess it means "from". Plainer: "`db.prepare(...)` does the same thing, but it passes you a SQL builder as the callback's first argument, so you do not need `db.sql`." - -**7.** "`prepare()` starts the connection itself, so calling `db.connect()` afterwards rejects with an error whose `code` is `DRIVER.ALREADY_CONNECTED`. Reach for the runtime with `db.runtime()` instead, which returns the one `prepare()` already built." -Stopped by: three ideas in two sentences, and "the one `prepare()` already built" has no antecedent I can resolve — the one *what*? The runtime? The connection? I think it means `db.runtime()` returns the already-connected runtime, but I am guessing. - -**8.** "A signal that is **already aborted** when you call `query(...)` rejects before any row is fetched, with an error whose `code` is `RUNTIME.ABORTED` and whose `details.phase` is `'stream'`." -Stopped by: the phase says `'stream'` but nothing streamed. If `'stream'` is the value even when zero rows were fetched, what other values exist? The next line says "`details.phase` says where the abort landed" without listing the possible values. Useless to me as written — I cannot branch on it. - -**9.** "`lane` names which API built the query, such as `orm-client` for `db.orm` or `raw` for `db.raw.sql`." -Stopped by: "lane" is a word I have never seen used this way. The examples rescue it, but I had to read it twice. Plainer: call the field what it is, or say "`lane` is the name of the API you used." - -**10.** "`durationMs` is how long the query took, and the runtime records it for every query it times." -Stopped by: circular. It records the duration for every query it times — which queries does it not time? The type is written `durationMs?`, so it is sometimes absent, and this sentence is exactly the sentence that should tell me when, and does not. - -**11.** "Multi-document transactions for MongoDB are planned, but nothing on this page works on MongoDB today." -Stopped by: flatly contradicted by the last line of the page: "the full rules on reading a result, shared identically by PostgreSQL and MongoDB." The `AsyncIterableResult` section is on this page and does work on MongoDB. It also repeats the callout directly above it almost word for word. - -## Words and phrases I had to guess - -- **`family-runtime`** in `@prisma/orm-postgres/family-runtime`. I have no idea what "family" means. I guessed it is just an import path I copy and never think about. If that is true, fine, but it reads like it means something. -- **`@1` in `pg/text@1`.** I guessed a version number for the encoding. Never explained. -- **`pg/timestamptz-temporal@1`.** I guessed "temporal" means it maps to the TC39 Temporal API rather than `Date`. Pure guess, and if I guessed wrong my column types are wrong. -- **`codecId`** ("It is called `codecId` in code"). Guessed: the internal name of the same string. I do not know why I was told. -- **`SqlQueryPlan`.** Introduced only as "what the tables below call `SqlQueryPlan`". I guessed it is the return type of `.build()`, the thing part 1 called "the built query". Two names for one thing. -- **"plan"** in the anchor `#run-a-sql-builder-plan-with-txsql-and-txexecute` while the visible heading says "query". I guessed they are the same and the anchor is stale. -- **"bound the queries"** (line 13). Guessed: put a time limit on them. -- **"The `target` is explicit"** (line 326). Explicit as opposed to what? I guessed it means "you must pass it, it is not inferred." -- **`toArray()`** in the warning example (line 90). Nowhere defined. The `AsyncIterableResult` section says `await` the result or `for await` it — `toArray()` is a third thing that appears once, in the one example I most need to trust. -- **"Evict"** in `connection.destroy()`. Guessed: remove from the pool permanently. The Remarks say "throws that connection away", the table says "Evict" — pick one word. - -## Places I asked "so what do I actually type?" - -1. **`TransactionContext`.** "Annotate that parameter" — show me the function. `async function addTag(tx: TransactionContext) { ... }`. This is the recommended fix for nested transactions and it has zero code. -2. **Getting a type id from a column.** "To take one straight from a column, read `db.sql.public.user.columns.id`." Read it and do what with it? Does it *return* `'pg/uuid@1'`? Can I write `prepare({ id: db.sql.public.user.columns.id }, ...)`? One line of code would settle it. -3. **The full list of type ids.** I get four examples plus "the PostgreSQL type plus `@1`". Is it literally `pg/` + the PostgreSQL type name + `@1`? Then what is `timestamptz-temporal`? That is not a PostgreSQL type name. I cannot construct one for `jsonb` or `numeric` with confidence. -4. **MongoDB transactions.** "share one `MongoClient` between Prisma ORM and your driver code and use a driver session." No code at all. This is the only thing a MongoDB reader came to this section for, and it is one sentence and a link. -5. **Cleaning up a manual connection when something throws.** Both manual examples are happy-path straight lines. If `transaction.execute(...)` throws, `connection.release()` never runs and I have leaked a pooled connection. The page tells me "Always `release()` the connection when done" but shows no `try`/`finally`. Show the `try`/`finally`. -6. **Does `release()` after `rollback()` differ from after `commit()`?** And what happens if I release without committing — implicit rollback, or an error? Not stated. -7. **Preparing a write from the SQL builder.** Line 316 says a `PreparedExecution` comes from "raw SQL with `db.raw.sql` and ending in `.affectedCount()`". Can I prepare `db.sql.public.tag.insert([...])`? The section on `db.transaction` uses exactly that shape with `tx.execute`. Unanswered. -8. **What `prepare` does with parameters in the raw example.** `db.raw.sql\`... label = ${'archived'} ...\`` with `prepare({}, ...)` — the values are baked into the statement and the declaration is empty. So how do I write a prepared *execution* that takes parameters? The one example shows the case where I would not need prepare at all. -9. **Disposing a prepared statement.** Is there a `close()`? Do they leak? Do I keep one per process? Nothing. -10. **Does `signal` abort the transaction or only the statement?** Line 13 tells me to pass a signal to each call inside `db.transaction`. If one statement aborts mid-transaction, does the transaction roll back? I assume yes because the callback throws — but "assume" is the word. -11. **Transaction timeouts.** "There are no `isolationLevel`, `timeout`, or `maxWait` options." Coming from Prisma 7, where `timeout` defaults to 5 seconds, my first question is: does a transaction now run forever? Not answered. - -## Places the page explains internals when I only wanted to know what to do - -- **"It is called `codecId` in code."** I do not read the source. Delete it. -- **"`db.transaction(...)` is built on it."** (line 112) True but irrelevant to choosing between them. -- **The whole `scope` remark** (line 375). Three lines to introduce an option, explain that middleware consumes it, and then tell me "Do not pass it yourself." If I must not pass it, it does not belong in a reference of options I pass. One sentence under the table would do: "`scope` is set for you; do not pass it." -- **"`for await` does not save memory on PostgreSQL: every row is loaded first. The upside is that a result you hold or read slowly does not keep a pooled connection checked out, because the client hands the connection back before you read the rows."** This is connection-pool mechanics. What I need is the rule: use `await`; use `for await` when X. -- **"An `AsyncIterableResult` is tied to the transaction connection."** (line 82) The rule that matters is "collect rows inside the callback." The reason is fine as a half-clause; it currently opens the callout. - -## Broken or mislabeled references - -- **"(see the Remark below)"** at line 18. The thing below is a `:::warning` callout, not a Remark, and it sits after the Examples. I scrolled looking for a bullet. -- **"See [`postgres(options)`](#postgresoptions)"** in the return-type table for `tx.enums`. That anchor is on the half of the page I do not have, and the link text tells me nothing about why I would follow it. -- **The `tx.enums` / `tx.nativeEnums` row is in a table headed "Return type".** They are not return types of `db.transaction`. It confused me into thinking `db.transaction` returns them. -- **The manual-control table is headed "Options"** but its columns are `Method | Type | Description` and every Type cell says "none". It is a method list wearing an options heading. -- **`#run-a-sql-builder-plan-with-txsql-and-txexecute`** — anchor says "plan", heading says "query". - -## Could I do what this page is for, after one reading? - -Partly. I could write `db.transaction(async (tx) => ...)` with `tx.orm` and `tx.execute`, and I could port my `$transaction([...])` array. The migration diff at line 96 is the single most useful thing on the page — it is the only place that connects what I already know to what I now type. - -Beyond that, no. - -What I still would not know: - -- **When to use the manual connection path.** The stated reason ("the same connection between statements") is the same thing `db.transaction` already gives me. I would use `db.transaction` for everything and never find out what I was missing. -- **How to write a helper that takes a transaction.** The page tells me to do this to avoid nesting and then shows no code and leaves me unsure whether the helper can still use `tx.orm`. -- **How to build a type id for any column that is not `text`, `int4`, `uuid`, or `bool`.** So I could not prepare a statement against my own schema. -- **How to prepare anything parameterized that writes.** The only `PreparedExecution` example hard-codes both its values. -- **Whether my transactions can hang.** No timeout, no default stated. -- **What to catch and retry.** I am told to retry serialization failures and given no code to compare against, on a page where every other error hands me an exact string. -- **How to clean up safely.** Every manual example leaks the connection if a statement throws. -- **Anything about MongoDB transactions** beyond "not yet", stated three times. - -One correctness problem worth checking with the author: `escaped.rows.toArray()` at line 90. `toArray()` appears exactly once on this page and is absent from the `AsyncIterableResult` section that claims to give the reading rules. Either that section is missing a method or the example is wrong. \ No newline at end of file diff --git a/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part2.round4.md b/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part2.round4.md deleted file mode 100644 index b707c39a2e2..00000000000 --- a/docs/orm-docs-audit/c21-orm-reference/transactions-and-runtime.part2.round4.md +++ /dev/null @@ -1,81 +0,0 @@ -I read `/Users/wmadden/Projects/prisma/web/.claude/worktrees/error-reference-follow-ups-81a8e7/wip/c21/ref/r4/transactions-and-runtime.part2.mdx` top to bottom, once. - -# Sentences I could not restate after one reading - -**Line 3.** "Open a connection and drive the transaction yourself when you need statements on one connection outside a transaction, such as a `SET` before you begin." -Stopped by: "drive the transaction yourself" names no method, and "statements on one connection outside a transaction" is three ideas stacked. I did not know what "open a connection" meant in this API until line 156 told me it is `runtime.connection()`. Plainer: "If you need to run a statement like `SET` on the same connection before the transaction starts, get a connection with `runtime.connection()` and call `commit()` or `rollback()` yourself." - -**Line 14.** "To pass the full `tx`, take its type from the client: `type Tx = Parameters[0]>[0]`." -Stopped by: nested `Parameters<...>` with two index accesses. I can decode it given a minute, but not in one reading, and the page does not say why no exported type exists. Plainer: add "There is no exported type for the full `tx`, so derive it:" before the code. - -**Line 19.** "Do not let an `AsyncIterableResult` escape the callback unread." -Stopped by: `AsyncIterableResult` is not defined until line 423, the last section of the page. On first reading it is a word I have never seen. "escape ... unread" is a metaphor. Plainer: "`tx.query(...)` returns a result object that only works while the transaction is open. Read it inside the callback." - -**Line 116.** "That path holds the SQL runtime API, the same one for every SQL database." -Stopped by: I do not know what "the SQL runtime API" is as a thing, and "the same one for every SQL database" tells me about Prisma's internal layering, not about what I do. Also the import path contains `family-runtime` and the page never says what a "family" is. - -**Line 310.** "A statement written as raw SQL with `db.raw.sql` and ending in `.affectedCount()`, which asks for a row count instead of rows, prepares into a `PreparedExecution` instead." -Stopped by: four ideas in one sentence (raw SQL, `.affectedCount()`, what it means, the resulting type). Plainer: "Some queries return a row count instead of rows. End the query with `.affectedCount()`. Preparing one gives you a `PreparedExecution`, which you run with `execute(target, params)`. It resolves to `{ affectedRows }`." - -**Line 368.** "A signal that is already aborted when you call `query(...)` rejects before any row is fetched, with an error whose `code` is `RUNTIME.ABORTED` and whose `details.phase` is `'stream'`." -Stopped by: the phase is `'stream'` for a query that never streamed anything. That reads like a contradiction, and nothing explains it. - -**Line 400.** "`durationMs` is how long the query took, and it is present for every query the runtime ran." -Stopped by: the type is written `durationMs?` twice (line 400 and the example comment on line 418). Optional in the type, always present in the prose. I could not tell which to trust or whether I need a null check. - -**Line 400.** "`lane` is the name of the API you used, such as `orm-client` for `db.orm` or `raw` for `db.raw.sql`." -Stopped by: "lane". I only understood it because of the examples. The word carries no meaning on its own. - -**Line 425.** "`for await` does not reduce memory on PostgreSQL: every row is loaded first." -Stopped by: this removes the only reason I would have used `for await`, and the page does not say whether it is different on MongoDB or why I would ever pick it. Line 423 offers it as an equal option and line 425 says it is pointless. I could not tell what the page wants me to do. - -# Words and phrases I had to guess - -- **"family-runtime"** (lines 14, 116, 138, 342) — I guessed a "family" is a group of databases that share a runtime, so PostgreSQL/MySQL/SQLite. The page never says. -- **"SqlQueryPlan"** (lines 229, 238, 284) — I guessed this is just the type of what `.build()` returns. Line 229 says so, but the name "plan" made me expect something I had to inspect or execute differently. -- **"codecId"** (line 228) — I guessed "codec" means the encoder/decoder for a column's type, and `codecId` gives me the same string as `'pg/text@1'`. -- **"type id"** / **"`pg/text@1`"** (line 228) — I guessed these are Prisma's own names for PostgreSQL types. I could not guess which id to use for a `String` column vs a `DateTime` beyond the two examples given. -- **"`pg/timestamptz-temporal@1`"** (line 228) — I guessed "temporal" refers to the JS Temporal API. Not stated. -- **"fingerprint"** (line 400) — I guessed a hash of the query text. The page says what it is for, which was enough. -- **"scope"** (line 371) — I guessed it is an internal tracing field. Guessing was forced: the page names it, tells me not to pass it, and never says what it is. -- **"lane"** (line 400) — guessed above. -- **"target"** (line 320) — I guessed "the thing that owns the connection the statement runs on." The word alone told me nothing. -- **"the offending names"** (line 230) — I guessed this means the parameter names I declared but never used. - -# "So what do I actually type?" — unanswered - -1. **Line 12: the transaction timeout.** "To set one, set PostgreSQL's `idle_in_transaction_session_timeout` on the connection." This is the one concrete way to bound a transaction and there is no code. I do not know whether that is a connection-string parameter, a `SET` I run via `connection.execute`, or server config. This is the first thing a Prisma 7 user will look for, because `$transaction` had `timeout` and `maxWait`. -2. **Lines 3 and 152: the `SET` before the transaction.** Both sentences say the entire reason to use manual control is running a `SET` first — and neither of the two manual examples (lines 175 and 191) contains a `SET`. `connection.execute(...)` appears to take a built query, and I cannot build `SET` with `db.sql.public.
`, so I would have to guess `db.raw.sql`. The page names the use case three times and never shows it. -3. **Line 117: `withTransaction` without a client.** The section exists for "a function that takes a runtime as a parameter instead of the client", then tells me to build queries "with a SQL builder you already hold, such as the client's `db.sql`" — and the example on line 142 reaches out to a module-scope `db`. If my function only has a runtime, I still do not know where a SQL builder comes from. The example does not demonstrate the situation the section is written for. -4. **Line 215: MongoDB transactions.** "share one `MongoClient` between Prisma ORM and your driver code" — no code, no indication of how a `MongoClient` is handed to the Prisma client. The link may cover it, but the reference gives me nothing. -5. **What `tx.execute(...)` returns.** Line 11 says use it "when the write returns no rows". Every example throws the result away. `{ affectedRows }` is only ever mentioned for `PreparedExecution` (line 310). I do not know if `tx.execute` gives me a row count. -6. **Line 228: which type id for my column.** Four ids are listed. My contract has `String`, `Int`, `DateTime`, `Boolean`, `Json`, enums. I would have to follow a link for the mapping, or use `codecId`, and the page does not say "use `codecId` and skip the table", which is what I actually want. -7. **Line 369: the phase list.** Six values, no meaning for any of them. If I catch `RUNTIME.ABORTED`, I cannot tell what I should do differently for `'encode'` vs `'afterQuery'`. -8. **Retrying.** Line 12 says "Nothing retries a failed transaction for you." A serialization or deadlock failure is the normal reason to retry, and the page never says which `code` to catch or that I should write the loop myself. - -# Places that explain the internals when I only wanted to know what to do - -- **Line 116:** "That path holds the SQL runtime API, the same one for every SQL database." Package layout, not my problem. -- **Line 371:** "`scope` is set for you. Do not pass it." Telling me a field exists purely so I can avoid it. If I must not pass it, do not name it. -- **Line 228:** "A type id is the PostgreSQL type plus a version, always `@1` today." The versioning scheme is Prisma's business. I need the id string. -- **Line 157:** "It does not close the pool or the runtime, and the runtime opens a replacement connection on the next query." Reassuring, but it is pool mechanics; the actionable half is the last sentence, "Use `destroy()` only for a connection you no longer trust." -- **Line 400:** "Every run of the same query text shares one `fingerprint`, whatever values you pass" — this one earns its place, but it sits in a paragraph that also defines `lane`, `outcome`, `durationMs`, and the "most recent query only" rule. Five facts in one bullet. - -# Could I do what the page is for after one reading? - -Partly. - -I could write a normal transaction. Lines 3–108 are clear, the `tx` vs `db` warning on line 11 is exactly what a Prisma 7 user needs, and the diff on line 96 answered my first question — where did `$transaction([...])` go — better than anything else on the page. - -What I would still not know: - -- How to put a time limit on a transaction. The page names `idle_in_transaction_session_timeout` and stops. -- How to run the `SET` that is the page's own stated reason for manual connection control. -- How to use `withTransaction` in the situation it was written for, since the example uses an outer-scope `db`. -- What to do when a transaction fails on a conflict — which code, and that I write the retry loop myself. -- Which type id my columns need, beyond `codecId`. -- Whether `durationMs` can be missing. -- Whether I should ever use `for await`. -- What `tx.execute(...)` returns. - -The prepared statements section is the strongest part: line 252 and line 297 gave me two working shapes and the difference between them is stated plainly on line 276. \ No newline at end of file From a272f0652d8115457f21837deb81da274674c889 Mon Sep 17 00:00:00 2001 From: willbot Date: Sun, 13 Sep 2026 14:16:42 +0200 Subject: [PATCH 12/14] docs(orm8): orm/reference review fixes Raw MongoDB results keep each value as stored, so only an id MongoDB generated is an ObjectId. The nulls-last example sorts a nullable column. for await saves nothing on PostgreSQL, where every row is loaded first. idle_in_transaction_session_timeout ends an idle transaction and transaction_timeout caps its total length. The manual-control examples roll back on every failure path before releasing the connection. The landing page's raw-queries sentence is a full sentence. Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- .../docs/content/docs/orm/reference/index.mdx | 2 +- .../docs/orm/reference/pipeline-builder.mdx | 4 +-- .../docs/orm/reference/raw-queries.mdx | 2 +- .../docs/orm/reference/sql-query-builder.mdx | 8 +++--- .../reference/transactions-and-runtime.mdx | 28 ++++++++++++------- 5 files changed, 26 insertions(+), 18 deletions(-) diff --git a/apps/docs/content/docs/orm/reference/index.mdx b/apps/docs/content/docs/orm/reference/index.mdx index 51cf5ee9269..f86423fe9d4 100644 --- a/apps/docs/content/docs/orm/reference/index.mdx +++ b/apps/docs/content/docs/orm/reference/index.mdx @@ -12,7 +12,7 @@ Prisma ORM gives you three typed ways to write a query. In Prisma ORM 7 you wrot - The **SQL query builder** is at `db.sql`. It has table methods that match SQL, such as `select()`, `innerJoin()`, and `groupBy()`, it is PostgreSQL only, and it returns whatever shape your `select()` asks for, not objects shaped like your models. - The **pipeline builder** is at `db.query`. It builds MongoDB aggregation pipelines, and it is MongoDB only. -Plus raw SQL and raw commands when none of them fits. See [Raw queries](/orm/reference/raw-queries). For connecting and disconnecting the client, transactions, and prepared statements, see [Transactions and runtime](/orm/reference/transactions-and-runtime). +When none of the three can express the query you need, write raw SQL on PostgreSQL or a raw command on MongoDB. See [Raw queries](/orm/reference/raw-queries). For connecting and disconnecting the client, transactions, and prepared statements, see [Transactions and runtime](/orm/reference/transactions-and-runtime). How a query runs: the ORM client runs when you `await` it. The SQL query builder and the pipeline builder each give you a built query from `.build()`, which you run with `runtime.query(...)`, or with `runtime.execute(...)` for a write that returns no rows. Get `runtime` from `db.runtime()` once and reuse it. diff --git a/apps/docs/content/docs/orm/reference/pipeline-builder.mdx b/apps/docs/content/docs/orm/reference/pipeline-builder.mdx index 33bf70c3cf0..e2c158de6c0 100644 --- a/apps/docs/content/docs/orm/reference/pipeline-builder.mdx +++ b/apps/docs/content/docs/orm/reference/pipeline-builder.mdx @@ -133,7 +133,7 @@ const posts = await runtime.query(plan); - On MongoDB, `db.runtime()` returns a promise, so you must await it: `const runtime = await db.runtime()`. Hold it in a variable and reuse it. - `await` that call for an array of documents, or write `for await (const doc of runtime.query(plan)) { ... }` to take one document at a time. - There is no `db.execute` on the MongoDB client. -- **Read results are decoded.** If every stage in your pipeline is one of `match()`, `sort()`, `limit()`, `skip()`, `sample()`, `project()`, `addFields()`, and `vectorSearch()`, `_id` comes back as a hex string. If any stage is not in that list, including `lookup()` and `group()`, every document comes back raw and `_id` is an `ObjectId` object from the `mongodb` package, the MongoDB driver Prisma ORM uses. Compare those ids as strings, as [`lookup()`](#lookup) shows. A `DateTime` field comes back as a JavaScript `Date` either way. +- **Read results are decoded.** If every stage in your pipeline is one of `match()`, `sort()`, `limit()`, `skip()`, `sample()`, `project()`, `addFields()`, and `vectorSearch()`, `_id` comes back as a hex string. If any stage is not in that list, including `lookup()` and `group()`, every document comes back raw, with each value as MongoDB stores it. An `_id` that MongoDB generated is then an `ObjectId` object from the `mongodb` package, the MongoDB driver Prisma ORM uses. Compare those ids as strings, as [`lookup()`](#lookup) shows. A `DateTime` field comes back as a JavaScript `Date` either way. #### Examples @@ -980,7 +980,7 @@ Append an arbitrary raw pipeline stage the typed methods don't cover. - `pipe(stage)` appends a raw stage that you construct yourself, and keeps the row type you already had. The stages are the `Mongo*Stage` classes, one per MongoDB stage, from `@prisma/orm-mongo/query-ast/execution`. The class name is `Mongo`, then the stage name without its `$`, then `Stage`, so `$unwind` is `MongoUnwindStage`. The example below uses `MongoMatchStage` and `MongoCountStage`. The same module exports `MongoFieldFilter`, which builds the filter a `MongoMatchStage` takes. - `pipe(stage)` declares the row type the rows have after that stage. `NewShape` is a type you write yourself, as at the end of the example below. -- After a `pipe()` stage the TypeScript type no longer describes the rows. They come back exactly as MongoDB returns them, so a date is a `Date` and an id is an `ObjectId` from the `mongodb` package, the driver Prisma ORM runs your queries through. +- After a `pipe()` stage the TypeScript type no longer describes the rows. They come back exactly as MongoDB returns them, so a date is a `Date` and an `_id` that MongoDB generated is an `ObjectId` from the `mongodb` package, the driver Prisma ORM runs your queries through. - Prefer a typed stage where one exists. `pipe()` adds one raw stage to a typed chain, and [`rawCommand()`](#rawcommand) replaces the whole command. #### Examples diff --git a/apps/docs/content/docs/orm/reference/raw-queries.mdx b/apps/docs/content/docs/orm/reference/raw-queries.mdx index 1e94aaba8bc..b981c5b8641 100644 --- a/apps/docs/content/docs/orm/reference/raw-queries.mdx +++ b/apps/docs/content/docs/orm/reference/raw-queries.mdx @@ -207,7 +207,7 @@ To bind any value whose type is not in the list, wrap it in [`param(value, { cod Each of these nine methods builds a command. Call `.build()` on it, then run every one of them with `runtime.query(...)`, including the writes. The raw collection methods all run through `query()`. A write returns a one-element array, so destructure it with `const [result] = ...`. :::warning[MongoDB raw results are native BSON values] -Nothing on the MongoDB side of this page is converted for you. An `_id` comes back as an `ObjectId` instance from the `mongodb` package, the MongoDB driver underneath, not a hex string. Compare one with `String(row._id) === aliceId`. Raw filters take native BSON values too, so build ids with that package's `ObjectId` class. The write methods give you the counts and ids under the `mongodb` package's own key names, `{ insertedId }`, `{ matchedCount, modifiedCount, upsertedCount, upsertedId }`, and `{ deletedCount }`, and you read those keys directly. +Nothing on the MongoDB side of this page is converted for you. An `_id` comes back as it is stored: one that MongoDB generated is an `ObjectId` instance from the `mongodb` package, the MongoDB driver underneath, not a hex string. Compare one with `String(row._id) === aliceId`. Raw filters take native BSON values too, so build ids with that package's `ObjectId` class. The write methods give you the counts and ids under the `mongodb` package's own key names, `{ insertedId }`, `{ matchedCount, modifiedCount, upsertedCount, upsertedId }`, and `{ deletedCount }`, and you read those keys directly. ::: The examples in this section run against the `users` / `posts` schema from the pipeline builder page. See its [example schema](/orm/reference/pipeline-builder#example-schema) for the full model definitions. Create the client with `mongo(...)`, get the connection that runs a built query from `await db.runtime()`, and declare the two example ids once: 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 2ce9718a1b3..bd68495040f 100644 --- a/apps/docs/content/docs/orm/reference/sql-query-builder.mdx +++ b/apps/docs/content/docs/orm/reference/sql-query-builder.mdx @@ -385,9 +385,9 @@ Sort the result set by a column or a computed expression, in a direction you cho TypeScript also accepts a `nulls` option. It does nothing, and no `NULLS FIRST` or `NULLS LAST` reaches the SQL. To put nulls last, sort by a computed value first, then by the column: ```ts -db.sql.public.post - .orderBy((f, fns) => fns.raw`CASE WHEN ${f.title} IS NULL THEN 1 ELSE 0 END`.returns('pg/int4@1')) - .orderBy('title', { direction: 'asc' }); +db.sql.public.user + .orderBy((f, fns) => fns.raw`CASE WHEN ${f.zip} IS NULL THEN 1 ELSE 0 END`.returns('pg/int4@1')) + .orderBy('zip', { direction: 'asc' }); ``` #### Return type @@ -1003,6 +1003,6 @@ const rows = await runtime.query(plan); // Row[] ### Streaming vs. collecting -`await runtime.query(plan)` collects every row into an array. This is the common case and what every example on this page uses. You can also loop the same result with `for await` to handle one row at a time, which lets you start work before the array is assembled. On PostgreSQL, use `await`. +`await runtime.query(plan)` collects every row into an array. This is the common case and what every example on this page uses. You can also loop the same result with `for await` to handle one row at a time. On PostgreSQL every row is loaded before the loop starts, so `for await` saves nothing there. Use `await`. Keep the result in a variable, as in `const result = runtime.query(plan)`, and you can `await result` as many times as you like. You cannot mix `await` and `for await` on the same result, and you cannot `for await` it twice. Either one throws an error whose `code` is `RUNTIME.ITERATOR_CONSUMED`. See [`AsyncIterableResult`](/orm/reference/orm-client#asynciterableresult). diff --git a/apps/docs/content/docs/orm/reference/transactions-and-runtime.mdx b/apps/docs/content/docs/orm/reference/transactions-and-runtime.mdx index a86f8aa01c1..147c750b0fd 100644 --- a/apps/docs/content/docs/orm/reference/transactions-and-runtime.mdx +++ b/apps/docs/content/docs/orm/reference/transactions-and-runtime.mdx @@ -352,7 +352,7 @@ Run a callback inside a transaction. The transaction commits when the callback r #### Remarks - Query through `tx`, not `db`. `tx.orm` holds your models. Build queries with `tx.sql`, then run them with `tx.query(...)` to get rows back, or `tx.execute(...)` when the write returns no rows. `tx.execute(...)` resolves to `{ affectedRows }`, the number of rows the statement changed. Every call on `tx` uses the same transaction connection, and queries on `db` run outside the transaction. -- `db.transaction(...)` takes the callback and nothing else. There are no `isolationLevel`, `timeout`, or `maxWait` options, and Prisma ORM offers no way to set the isolation level. Nothing retries a failed transaction for you. Write the retry loop yourself. A transaction has no time limit by default. To set one, set PostgreSQL's `idle_in_transaction_session_timeout` in your connection string or server configuration. See the [fundamentals transactions guide](/orm/fundamentals/transactions#options-and-isolation-level). +- `db.transaction(...)` takes the callback and nothing else. There are no `isolationLevel`, `timeout`, or `maxWait` options, and Prisma ORM offers no way to set the isolation level. Nothing retries a failed transaction for you. Write the retry loop yourself. A transaction has no time limit. PostgreSQL's `idle_in_transaction_session_timeout` ends one that sits idle between statements, and on PostgreSQL 17 and later `transaction_timeout` caps its total length. Set either in your connection string or server configuration. See the [fundamentals transactions guide](/orm/fundamentals/transactions#options-and-isolation-level). - To put a time limit on the queries inside a transaction, pass a `signal` to each `tx.query(...)` and `tx.execute(...)` call, as the example below does. See [`RuntimeExecuteOptions`](#runtimeexecuteoptions). - `tx` has no `transaction()` method, so transactions do not nest. If a helper you call starts its own transaction, change it to take `tx` as a parameter instead. `TransactionContext`, imported from `@prisma/orm-postgres/family-runtime`, has `query` and `execute` only, so a helper typed with it cannot use `tx.orm` or `tx.sql`: `async function addTag(tx: TransactionContext) { ... }`. There is no exported type for the full `tx`, so derive it: `type Tx = Parameters[0]>[0]`. - `tx.sql` is a full SQL builder, keyed the same way as `db.sql`: use `tx.sql.public.
`, where `public` is the PostgreSQL schema. @@ -496,7 +496,7 @@ The lowest level: acquire a connection, open a transaction on it, commit or roll #### Remarks -- `runtime.connection()` returns a dedicated connection. `connection.transaction()` opens a transaction on it. Run built queries with `transaction.query(...)` for rows or `transaction.execute(...)` for writes that return no rows, then call `transaction.commit()` or `transaction.rollback()`. Always `release()` the connection when done, to return it to the pool. Commit or roll back first: release it in a `finally` block, as the examples do, so a statement that throws does not leak the connection. +- `runtime.connection()` returns a dedicated connection. `connection.transaction()` opens a transaction on it. Run built queries with `transaction.query(...)` for rows or `transaction.execute(...)` for writes that return no rows, then call `transaction.commit()` or `transaction.rollback()`. Always `release()` the connection when done, to return it to the pool. Commit or roll back on every path first, then release: the examples nest a `try` for the transaction inside the `try` whose `finally` releases the connection, so a statement that throws neither leaves the transaction open nor leaks the connection. - `connection.destroy()` throws that connection away instead of returning it to the pool. Use `destroy()` only for a connection you no longer trust. #### Options @@ -519,10 +519,15 @@ const connection = await runtime.connection(); try { await connection.query(db.sql.public.tag.select('id').limit(1).build()); // a read on this connection const transaction = await connection.transaction(); - await transaction.execute( - db.sql.public.tag.insert([{ id: crypto.randomUUID(), label: 'manual-commit' }]).build(), - ); - await transaction.commit(); + try { + await transaction.execute( + db.sql.public.tag.insert([{ id: crypto.randomUUID(), label: 'manual-commit' }]).build(), + ); + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } } finally { await connection.release(); } @@ -534,10 +539,13 @@ try { const connection = await runtime.connection(); try { const transaction = await connection.transaction(); - await transaction.execute( - db.sql.public.tag.insert([{ id: crypto.randomUUID(), label: 'manual-rollback' }]).build(), - ); - await transaction.rollback(); + try { + await transaction.execute( + db.sql.public.tag.insert([{ id: crypto.randomUUID(), label: 'manual-rollback' }]).build(), + ); + } finally { + await transaction.rollback(); + } } finally { await connection.release(); } From b26befed504f23b7534af4d9e312a5bc53992757 Mon Sep 17 00:00:00 2001 From: willbot Date: Sun, 13 Sep 2026 14:20:06 +0200 Subject: [PATCH 13/14] docs(orm8): rewrite the orm/reference landing page The page said the ORM client runs a query when you await it. That is true only of all(), createAll(), updateAll(), and deleteAll(), which return a result object; first(), create(), update(), upsert(), delete(), and aggregate() send the query when called. PostgreSQL and MongoDB now each have their own example and explanation instead of alternating sentence by sentence, and the section describing how the reference pages are organised is gone. Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- .../docs/content/docs/orm/reference/index.mdx | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/apps/docs/content/docs/orm/reference/index.mdx b/apps/docs/content/docs/orm/reference/index.mdx index f86423fe9d4..2114cbe9a2a 100644 --- a/apps/docs/content/docs/orm/reference/index.mdx +++ b/apps/docs/content/docs/orm/reference/index.mdx @@ -14,7 +14,13 @@ Prisma ORM gives you three typed ways to write a query. In Prisma ORM 7 you wrot When none of the three can express the query you need, write raw SQL on PostgreSQL or a raw command on MongoDB. See [Raw queries](/orm/reference/raw-queries). For connecting and disconnecting the client, transactions, and prepared statements, see [Transactions and runtime](/orm/reference/transactions-and-runtime). -How a query runs: the ORM client runs when you `await` it. The SQL query builder and the pipeline builder each give you a built query from `.build()`, which you run with `runtime.query(...)`, or with `runtime.execute(...)` for a write that returns no rows. Get `runtime` from `db.runtime()` once and reuse it. +## Running a query + +The ORM client sends its own queries. `first()`, `create()`, `update()`, `upsert()`, `delete()`, and `aggregate()` send the query the moment you call them. `all()`, `createAll()`, `updateAll()`, and `deleteAll()` return a result object and send the query when you `await` that object or loop over it with `for await`. `all()` is what replaced `findMany`. + +The SQL query builder and the pipeline builder send nothing on their own. `.build()` gives you a built query. Pass it to `runtime.query(...)` for rows, or to `runtime.execute(...)` for a write that returns no rows. Get `runtime` from `db.runtime()` once and reuse it. + +### PostgreSQL ```ts const runtime = db.runtime(); @@ -23,15 +29,18 @@ const plan = db.sql.public.user.select('id', 'email').where((f, fns) => fns.eq(f const rows = await runtime.query(plan); ``` -`const runtime = db.runtime();` is the PostgreSQL form. On MongoDB the call returns a promise, so there you write `const runtime = await db.runtime();`. The two callbacks differ: the ORM client compares on the column (`u.kind.eq(...)`), and the SQL query builder compares through `fns` (`fns.eq(f.kind, ...)`). `.all()` runs the query and returns every row. It is what replaced `findMany`. +`User` is the model name and `user` is the table name. The ORM client compares on the column, `u.kind.eq(...)`. The SQL query builder compares through `fns`, `fns.eq(f.kind, ...)`. -Watch the casing: `User` is the model name and `user` is the table name. +### MongoDB -Two different things are both named `query`. `db.query` builds a MongoDB pipeline. `runtime.query(...)` runs a query that has already been built. - -## How database differences are documented +```ts +const runtime = await db.runtime(); +const admins = await db.orm.users.where({ role: 'admin' }).all(); +const plan = db.query.from('users').match((f) => f.role.eq('admin')).build(); +const docs = await runtime.query(plan); +``` -Every method on a reference page has the same four parts: Remarks, Options, Return type, and Examples. If a method works differently on PostgreSQL and MongoDB, or exists on only one of them, its Remarks say so. For a walkthrough of reading and writing data rather than a method list, see [Reading data](/orm/fundamentals/reading-data), [Writing data](/orm/fundamentals/writing-data), [Relations and joins](/orm/fundamentals/relations-and-joins), [Transactions](/orm/fundamentals/transactions), and [Advanced queries](/orm/fundamentals/advanced-queries). +`db.runtime()` returns a promise on MongoDB, so `await` it. `users` is the collection name, and both `db.orm` and `db.query.from()` are keyed by it. The ORM client's `where()` takes an object of field values. The pipeline builder's `match()` takes a callback that compares on the field, `f.role.eq(...)`. Two different things are named `query`: `db.query` builds a pipeline, and `runtime.query(...)` runs a query that has already been built. }>Every ORM client method, with PostgreSQL and MongoDB behavior documented side by side. From fbbdafcba3c3263935785170a53fc14746fb89ef Mon Sep 17 00:00:00 2001 From: willbot Date: Sun, 13 Sep 2026 14:21:01 +0200 Subject: [PATCH 14/14] docs(orm8): remove commentary about the reference pages from the reference pages The sentences saying what each page documents, that every method has a Remarks list, and that examples carry over between pages are gone. The raw-queries PostgreSQL section no longer ends with a MongoDB sentence. Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- apps/docs/content/docs/orm/reference/pipeline-builder.mdx | 2 +- apps/docs/content/docs/orm/reference/raw-queries.mdx | 2 +- apps/docs/content/docs/orm/reference/sql-query-builder.mdx | 4 ++-- .../content/docs/orm/reference/transactions-and-runtime.mdx | 4 +--- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/apps/docs/content/docs/orm/reference/pipeline-builder.mdx b/apps/docs/content/docs/orm/reference/pipeline-builder.mdx index e2c158de6c0..af9175d0d3d 100644 --- a/apps/docs/content/docs/orm/reference/pipeline-builder.mdx +++ b/apps/docs/content/docs/orm/reference/pipeline-builder.mdx @@ -16,7 +16,7 @@ The pipeline builder targets MongoDB only. PostgreSQL has no pipeline builder: u ## Example schema -All examples on this page run against the schema below. It is the same MongoDB schema as the [ORM client](/orm/reference/orm-client) page, so examples carry over between the two pages. `Article` and `Tutorial` are two kinds of `Post`, stored in one `posts` collection and told apart by the value of the `kind` field. `@@base(Post, "article")` and `@@discriminator(kind)` set that up. See [`@@base`](/orm/reference/orm-client#variant) on the ORM client page. `@@type("mongo/string@1")` says the enum is stored as a MongoDB string, and `@1` is the version of that type id, always `@1` today. On this page you never need any of that, because `kind` is an ordinary field you can filter and group on. +The examples run against the schema below, the same MongoDB schema as the [ORM client](/orm/reference/orm-client) page. `Article` and `Tutorial` are two kinds of `Post`, stored in one `posts` collection and told apart by the value of the `kind` field. `@@base(Post, "article")` and `@@discriminator(kind)` set that up. See [`@@base`](/orm/reference/orm-client#variant) on the ORM client page. `@@type("mongo/string@1")` says the enum is stored as a MongoDB string, and `@1` is the version of that type id, always `@1` today. None of that matters to a pipeline: `kind` is an ordinary field you can filter and group on.
diff --git a/apps/docs/content/docs/orm/reference/raw-queries.mdx b/apps/docs/content/docs/orm/reference/raw-queries.mdx index b981c5b8641..251a6205bb4 100644 --- a/apps/docs/content/docs/orm/reference/raw-queries.mdx +++ b/apps/docs/content/docs/orm/reference/raw-queries.mdx @@ -22,7 +22,7 @@ A **whole raw statement** is a complete SQL statement written with `db.raw.sql`. A type id such as `pg/text@1` tells Prisma ORM what JavaScript type a column should become. The property is named `codecId`. `.returnsRow(spec)` names a type id for every column of a whole `db.raw.sql` statement, so you get real JavaScript values back. A fragment's `.returns(...)` does not convert anything. It only tells TypeScript what type to expect, so the value is whatever the `pg` package, the PostgreSQL driver underneath, gives you. For a `text` or an `integer` column that is already a JavaScript string or number. A `numeric` column arrives as a string. A date column gets no conversion by Prisma ORM, so you get the `pg` package's own value for that column. ::: -The examples in this section run against the `user` / `post` schema from the SQL query builder page. See its [example schema](/orm/reference/sql-query-builder#example-schema) for the full model definitions. As on that page, create the client with `postgres(...)` and reach tables through `db.sql.public`, which is keyed by table name (the model name with a lowercase first letter unless the model sets the `@@map` attribute, which renames a table). `db.runtime()` gives you the connection that runs a built query. On PostgreSQL it gives you that connection directly. On MongoDB it returns a promise, so there you write `const runtime = await db.runtime();`. +The examples in this section run against the `user` / `post` schema from the SQL query builder page. See its [example schema](/orm/reference/sql-query-builder#example-schema) for the full model definitions. As on that page, create the client with `postgres(...)` and reach tables through `db.sql.public`, which is keyed by table name (the model name with a lowercase first letter unless the model sets the `@@map` attribute, which renames a table). `db.runtime()` gives you the connection that runs a built query. ```ts import postgres from '@prisma/orm-postgres/runtime'; 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 bd68495040f..138417b8dfb 100644 --- a/apps/docs/content/docs/orm/reference/sql-query-builder.mdx +++ b/apps/docs/content/docs/orm/reference/sql-query-builder.mdx @@ -8,7 +8,7 @@ metaDescription: 'Reference for the Prisma ORM SQL query builder''s select, muta The SQL query builder builds typed queries from a table, with one method per SQL clause: `select()`, `innerJoin()`, `groupBy()`, and the rest. It is closer to SQL than the [ORM client](/orm/reference/orm-client) is, and the same `db` gives you both. Reach for it when you need a join, aggregate, or SQL feature the ORM client doesn't expose, or when you want direct control over the generated SQL. -This page documents every method, its options, and its return type. Some methods work only on some databases, and some fail only when the query runs. Each method's **Remarks** section says so. For a task-oriented guide to when and how to reach for the builder, see [Advanced queries](/orm/fundamentals/advanced-queries). +Some methods work only on some databases, and some fail only when the query runs. For a task-oriented guide to when and how to reach for the builder, see [Advanced queries](/orm/fundamentals/advanced-queries). :::note[The builder for SQL databases] The SQL query builder targets SQL databases; PostgreSQL and SQLite are supported today. MongoDB has no SQL builder: use the [ORM client](/orm/reference/orm-client) for MongoDB queries, and the [pipeline builder](/orm/reference/pipeline-builder) for aggregation pipelines. @@ -16,7 +16,7 @@ The SQL query builder targets SQL databases; PostgreSQL and SQLite are supported ## Example schema -All examples on this page run against the schema below, which is the full example schema shared across the reference pages. This page uses only the `user`, `post`, `post_tag`, and `tag` tables. The grouped-query examples use a separate `customer` and `order` schema, shown under [Grouped queries](#grouped-queries). `PostTag` is the join table. The two list fields and the `PostTag` model together are how Prisma ORM 8 declares a many-to-many, which [Relations and joins](/orm/fundamentals/relations-and-joins#many-to-many) explains. For the schema syntax itself, see [Contract authoring](/orm/contract-authoring/psl-syntax). +The examples run against the schema below and use the `user`, `post`, `post_tag`, and `tag` tables. The grouped-query examples use a separate `customer` and `order` schema, shown under [Grouped queries](#grouped-queries). `PostTag` is the join table. The two list fields and the `PostTag` model together are how Prisma ORM 8 declares a many-to-many, which [Relations and joins](/orm/fundamentals/relations-and-joins#many-to-many) explains. For the schema syntax itself, see [Contract authoring](/orm/contract-authoring/psl-syntax).
diff --git a/apps/docs/content/docs/orm/reference/transactions-and-runtime.mdx b/apps/docs/content/docs/orm/reference/transactions-and-runtime.mdx index 147c750b0fd..25c63cc7747 100644 --- a/apps/docs/content/docs/orm/reference/transactions-and-runtime.mdx +++ b/apps/docs/content/docs/orm/reference/transactions-and-runtime.mdx @@ -10,9 +10,7 @@ Prisma ORM 8 renames `schema.prisma` to `contract.prisma`. Run `npx prisma contr `db` is the client that `postgres(...)` or `mongo(...)` returns. `db.runtime()` gives you the object that actually runs queries. That object is called the runtime. -This page documents the client lifecycle (create, connect, close), transactions, prepared statements, and per-query execution options across PostgreSQL and MongoDB. For a task-oriented walkthrough, see the Fundamentals guide to [Transactions](/orm/fundamentals/transactions). For the ways to query inside a transaction, see the [ORM client reference](/orm/reference/orm-client), the [SQL query builder reference](/orm/reference/sql-query-builder), and the [raw queries reference](/orm/reference/raw-queries). - -Each method below has a Remarks list. If a method behaves differently on PostgreSQL and MongoDB, or exists on only one of them, that list says so. +Create, connect, and close the client, run transactions, prepare statements, and set per-query execution options, on PostgreSQL and MongoDB. For a task-oriented walkthrough, see the Fundamentals guide to [Transactions](/orm/fundamentals/transactions). For the ways to query inside a transaction, see the [ORM client reference](/orm/reference/orm-client), the [SQL query builder reference](/orm/reference/sql-query-builder), and the [raw queries reference](/orm/reference/raw-queries). Transactions are not available on MongoDB. To run one there, pass your own `MongoClient` with the `mongoClient` option and use the driver's `session.withTransaction(...)`, which [Transactions on MongoDB](/orm/fundamentals/transactions#transactions-on-mongodb) shows in code.