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/apps/docs/content/docs/orm/reference/index.mdx b/apps/docs/content/docs/orm/reference/index.mdx index b51ca6da61f..2114cbe9a2a 100644 --- a/apps/docs/content/docs/orm/reference/index.mdx +++ b/apps/docs/content/docs/orm/reference/index.mdx @@ -6,36 +6,46 @@ 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 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 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). +- The **ORM client** is at `db.orm`. It has model methods such as `where()`, `create()`, and `include()`, it works on both PostgreSQL and MongoDB, and it covers everyday reads and writes across your models. +- The **SQL query builder** is at `db.sql`. It has table methods that match SQL, such as `select()`, `innerJoin()`, and `groupBy()`, it is PostgreSQL only, and it returns whatever shape your `select()` asks for, not objects shaped like your models. +- The **pipeline builder** is at `db.query`. It builds MongoDB aggregation pipelines, and it is MongoDB only. -## How database differences are documented +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). -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. +## Running a query -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: +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`. -- [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) +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(); +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); +``` + +`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, ...)`. + +### MongoDB + +```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); +``` + +`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. - - }> - Every SQL query builder method for building typed, table-level queries against PostgreSQL. - - }> - Every MongoDB pipeline-builder stage, accumulator, expression helper, and write terminal. - - }> - Raw escape hatches: PostgreSQL raw SQL fragments and MongoDB raw commands. - - }> - Client lifecycle, 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 66c5a0665ff..af9175d0d3d 100644 --- a/apps/docs/content/docs/orm/reference/pipeline-builder.mdx +++ b/apps/docs/content/docs/orm/reference/pipeline-builder.mdx @@ -1,18 +1,14 @@ --- 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. +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. @@ -20,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 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`). +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.
@@ -83,7 +79,11 @@ 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('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 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. ### `from()` @@ -91,41 +91,49 @@ 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()` 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). +- `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: ...` (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. `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 | Name | Type | Required | Description | |---|---|---|---| -| `root` | Contract root name (`string` literal) | Yes | The collection to aggregate over. | +| `collection` | Collection name (`string` literal) | Yes | The collection to aggregate over. | #### Return type | 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 ##### 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 (await db.runtime()).query(plan); +const posts = await runtime.query(plan); ``` ### 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. +`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 -- `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. +- 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, 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 @@ -133,12 +141,12 @@ 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 -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,10 +154,17 @@ 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). -- 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). +- `match()` takes a callback whose `f` argument has one property per field, and returns a filter expression (`(f) => f.kind.eq('tutorial')`). +- 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 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()`, `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 @@ -161,7 +176,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 @@ -172,7 +187,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,13 +199,18 @@ 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**, 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 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'; +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()` @@ -219,7 +239,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 +268,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 +293,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 +318,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 +352,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,15 +361,16 @@ 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(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. 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 | 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 @@ -371,8 +392,7 @@ 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); ``` ### `project()` @@ -382,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) }))`), 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, 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 @@ -403,8 +423,7 @@ 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); ``` ##### Callback form @@ -416,7 +435,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 +444,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. +- `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 @@ -439,7 +457,23 @@ 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 + +##### Unroll an array field + +This example needs a `tags String[]` field on `Post`, which the example schema 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); +``` ### `group()` @@ -447,10 +481,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**. -- 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.) +- `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 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 with the same code and the message `group() field "" must not be null. Only _id can be null.` #### Options @@ -479,7 +513,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,15 +525,14 @@ 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); -// { _id: null, total: 2, latest: } +const [summary] = await runtime.query(plan); ``` For Prisma ORM 7 users, `groupBy` becomes a `group()` stage on the pipeline builder: ```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(), + ); ``` @@ -510,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 @@ -542,13 +575,12 @@ const plan = db.query ) .replaceRoot((f) => fn.arrayElemAt(f.author, fn.literal(0))) .build(); -const authors = await (await db.runtime()).query(plan); -// each document is the promoted author sub-document +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 @@ -568,8 +600,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); -// { total: 2 } +const [{ total }] = await runtime.query(plan); ``` ### `sortByCount()` @@ -601,8 +632,7 @@ 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); ``` ### `redact()` @@ -611,20 +641,54 @@ 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. +- `redact()` maps to MongoDB's `$redact`, whose expression must evaluate to one of MongoDB's `$$` variables: `$$KEEP`, `$$DESCEND`, or `$$PRUNE`. +- 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 + +| 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 -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. 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 -- 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({ 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 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. +- `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. +- 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 @@ -632,11 +696,49 @@ Several stages take a single MongoDB stage options object rather than a typed fi ```typescript const plan = db.query.from('posts').unionWith('users').build(); -const combined = await (await db.runtime()).query(plan); -// posts followed by users +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'; + +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: }]; facets.newest is the two newest posts ``` -`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. +##### Sort into ranges with `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'; + +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 @@ -644,10 +746,10 @@ const combined = await (await db.runtime()).query(plan); #### 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 }`). -- Verify these against a real Atlas deployment before relying on them. +- 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. #### Return type @@ -657,13 +759,13 @@ const combined = await (await db.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'; ``` -The builder exposes all nineteen MongoDB accumulators: +The builder exposes nineteen accumulators: | Accumulator | Signature | Description | |---|---|---| @@ -678,180 +780,196 @@ The builder exposes all nineteen MongoDB 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. | -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. +- `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. 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. #### 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. 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'; -const plan = db.query - .from('posts') - .group((f) => ({ _id: null, total: acc.count(), durationSum: acc.sum(fn.literal(1)) })) - .build(); -const [{ total, durationSum }] = await (await db.runtime()).query(plan); -// { total: 2, durationSum: 2 } +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 } ``` ##### `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), - latest: acc.max(f.createdAt), - avgYear: acc.avg(fn.year(f.createdAt)), - })) +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 (await db.runtime()).query(plan); -// { earliest: , latest: , avgYear: 2024 } +const [stats] = await runtime.query(plan); +// { earliest: , avgYear: 2024 } ``` ##### `first()` and `last()` ```typescript -import { acc } from '@prisma/orm-mongo/query-builder'; - -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 (await db.runtime()).query(plan); +const [{ firstTitle, lastTitle }] = await runtime.query(plan); // { firstTitle: 'Hello world', lastTitle: 'Tutorial one' } ``` ##### `push()` and `addToSet()` -```typescript -import { acc } from '@prisma/orm-mongo/query-builder'; +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. -const plan = db.query - .from('posts') +##### `firstN()` (an `N`-variant) + +```typescript +const plan = db.query.from('posts').sort({ createdAt: 1 }) .group((f) => ({ _id: null, - allTitles: acc.push(f.title), - distinctKinds: acc.addToSet(f.kind), + firstTwo: acc.firstN({ input: f.title, n: fn.literal(2) }), + newestTitle: acc.top({ output: f.title, sortBy: { createdAt: -1 } }), // the title string, not a document })) .build(); -const [{ allTitles, distinctKinds }] = await (await db.runtime()).query(plan); +const [{ firstTwo, newestTitle }] = await runtime.query(plan); +// firstTwo is ['Hello world', 'Tutorial one'] ``` -##### `firstN()` (an `N`-variant) +## Expression helpers -```typescript -import { acc, fn } 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`: -const plan = db.query - .from('posts') - .sort({ createdAt: 1 }) - .group((f) => ({ _id: null, firstTwo: acc.firstN({ input: f.title, n: fn.literal(2) }) })) - .build(); -const [{ firstTwo }] = await (await db.runtime()).query(plan); -// firstTwo is ['Hello world', 'Tutorial one'] +```typescript +import { fn } from '@prisma/orm-mongo/query-builder'; ``` -## Expression helpers +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 | +|---|---| +| `$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`. + +| 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` | -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`: +`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()`, `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. | +| 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`: ```typescript -import { fn } from '@prisma/orm-mongo/query-builder'; +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(); ``` -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. +For OR, AND, or NOT, pass `match()` a filter you build yourself instead of a callback: -| 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`. | +```typescript +import { MongoAndExpr, 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 -- **`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. +- 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 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). +- `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?, 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 }` | +| `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 ##### 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))), - })) +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 (await db.runtime()).query(plan); +const rows = await runtime.query(plan); // computed is 7 for every document ``` ##### 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('!')) })) - .build(); -const rows = await (await db.runtime()).query(plan); -``` +`project((f) => ({ shout: fn.concat(fn.toUpper(f.title), fn.literal('!')) }))` puts the title in upper case next to a `!`. ##### Branch with `cond()` ```typescript -import { fn } from '@prisma/orm-mongo/query-builder'; - -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'), - ), + 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); -``` - -##### Array length - -```typescript -import { fn } from '@prisma/orm-mongo/query-builder'; - -const plan = db.query - .from('posts') - .project((f) => ({ title: 1, titleWords: fn.size(fn.split(f.title, fn.literal(' '))) })) - .build(); -const rows = await (await db.runtime()).query(plan); +const rows = await runtime.query(plan); ``` ### `pipe()` @@ -860,108 +978,97 @@ 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. -- Prefer a typed stage when one exists; `pipe()` is the in-chain escape hatch, and [`rawCommand()`](#rawcommand) is the whole-command one. - -## Read terminals +- `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` 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. -A read terminal turns the chain into an executable plan. Pass the plan to `(await db.runtime()).query(plan)` to run it. +#### Examples -### `build()` and `aggregate()` +##### Append a raw stage -Compile the pipeline into a plan. +```typescript +import { MongoCountStage, MongoFieldFilter, MongoMatchStage } from '@prisma/orm-mongo/query-ast/execution'; -#### Remarks +const plan = db.query.from('posts') + .pipe(new MongoMatchStage(MongoFieldFilter.eq('kind', 'tutorial'))).build(); +const rows = await runtime.query(plan); -- `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. +type Counted = { total: number }; +const countPlan = db.query.from('posts').pipe(new MongoCountStage('total')).build(); +``` -#### Return type +## Read methods [#read-terminals] -| Return type | Example | Description | -|---|---|---| -| Query plan | `db.query.from('posts').build()` | A plan you run with `(await db.runtime()).query(...)`. | +Finish a read chain with `build()`, then run the built query with `runtime.query(...)`. -#### Examples +### `build()` and `aggregate()` -##### `build()` and its `aggregate()` alias +Compile the pipeline into a query you can run. -```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 +#### Remarks -const rows = await (await db.runtime()).query(aggregated); -``` +- 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 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 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: -- 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. +- 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 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 -`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 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. +- 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 }`, 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. +- [`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 ##### `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); -// { insertedIds: { '0': , '1': } } +const [insertManyResult] = await runtime.query(manyPlan); +// { insertedIds: [, ], insertedCount: 2 } ``` ##### `updateAll()` (array-of-ops updater) -```typescript -const plan = db.query.from('users').updateAll((f) => [f.bio.set('everyone now has bio')]); -await (await db.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 (await db.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) -On the root collection, `upsertOne()` takes a filter callback and an updater callback: +`upsertOne(filterCallback, updaterCallback)`. The first callback returns the filter. The second returns the array of update operations. ```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 ``` @@ -971,85 +1078,57 @@ 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. +- `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)`. +- `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 ##### `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 (await db.runtime()).query(manyPlan); +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 (await db.runtime()).query(onePlan); +const [result] = await runtime.query(onePlan); // { matchedCount: 1, modifiedCount: 1 } ``` ##### `deleteMany()` and `deleteOne()` ```typescript -const plan = db.query - .from('users') - .match((f) => f.role.eq('author')) - .deleteMany(); -const [result] = await (await db.runtime()).query(plan); +const plan = db.query.from('users').match((f) => f.role.eq('author')).deleteMany(); +const [result] = await runtime.query(plan); // { deletedCount: 2 } ``` ##### `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 (await db.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` -`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 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 - .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 (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'`. +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')) - .findOneAndDelete(); -const [deleted] = await (await db.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 @@ -1057,69 +1136,84 @@ 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. -- 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, 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`. +- 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. 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. | +| `setOnInsert(value)` | A value written only when an upsert inserts a new document, so it does nothing outside `upsertOne()`. | #### Examples ##### 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 (await db.runtime()).query(plan); +await runtime.query(plan); +// 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')]) ``` ##### 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 (await db.runtime()).query(plan); +await 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. 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`. `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 -##### Materialize with `out()` +##### Write the output with `out()` -```typescript -const plan = db.query.from('users').out('users_snapshot'); -await (await db.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()` ```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 raw command (for example `new RawAggregateCommand(collection, pipeline)`) into a plan. 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). +- `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,19 +1223,18 @@ 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'; 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 db0554ed5eb..251a6205bb4 100644 --- a/apps/docs/content/docs/orm/reference/raw-queries.mdx +++ b/apps/docs/content/docs/orm/reference/raw-queries.mdx @@ -1,41 +1,41 @@ --- 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] -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. -::: +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 -Raw SQL comes in two sizes. +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 **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 **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(...)`. -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. +:::warning[Only `.returnsRow()` converts values for you] +A type id such as `pg/text@1` tells Prisma ORM what JavaScript type a column should become. The property is named `codecId`. `.returnsRow(spec)` names a type id for every column of a whole `db.raw.sql` statement, so you get real JavaScript values back. A fragment's `.returns(...)` does not convert anything. It only tells TypeScript what type to expect, so the value is whatever the `pg` package, the PostgreSQL driver underneath, gives you. For a `text` or an `integer` column that is already a JavaScript string or number. A `numeric` column arrives as a string. A date column gets no conversion by Prisma ORM, so you get the `pg` package's own value for that column. +::: -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(...)` 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'; 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 -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 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 @@ -47,11 +47,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(...)` 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 -`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 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 @@ -64,16 +64,12 @@ 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. 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 .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' @@ -81,18 +77,32 @@ 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`. +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`, which reads back as a `number` | +| any other `number`, including a fractional one | `pg/float8@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 `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'; -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 @@ -102,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 @@ -115,9 +120,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 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'); @@ -128,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 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 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; @@ -140,46 +145,41 @@ 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, - 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, 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 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` UPDATE "post" - SET "viewCount" = "viewCount" + 1 - WHERE "userId" IN (SELECT id FROM "user" WHERE kind = ${kind}) -` - .affectedCount() - .build(); + SET priority = ${'urgent'} + WHERE "userId" IN (SELECT id FROM "user" WHERE kind = ${'admin'}) +`.affectedCount().build(); 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: +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` SELECT p."userId" AS "userId", count(*) AS "postCount" FROM "post" p GROUP BY p."userId" - HAVING count(*) >= ${minPosts} -`.returnsRow({ - userId: post.columns.userId, - postCount: 'pg/int8@1', -}); + HAVING count(*) >= ${3} +`.returnsRow({ userId: post.columns.userId, postCount: 'pg/int8@1' }); const plan = db.raw.sql` WITH active AS (${authorsWithPosts}) @@ -187,93 +187,76 @@ 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(); ``` -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. - ### 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 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 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(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 an object with nine methods for running MongoDB commands directly against a collection. The collection name is the one your contract declares (`'users'`, `'posts'`), the same name the ORM client uses (`db.orm.users`). MongoDB has no schemas, so there is no `public` segment here and none on `db.orm`. There is no raw `find()`, so use `aggregate()` with a `$match` stage. MongoDB has no transactions in Prisma ORM, so a raw command cannot run inside one. See [Transactions (MongoDB)](/orm/reference/transactions-and-runtime#transactions-mongodb). + +Each of these nine methods builds a command. Call `.build()` on it, then run every one of them with `runtime.query(...)`, including the writes. The raw collection methods all run through `query()`. A write returns a one-element array, so destructure it with `const [result] = ...`. -Each 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: +:::warning[MongoDB raw results are native BSON values] +Nothing on the MongoDB side of this page is converted for you. An `_id` comes back as it is stored: one that MongoDB generated is an `ObjectId` instance from the `mongodb` package, the MongoDB driver underneath, not a hex string. Compare one with `String(row._id) === aliceId`. Raw filters take native BSON values too, so build ids with that package's `ObjectId` class. The write methods give you the counts and ids under the `mongodb` package's own key names, `{ insertedId }`, `{ matchedCount, modifiedCount, upsertedCount, upsertedId }`, and `{ deletedCount }`, and you read those keys directly. +::: + +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'; ``` -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, 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); -// rows[0].title === 'Hello world' -// rows[0]._id is a raw ObjectId — String(rows[0]._id) is the hex string +const plan = db.raw.collection('posts').aggregate<{ _id: unknown; title: string }>([{ $match: { title: 'Hello world' } }]).build(); +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. +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 onePlan = db.raw - .collection('users') - .insertOne({ name: 'Dave', email: 'dave@example.com', role: 'author' }) - .build(); -const [oneResult] = await (await db.runtime()).query(onePlan); +const users = db.raw.collection('users'); +const onePlan = users.insertOne({ name: 'Dave', email: 'dave@example.com', role: 'author' }).build(); +const [oneResult] = await runtime.query(onePlan); // { insertedId: } -const manyPlan = db.raw - .collection('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: [, ] } ``` -`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 [result] = await (await db.runtime()).query(plan); +const plan = db.raw.collection('users').updateOne({ _id: new ObjectId(aliceId) }, { $set: { bio: 'Updated bio' } }).build(); +const [result] = await runtime.query(plan); // { matchedCount: 1, modifiedCount: 1, ... } ``` @@ -282,11 +265,8 @@ 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 [result] = await (await db.runtime()).query(plan); +const plan = db.raw.collection('users').updateMany({ role: 'author' }, [{ $set: { bio: { $concat: ['bio for ', '$name'] } } }]).build(); +const [result] = await runtime.query(plan); // { matchedCount: 2, modifiedCount: 2, ... } ``` @@ -296,39 +276,51 @@ 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 } ``` ### `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. 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. -- 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 `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: + +- `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'; + +// 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 -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. +`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' }; 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()` @@ -337,20 +329,26 @@ 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); -// deleted is the removed document; deleted._id is a raw ObjectId +const [deleted] = await runtime.query(plan); +// 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), bypassing the typed AST. Its rows come back as `unknown`; you type 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'; const plan = db.query.rawCommand(new RawAggregateCommand('posts', [{ $count: 'total' }])); -const rows = await (await db.runtime()).query(plan); +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, 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 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 rows = await runtime.query(plan); +``` 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..138417b8dfb 100644 --- a/apps/docs/content/docs/orm/reference/sql-query-builder.mdx +++ b/apps/docs/content/docs/orm/reference/sql-query-builder.mdx @@ -6,17 +6,17 @@ 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 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. 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). +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[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, 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). +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).
@@ -128,11 +128,15 @@ 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. +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. -### The `db.sql` facet +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`. -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(...)`. +### The `db.sql` builder [#the-dbsql-facet] + +`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. ```ts import postgres from '@prisma/orm-postgres/runtime'; @@ -140,55 +144,38 @@ 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): - -```ts -import { sql } from '@prisma/orm-postgres/builder/runtime'; - -const publicSql = sql({ context, rawCodecInferer: { inferCodec: () => 'pg/text' } }).public; -``` - -This page reaches tables through the client facet 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`. 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 -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. ```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()` -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). - -```ts -const highPriorityPosts = db.sql.public.post - .select('id', 'userId') - .where((f, fns) => fns.eq(f.priority, 'high')) - .as('hp'); -``` - -:::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. -::: +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(...)`. +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 `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 set of column names a query can use is its scope. It starts as the columns of the table you began with, and every join adds the joined table's columns to it. + +The `where()`, `select()`, `orderBy()`, `update()`, and other callback forms receive two arguments. The first is `f`, which has one property per column in scope. After a join, every column is on `f` under its table name, such as `f.user.email` or `f.post.id`. A column name that only one table has is also on `f` directly. The second is `fns`, an object of [expression helpers](#expressions-and-functions) such as `fns.eq()` and `fns.raw`. For an enum column you pass the value stored in the database. That is the string after `=`, so `High = "high"` is matched by `fns.eq(f.priority, 'high')`. A member with no `=`, such as `admin` in `user_type`, is matched by its own name, `fns.eq(f.kind, 'admin')`. ### `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 @@ -196,11 +183,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 `Column "x" not found in scope`. | +| 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. | -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. To return fewer columns, start a new query instead. + +A computed expression's result type comes from `.returns(...)` on `fns.raw`, or from the `fns.*` function you called. Every `fns.raw` fragment needs `.returns()`. A type id is `pg/` plus the PostgreSQL type name plus a version, always `@1` today, such as `pg/int4@1` or `pg/uuid@1`. A few use a longer name, such as `pg/timestamptz-temporal@1` for `DateTime`. The common ones are `pg/text@1`, `pg/int4@1`, `pg/int8@1`, `pg/float8@1`, `pg/bool@1`, `pg/uuid@1`, and `pg/timestamptz-temporal@1`. The full list is what your contract uses. Read any column's id from `db.sql.public.
.columns..codecId`, and see [Binding a bare value with `param()`](/orm/reference/raw-queries#binding-a-bare-value-with-param) for which JavaScript type becomes which id. See [`fns.raw` and `.returns()`](#fnsraw-and-returns). #### Return type @@ -210,12 +199,22 @@ 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 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 @@ -252,7 +251,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 +278,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. 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 @@ -292,7 +291,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 @@ -310,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()` 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. +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 @@ -332,20 +327,21 @@ 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:** `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. -- 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. +- **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. +- 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 | 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,17 +373,22 @@ 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 -`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. | -| `options.nulls` | `'first' \| 'last'` | No | Where nulls sort relative to non-null values. | + +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.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 @@ -414,16 +415,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' }) - .build(); -const rows = await runtime.query(plan); -``` - ### `distinct()` De-duplicate identical projected rows (`SELECT DISTINCT`). @@ -450,8 +441,8 @@ 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. -- 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. 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 @@ -488,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 @@ -513,7 +504,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 `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 @@ -525,7 +516,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 @@ -546,44 +537,21 @@ 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 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 `customer` / `order` schema, where `order.amount` and `order.quantity` are numeric columns: - -
- -Expand for the aggregate example schema +The examples below use an extra table, `order`, whose `amount` column is an integer: ```prisma -model Customer { - id Uuid @id @default(uuid()) - name String - segment String - - orders Order[] - - @@map("customer") -} - model Order { id Uuid @id @default(uuid()) customerId Uuid amount Int - quantity Int placedAt DateTime @default(now()) - customer Customer @relation(fields: [customerId], references: [id]) - @@map("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. -::: - ### `groupBy()` Group rows by one or more columns or by a computed expression, producing one row per distinct group. @@ -605,37 +573,60 @@ Group rows by one or more columns or by a computed expression, producing one row ##### Group by a column with a count +The examples below reuse the `runtime` created here, and name each built query `plan`, the object `build()` returns. + ```ts +const runtime = db.runtime(); + 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: acmeId, orderCount: 5 } +// rows === [{ customerId: '', orderCount: 5 }] +``` + +:::note[What the aggregates give you back] +- 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((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 a string: a raw expression is not decoded through a codec, and EXTRACT returns numeric. +// 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')) +``` + +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()` 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)`. 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 @@ -660,37 +651,27 @@ 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()` ```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; avgDecimal(f.amount) would give the exact '300.0000000000000000' -// minAmount / maxAmount are 100 / 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()` 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. 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 @@ -704,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. +:::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()` @@ -718,14 +702,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 `@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. Values are auto-parameterized; their codecs are inferred from the target columns. | +| `rows` | Array of row objects | Yes | The rows to insert. Every column is optional in TypeScript. | + +PostgreSQL still rejects a missing required column that has no default. Never call `.returns()` or [`param()`](#param) on an insert value. #### Return type @@ -738,19 +725,14 @@ 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(); -await runtime.execute(plan); +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()) ``` ##### 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' }, - ]) - .build(); +const plan = db.sql.public.tag.insert([{ label: 'multi-row-a' }, { label: 'multi-row-b' }]).build(); await runtime.execute(plan); ``` @@ -760,7 +742,8 @@ 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. +- `returning()` takes column names only. There is no `returning('*')`, and you cannot return a computed expression. #### Options @@ -772,32 +755,32 @@ 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 ##### 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 rows = await runtime.query(plan); -// rows === [{ id, label: 'returned-tag' }] +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' }] ``` ### `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. #### 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 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 | Return type | Example | Description | @@ -811,28 +794,29 @@ 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' }] +const rows = await runtime.query(plan); // one row, with displayName now 'Bobby' ``` ##### Derive a value from an existing column ```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)) + .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); -// rows === [{ id: carolId, displayName: 'CAROL' }] +const rows = await runtime.query(plan); // one row, with displayName now 'CAROL' and a fixed new email ``` ### `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. #### Return type @@ -847,29 +831,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' }] +const rows = await runtime.query(plan); // one row, the tag that was deleted ``` ### `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`. +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 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. -- There is **no** `build({ params })` form. Parameter values are embedded into the query where they're supplied. See [`build()`](#build). +- 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 | 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 send the value as, for example `'pg/text@1'`. | #### Return type @@ -884,65 +867,80 @@ Force an explicit codec on a raw value that has no column context to infer one f ```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')) - .orderBy('email', { direction: 'asc' }) + .where((f, fns) => fns.raw`${f.id} = ${targetId}`.returns('pg/bool@1')) .build(); 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 `fns` alongside the `f` argument. `fns` is an object of helper functions: comparisons, boolean combinations, aggregates, and raw SQL. ### 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` | -| Raw SQL | `raw` | +| 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)` | +| Boolean | `and(...predicates)`, `or(...predicates)` | +| 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. -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: +```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 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)))) + .build(); +``` + +Which aggregate functions exist depends on your database. PostgreSQL gives you these eight. The contract type is the one you write in `contract.prisma`, and the PostgreSQL type is in brackets: | 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 | -| `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 | - -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. - -:::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. -::: +| `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 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 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 `${...}`; 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 `${...}`. 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 for a projected or compared value | Declares the result type, for example `'pg/int4@1'`, `'pg/text@1'`, `'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 -- `.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)). -- Use `fns.raw` for any SQL feature without a dedicated helper: `COALESCE`, `CAST`, `LENGTH`, `UPPER`, `EXTRACT`, and so on. +- **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. 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 @@ -952,56 +950,47 @@ 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); -``` - -##### `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()` -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. You supply parameter values where you write them, inside `insert([...])`, a `where()` callback, or a [`param()`](#param) call. #### 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(...)`. You can get the TypeScript type of one row 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`, which is `db.runtime()`. -```ts -const plan = db.sql.public.user.select('id', 'email').build(); -const rows = await runtime.query(plan); // Row[] -``` +| 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` -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`. -- 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). +- 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`. +- 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 -##### 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 +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. 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. 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 4faf32daef1..25c63cc7747 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,13 @@ 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. +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 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. +`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. -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. +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). -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). +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 @@ -24,28 +24,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 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`. -- 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. +- 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`. `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). +- 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 +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. | -| `poolOptions` | `{ connectionTimeoutMillis?: number; idleTimeoutMillis?: number }` | No | Overrides for the internal pool timeouts (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. | +| `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 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'`. | #### 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 })` | 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 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.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 @@ -56,9 +68,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. 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 ```typescript @@ -67,9 +81,8 @@ 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 ``` ### `connect()` @@ -78,8 +91,15 @@ 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 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). + - `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()`. #### Return type @@ -102,7 +122,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 @@ -110,23 +130,14 @@ 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()`, calling `connect()` again rejects with `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`. `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`). #### Return type @@ -139,9 +150,8 @@ Close the client and release its pool. ##### Close when finished ```typescript -const db = postgres({ contractJson, url: process.env.DATABASE_URL }); -await db.connect(); -// ... run queries ... +const db = postgres({ contractJson, url: process.env.DATABASE_URL! }); +const admins = await db.orm.public.User.where((u) => u.kind.eq('admin')).all(); await db.close(); ``` @@ -151,9 +161,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 `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 `"esnext.disposable"` added to the `lib` array in your `tsconfig.json`. #### Examples @@ -161,9 +170,8 @@ 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 ... + 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 ``` @@ -177,26 +185,35 @@ 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. -- `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. +- 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. `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). #### Options +Pass exactly one of `url`, `uri`, `mongoClient`, and `binding`. `dbName` is separate, and `uri` and `mongoClient` both require it. + | 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. | +| `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 | 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 | 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 })` | 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). +- `.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 @@ -207,7 +224,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` @@ -218,9 +235,9 @@ 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 ``` ### `connect()` @@ -229,9 +246,8 @@ 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`). +- 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' })`. #### Return type @@ -239,28 +255,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(); // throws DRIVER.ALREADY_CONNECTED -``` - ### `runtime()` 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. - -:::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)). +- 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 @@ -268,37 +270,30 @@ PostgreSQL's `runtime()` is synchronous and returns a `Runtime` directly. MongoD |---|---|---| | `Promise` | `const runtime = await db.runtime()` | The runtime, resolved asynchronously. | -#### Examples - -##### Await the runtime - -```typescript -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. -- 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. +- `(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 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 | 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); +// '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); ``` ### `close()` @@ -307,9 +302,8 @@ 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. -- When you supplied a `mongoClient`, `close()` does not close your client (see [`mongo(options)`](#mongooptions)). +- 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 @@ -323,7 +317,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) @@ -332,8 +326,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`. +- 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 @@ -341,15 +334,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. 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. 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)` @@ -357,23 +349,27 @@ 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.
`. +- 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. 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. - 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 `RUNTIME.TRANSACTION_CLOSED` (see the Remark below). +- `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 | 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 | 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. | #### Examples @@ -411,18 +407,20 @@ 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) => { + 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 }, ); }); ``` :::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. +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) => { @@ -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 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 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. +- 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 @@ -478,115 +476,114 @@ A lower-level transaction helper you import directly, without going through the ##### Commit two writes with `withTransaction` ```typescript -import { withTransaction } from '@prisma/orm-postgres/family-runtime'; +import { withTransaction, type Runtime } from '@prisma/orm-postgres/family-runtime'; -const runtime = await db.connect(); -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()); -}); -``` - -##### Roll back on error - -```typescript -import { withTransaction } from '@prisma/orm-postgres/family-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, sql: typeof db.sql) { + return withTransaction(runtime, async (tx) => { + 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()); }); -} catch { - // the tag was rolled back } + +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. +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 plans 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. +- `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 -| 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` | — | 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()` | none | Throw this connection away instead of returning it to 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( - db.sql.public.tag.insert([{ id: crypto.randomUUID(), label: 'manual-commit' }]).build(), -); -await transaction.commit(); -await connection.release(); +try { + await connection.query(db.sql.public.tag.select('id').limit(1).build()); // a read on this connection + const transaction = await connection.transaction(); + 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(); +} ``` ##### 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(); + try { + await transaction.execute( + db.sql.public.tag.insert([{ id: crypto.randomUUID(), label: 'manual-rollback' }]).build(), + ); + } finally { + await transaction.rollback(); + } +} finally { + await connection.release(); +} ``` -##### Evict a connection with `destroy()` +##### Throw a connection away with `destroy()` ```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()`. 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. - -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 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. ::: -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. - ## 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 not available on MongoDB. ### `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 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' }`. 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 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 | 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 @@ -599,12 +596,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' }); @@ -616,30 +610,26 @@ 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'] } ``` ### `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. +- `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 | 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 @@ -653,11 +643,7 @@ Prepare a statement off the client facade. ```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' }); @@ -669,9 +655,18 @@ 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. -- 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. +- 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) => + db.raw.sql`UPDATE "tag" SET label = 'archived' WHERE label = ${params.label}`.affectedCount().build(), + ); + + const { affectedRows } = await archive.execute(runtime, { label: 'stale' }); + ``` + +- 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. #### Options @@ -684,7 +679,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 @@ -694,11 +689,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) => { @@ -713,24 +704,24 @@ 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` -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(...)` 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(...)` 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. #### 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 | A label middleware reads. The runtime sets it; setting it yourself changes only the label, not where the query runs. | #### Examples @@ -743,7 +734,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()` @@ -752,9 +743,11 @@ 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`). -- `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. +- 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? }`. 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 @@ -777,6 +770,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. +[`all()`](/orm/reference/orm-client#all), [`createAll()`](/orm/reference/orm-client#createall), and the runtime's `query(...)` return an `AsyncIterableResult`. `all()` and `query(...)` read rows, and `createAll()` writes rows and returns the ones it wrote. `await` the result to collect an array, or `for await` it to take rows one at a time. + +Use `await`. Use `for await` only to handle rows as they arrive. On PostgreSQL it does not reduce memory, because every row is loaded first. -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. +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/apps/docs/cspell.json b/apps/docs/cspell.json index e90bae5aaf1..e1eb0c6d4d7 100644 --- a/apps/docs/cspell.json +++ b/apps/docs/cspell.json @@ -467,7 +467,12 @@ "urlencode", "extensionless", "EBADDEVENGINES", - "Initialising" + "Initialising", + "dbsql", + "txexecute", + "txsql", + "ltrim", + "rtrim" ], "patterns": [ {