From 1fc786e68c409aa787aac762d1bd64747e4599e2 Mon Sep 17 00:00:00 2001 From: Nurul Sundarani Date: Fri, 3 Jul 2026 20:18:17 +0530 Subject: [PATCH 01/15] docs(next): add Prisma Next reference section index Co-Authored-By: Claude Fable 5 --- .claude/launch.json | 12 ++ apps/docs/content/docs/(index)/meta.json | 1 + .../docs/(index)/next/reference/index.mdx | 183 ++++++++++++++++++ .../docs/(index)/next/reference/meta.json | 4 + .../(index)/next/reference/orm-client.mdx | 12 ++ .../next/reference/sql-query-builder.mdx | 12 ++ 6 files changed, 224 insertions(+) create mode 100644 .claude/launch.json create mode 100644 apps/docs/content/docs/(index)/next/reference/index.mdx create mode 100644 apps/docs/content/docs/(index)/next/reference/meta.json create mode 100644 apps/docs/content/docs/(index)/next/reference/orm-client.mdx create mode 100644 apps/docs/content/docs/(index)/next/reference/sql-query-builder.mdx diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000000..05542fc1b7 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,12 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "docs", + "runtimeExecutable": "pnpm", + "runtimeArgs": ["--filter", "docs", "exec", "next", "dev"], + "port": 3001, + "autoPort": true + } + ] +} diff --git a/apps/docs/content/docs/(index)/meta.json b/apps/docs/content/docs/(index)/meta.json index c7375e791a..dcd7b18ac7 100644 --- a/apps/docs/content/docs/(index)/meta.json +++ b/apps/docs/content/docs/(index)/meta.json @@ -11,6 +11,7 @@ "---Prisma Next---", "next/quickstart", "next/add-to-existing-project", + "next/reference", "---Prisma ORM---", "...prisma-orm", "---Prisma Postgres---", diff --git a/apps/docs/content/docs/(index)/next/reference/index.mdx b/apps/docs/content/docs/(index)/next/reference/index.mdx new file mode 100644 index 0000000000..4f61805f5d --- /dev/null +++ b/apps/docs/content/docs/(index)/next/reference/index.mdx @@ -0,0 +1,183 @@ +--- +title: Prisma Next API reference +description: Availability matrix and reference index for the Prisma Next ORM client and SQL query builder. +url: /next/reference +metaTitle: Prisma Next API reference +metaDescription: Availability matrix and reference index for the Prisma Next ORM client and SQL query builder. +badge: early-access +--- + +Prisma Next has two 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. + +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, or when you want direct control over the generated SQL. + +## How availability is documented + +Each method's reference page includes a Remarks note when its behavior differs between PostgreSQL and MongoDB, or when it's type-checked but not enforced at runtime. The table below summarizes availability across both databases. Read the linked method page for the full behavior, including any type-vs-runtime gaps. + +The tables use this legend: + +- **✅ Available**: verified working against a live database. +- **❌ Not available**: the method doesn't exist on this database's client. Calling it throws `TypeError`. +- **⚠️ Unverified**: the method is expected to work but hasn't been verified against a live database on the current test contract, or is a type-only restriction rather than a runtime one. See the method's reference page for details. +- **— Not applicable**: this database has no equivalent surface by design (rather than a missing or unverified method). + +### Query building + +| Method | PostgreSQL | MongoDB | +|---|---|---| +| `where()` (callback form) | ✅ | ⚠️ | +| `where()` (shorthand object form) | ✅ | ✅ | +| `where()` (chained calls, ANDed) | ✅ | ⚠️ | +| `select()` | ✅ | ✅ | +| `include()` (to-one relation) | ✅ | ⚠️ | +| `include()` (to-many relation) | ✅ | ⚠️ | +| `include()` (reference relation) | ⚠️ | ✅ | +| `include()` refinement (filter, take, orderBy) | ✅ | ❌ | +| `include()` refinement scalar reducers (`count`, `sum`, `avg`, `min`, `max`, `combine`) | ✅ | ❌ | +| `orderBy()` | ✅ | ✅ | +| `take()` | ✅ | ✅ | +| `skip()` | ✅ | ✅ | +| `cursor()` | ✅ | ❌ | +| `distinct()` | ✅ | ❌ | +| `distinctOn()` | ✅ | ❌ | +| `groupBy()` | ✅ | ❌ | +| `variant()` | ✅ | ✅ | + +### Read terminals + +| Method | PostgreSQL | MongoDB | +|---|---|---| +| `all()` awaited (collect to array) | ✅ | ✅ | +| `all()` as an async iterable (stream) | ✅ | ✅ | +| `first()` | ✅ | ✅ | +| `first()` inline callback filter | ✅ | ⚠️ | +| Custom `Collection` subclass with domain methods | ✅ | — | + +### Mutations + +| Method | PostgreSQL | MongoDB | +|---|---|---| +| `create()` | ✅ | ✅ | +| `create()` nested `create()` (child-owned relation) | ✅ | ⚠️ | +| `create()` nested `connect()` (parent-owned relation) | ✅ | ⚠️ | +| `createAll()` awaited and streamed | ✅ | ✅ | +| `createCount()` | ✅ | ✅ | +| `update()` (data object) | ✅ | ✅ | +| `update()` requires a prior `where()` | ⚠️ | ✅ | +| `update()` nested `connect()` | ✅ | ⚠️ | +| `update()` nested `disconnect()` | ✅ | ⚠️ | +| `update()` field-operations callback form | ❌ | ✅ | +| `updateAll()` | ✅ | ✅ | +| `updateCount()` | ✅ | ✅ | +| `updateCount()` field-operations callback form | ⚠️ | ✅ | +| `delete()` | ✅ | ✅ | +| `delete()` requires a prior `where()` | ⚠️ | ✅ | +| `deleteAll()` | ✅ | ✅ | +| `deleteCount()` | ✅ | ✅ | +| `upsert()` | ✅ | ✅ | +| `upsert()` field-operations callback form (update side) | — | ✅ | + +### Field update operations + +Mongo field operations are accessed through a field accessor inside `update()`/`updateCount()`/`upsert()` callbacks (for example `t.field.inc(...)`). PostgreSQL's `update()` has no equivalent callback form; see the mutations table above. + +| Field operation | MongoDB | +|---|---| +| `set()` | ✅ | +| `inc()` | ✅ | +| `mul()` | ✅ | +| `unset()` | ✅ | +| `push()` / `pull()` / `addToSet()` / `pop()` | ⚠️ | +| `rename()` | ❌ | +| `min()` / `max()` / `currentDate()` | ❌ | + +### Filters & operators + +| Filter or operator | PostgreSQL | MongoDB | +|---|---|---| +| `eq()` / `neq()` | ✅ | ✅ | +| `gt()` / `lt()` / `gte()` / `lte()` | ✅ | ✅ | +| `like()` | ✅ | — | +| `ilike()` | ✅ | — | +| `in()` / `notIn()` | ✅ | ✅ | +| `isNull()` / `isNotNull()` | ✅ | ✅ | +| `and()` / `not()` (combinators) | ✅ | ✅ | +| `or()` (combinator) | ✅ | ❌ (use `MongoOrExpr.of(...)`) | +| `all()` (constant-true predicate) | ✅ | — | +| `some()` / `every()` / `none()` (relation filters) | ✅ | — | +| Dot-notation path into an embedded object | — | ⚠️ | +| `exists` / `regex` / `elemMatch` / `all` / `size` filters | — | ❌ | +| `$nor` combinator | — | ❌ | + +MongoDB filters are built with `MongoFieldFilter` static factories (`eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`, `isNull`, `isNotNull`) plus `and()`/`not()` instance methods and the standalone `MongoOrExpr.of(...)` for OR. There's no `.or()` instance method and no `$nor` support. + +### Aggregates + +| Method | PostgreSQL | MongoDB | +|---|---|---| +| `aggregate()` with `count()` | ✅ | ❌ | +| `aggregate()` with `sum()` / `avg()` | ✅ | ❌ | +| `aggregate()` with `min()` / `max()` | ✅ | ❌ | +| `groupBy().aggregate()` | ✅ | ❌ | +| `groupBy().having()` | ✅ | ❌ | + +`sum()` and `avg()` resolve to `null`, not `0`, when the aggregated result set is empty. `count()` resolves to `0` in the same case. + +### SQL builder + +The SQL query builder targets PostgreSQL only, so these tables have no MongoDB column. + +| Method | PostgreSQL | +|---|---| +| `select()` (columns, aliased expression, object-of-expressions) | ✅ | +| `where()` expression callback (`and()`/`or()`) | ✅ | +| `innerJoin()` | ✅ | +| `outerLeftJoin()` / `outerRightJoin()` / `outerFullJoin()` | ✅ | +| `lateralJoin()` | ✅ | +| `orderBy()` (field name, expression, or options) | ✅ | +| `distinct()` | ✅ | +| `distinctOn()` | ✅ | +| `limit()` / `offset()` | ✅ | +| Subquery via `.as()` | ✅ | +| `build()` + `runtime.execute()` | ✅ | + +| Mutation method | PostgreSQL | +|---|---| +| `insert()` (single or multi-row) | ✅ | +| `insert().returning()` | ✅ | +| `update()` (values object or expression callback) | ✅ | +| `delete()` with `where()` and `returning()` | ✅ | +| `param()` (explicit codec on a raw value) | ✅ | + +| Grouped query method | PostgreSQL | +|---|---| +| `groupBy()` (field names or expression) | ✅ | +| `having()` with `count()` / `sum()` / `avg()` / `min()` / `max()` | ✅ | +| `orderBy()` / `limit()` on a grouped query | ✅ | + +`returning()` and `lateralJoin()` require the adapter to report the `sql.returning` and `sql.lateral` capabilities. `distinctOn()` requires the `postgres.distinctOn` capability. All three are available on Prisma Next's Postgres adapter. `COUNT()`, `SUM()`, and `AVG()` results decode as strings, not numbers, at this layer; call `Number(...)` before doing arithmetic on them. `MIN()` and `MAX()` decode as numbers. + +:::note[SQLite is next on deck] +Prisma Next ships first-class support for PostgreSQL and MongoDB today. SQLite is the next SQL target on deck, with MySQL to follow. +::: + +## Coming to this reference + +These surfaces are planned but not yet documented here: + +:::note +- The MongoDB pipeline builder (`db.query`) +- Raw query escape hatches +- Transaction and runtime APIs +- Middleware +::: + + + }> + 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. + + diff --git a/apps/docs/content/docs/(index)/next/reference/meta.json b/apps/docs/content/docs/(index)/next/reference/meta.json new file mode 100644 index 0000000000..d281dd7e68 --- /dev/null +++ b/apps/docs/content/docs/(index)/next/reference/meta.json @@ -0,0 +1,4 @@ +{ + "title": "Reference", + "pages": ["index", "orm-client", "sql-query-builder"] +} diff --git a/apps/docs/content/docs/(index)/next/reference/orm-client.mdx b/apps/docs/content/docs/(index)/next/reference/orm-client.mdx new file mode 100644 index 0000000000..7ecf972e99 --- /dev/null +++ b/apps/docs/content/docs/(index)/next/reference/orm-client.mdx @@ -0,0 +1,12 @@ +--- +title: ORM client reference +description: Reference for the Prisma Next ORM client's query, mutation, filter, and aggregate methods. +url: /next/reference/orm-client +metaTitle: Prisma Next ORM client reference +metaDescription: Reference for the Prisma Next ORM client's query, mutation, filter, and aggregate methods. +badge: early-access +--- + +Content landing in this PR. + +See the [Prisma Next API reference](/next/reference) for the availability matrix in the meantime. diff --git a/apps/docs/content/docs/(index)/next/reference/sql-query-builder.mdx b/apps/docs/content/docs/(index)/next/reference/sql-query-builder.mdx new file mode 100644 index 0000000000..6b5b85e750 --- /dev/null +++ b/apps/docs/content/docs/(index)/next/reference/sql-query-builder.mdx @@ -0,0 +1,12 @@ +--- +title: SQL query builder reference +description: Reference for the Prisma Next SQL query builder's select, mutation, and grouped query methods. +url: /next/reference/sql-query-builder +metaTitle: Prisma Next SQL query builder reference +metaDescription: Reference for the Prisma Next SQL query builder's select, mutation, and grouped query methods. +badge: early-access +--- + +Content landing in this PR. + +See the [Prisma Next API reference](/next/reference) for the availability matrix in the meantime. From bdda6d2bc664327e4fccb64bac8351a422872fcf Mon Sep 17 00:00:00 2001 From: Nurul Sundarani Date: Fri, 3 Jul 2026 20:32:29 +0530 Subject: [PATCH 02/15] docs(next): add ORM client reference Co-Authored-By: Claude Fable 5 --- .../(index)/next/reference/orm-client.mdx | 1832 ++++++++++++++++- 1 file changed, 1830 insertions(+), 2 deletions(-) diff --git a/apps/docs/content/docs/(index)/next/reference/orm-client.mdx b/apps/docs/content/docs/(index)/next/reference/orm-client.mdx index 7ecf972e99..ecde36c12b 100644 --- a/apps/docs/content/docs/(index)/next/reference/orm-client.mdx +++ b/apps/docs/content/docs/(index)/next/reference/orm-client.mdx @@ -7,6 +7,1834 @@ metaDescription: Reference for the Prisma Next ORM client's query, mutation, fil badge: early-access --- -Content landing in this PR. +The ORM client gives you model-level methods for reading and writing data across PostgreSQL and MongoDB. This page documents every method, its availability on each database, and the behavior that differs between the two. -See the [Prisma Next API reference](/next/reference) for the availability matrix in the meantime. +Availability is stated per method. When a method behaves the same on both databases, the Remarks say so; when it differs, exists on only one, or is type-checked but not enforced at runtime, the Remarks call that out. For a one-screen summary of availability across every method, see the [Prisma Next API reference](/next/reference). + +Every code example on this page is transcribed from a test that runs against a live database. PostgreSQL examples use a `User` / `Post` / `Tag` / `Task` schema; MongoDB examples use a `users` / `posts` schema; numeric aggregate examples use a `Customer` / `Order` schema. + +## Setting up the client + +You create an ORM client by connecting a runtime and calling `orm(...)`. The two databases have different entry points and different accessor conventions. + +### PostgreSQL + +Create a Postgres client, connect it to get a runtime, then pass that runtime to `orm(...)`. Model accessors hang off `.public` and use the contract's **root names**, which match your PSL model names (`orm.User`, `orm.Post`). + +```ts +import postgres from '@prisma-next/postgres/runtime'; +import { orm } from '@prisma-next/sql-orm-client'; +import type { Contract } from './contract.d'; +import contractJson from './contract.json' with { type: 'json' }; + +const client = postgres({ contractJson, url: process.env.DATABASE_URL }); +const runtime = await client.connect(); + +const db = orm({ runtime, context: client.context, collections: {} }).public; + +const users = await db.User.all(); +``` + +You can register a custom `Collection` subclass to attach domain methods to a model. See [Custom `Collection` subclass](#custom-collection-subclass). + +### MongoDB + +Create a Mongo client with `mongo(...)`; its `.orm` property is the client. MongoDB accessors use the contract's **root names too, but those are the lowercase plural collection names** from the contract's `roots` map, not the PSL model names (`orm.users`, `orm.posts`, not `orm.User`). + +```ts +import mongo from '@prisma-next/mongo/runtime'; +import type { Contract } from './contract.d'; +import contractJson from './contract.json' with { type: 'json' }; + +const client = mongo({ contractJson, url: process.env.MONGODB_URL, dbName: 'app' }); +const orm = client.orm; + +const users = await orm.users.all(); +``` + +:::note[Accessor naming differs by database] +PostgreSQL exposes models under their contract root names (matching PSL model names): `orm.User`. MongoDB exposes them under the registered collection root names (lowercase plural): `orm.users`. The examples below follow each database's convention. +::: + +## Query-building methods + +These methods narrow a query. They return a collection you can chain further methods on, and you resolve the query with a [read terminal](#read-terminals) such as `all()` or `first()`. MongoDB supports a smaller set than PostgreSQL; each method's Remarks state its availability. + +Import the standalone filter combinators used in some examples from `@prisma-next/sql-orm-client` (PostgreSQL) or `@prisma-next/mongo-query-ast/execution` (MongoDB). See [Filter conditions and operators](#filter-conditions-and-operators). + +### `where()` + +Restrict a query to rows matching a filter. + +#### Remarks + +- Available for PostgreSQL and MongoDB, but the accepted filter shapes differ. +- On PostgreSQL, `where()` accepts a callback with column-level operators (`u.email.eq(...)`) or a shorthand object of equality matches. +- On MongoDB, `where()` accepts a shorthand object of equality matches or a `MongoFieldFilter` expression. The plain-object form supports equality only, not nested operators; for comparisons use `MongoFieldFilter` (see [MongoFieldFilter](#mongofieldfilter)). +- Chaining multiple `where()` calls ANDs the filters together. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `filter` | Callback `(fields) => Expression`, shorthand object, or (MongoDB) a `MongoFieldFilter` | Yes | The condition rows must satisfy. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `Collection` | `db.User.where(...)` | A collection narrowed by the filter, chainable and awaitable through a terminal. | + +#### Examples + +##### Callback form with a column operator (PostgreSQL) + +```ts +const admins = await db.User.where((u) => u.kind.eq('admin')).all(); +``` + +##### Shorthand object form + + + + + +```ts +const bob = await db.User.where({ email: 'bob@example.com' }).first(); +``` + + + + + +```ts +const authors = await orm.users.where({ role: 'author' }).all(); +``` + + + + + +##### Chaining `where()` calls (ANDed) + +```ts +const carolUrgentPosts = await db.Post.where({ userId: carolId }) + .where((p) => p.priority.eq('urgent')) + .all(); +``` + +##### `MongoFieldFilter` expression (MongoDB) + +```ts +import { MongoFieldFilter } from '@prisma-next/mongo-query-ast/execution'; + +const alice = await orm.users.where(MongoFieldFilter.eq('email', 'alice@example.com')).first(); + +const recentPosts = await orm.posts + .where(MongoFieldFilter.gte('createdAt', new Date('2024-01-02T00:00:00.000Z'))) + .all(); +``` + +### `select()` + +Project a row down to a subset of scalar fields. + +#### Remarks + +- Available for PostgreSQL and MongoDB. +- On PostgreSQL, `select()` narrows the returned row shape at the type level: fields you didn't select are absent from the result type. +- On MongoDB, `select()` narrows the projected fields at runtime, but does not strip fields from the returned row type at compile time. The returned type is unchanged, even though the query projects fewer fields. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `...fields` | Field names (`string`) | Yes | One or more scalar field names to keep. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `Collection` | `db.User.select('id', 'email')` | A collection projected to the named fields. | + +#### Examples + +##### Project to a subset of fields + + + + + +```ts +const summaries = await db.User.select('id', 'email').orderBy((u) => u.email.asc()).all(); +// summaries[0] is { id, email } — no displayName +``` + + + + + +```ts +const summaries = await orm.users.select('name', 'email').all(); +``` + + + + + +### `include()` + +Eagerly load a relation onto the returned rows. + +#### Remarks + +- Available for PostgreSQL and MongoDB, with different capabilities. +- On PostgreSQL, `include(relationName, refineFn?)` loads to-one and to-many relations, and the optional refinement callback receives a nested `Collection` you can filter, order, take, and reduce (see [Refinements, reducers, and combine](#refinements-reducers-and-combine)). +- On MongoDB, `include(relationName)` adds a `$lookup` for a reference relation and takes only a relation name; there is no refinement-callback overload. +- On MongoDB, a looked-up sub-document's `_id` comes back as a raw driver `ObjectId`, not decoded to a hex string the way top-level `_id` fields are. Compare it with `String(...)`. +- On MongoDB, passing a second argument to `include()` does **not** throw. JavaScript does not arity-check, so the extra argument is silently ignored and a plain, unrefined `$lookup` runs. This differs from calling a genuinely absent method, which throws `TypeError`. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `relationName` | `string` | Yes | The relation to load. | +| `refineFn` | `(relation: Collection) => Collection \| reducer` | No | PostgreSQL only. Refines the loaded relation. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `Collection` | `db.User.include('posts')` | A collection whose rows carry the loaded relation. | + +#### Examples + +##### To-one relation (PostgreSQL) + +```ts +const posts = await db.Post.include('user').where({ id: postId }).all(); +// posts[0].user is the related User +``` + +##### To-many relation (PostgreSQL) + +```ts +const users = await db.User.include('posts').where({ id: aliceId }).all(); +// users[0].posts is an array of the user's posts +``` + +##### Reference relation (MongoDB) + +```ts +const posts = await orm.posts.include('author').where({ title: 'Hello world' }).all(); +// posts[0].author._id is a raw ObjectId — compare with String(posts[0].author._id) +``` + +### Refinements, reducers, and combine + +On PostgreSQL, `include()`'s refinement callback receives the nested relation as a full `Collection`. You can filter, order, and paginate it, reduce it to a scalar, or `combine()` several sub-views into one shape. + +#### Remarks + +- PostgreSQL only. MongoDB's `include()` has no refinement callback; the scalar reducers `count`/`sum`/`avg`/`min`/`max`/`combine` do not exist on the Mongo collection at all (calling them throws `TypeError`). +- The reducers are only callable **inside** an `include()` refinement callback. Called elsewhere on a PostgreSQL collection, they throw `Error` (the method exists but asserts refinement mode). +- `sum()` and `avg()` require a field with the numeric codec trait. Over a to-many relation with no rows, they resolve to `null`, not `0`. +- `min()` and `max()` are typed for numeric fields (`NumericFieldNames`). At the SQL level, Postgres's `MIN`/`MAX` also accept date/time columns, so `min('createdAt')` returns a real value at runtime even though it fails the type check. Treat the numeric-only typing as a type-level restriction, not a runtime one, for date and time columns. Do not rely on this: it requires bypassing the type system. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `filter` / `orderBy` / `take` / `skip` | Chained on the nested collection | No | Refine which related rows load. | +| `count()` | Reducer, no arguments | No | Reduces the relation to a row count. | +| `sum(field)` / `avg(field)` | Reducer over a numeric field | No | Reduces the relation to a numeric aggregate; `null` over an empty relation. | +| `min(field)` / `max(field)` | Reducer over a numeric field | No | Reduces the relation to a minimum/maximum. | +| `combine(shape)` | Object of sub-views and reducers | No | Projects several refinements into one shape. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| Refined relation, scalar, or combined shape | `include('posts', (p) => p.count())` | The relation is replaced by the refinement's result. | + +#### Examples + +##### Filter, order, and take within a relation (PostgreSQL) + +```ts +const users = await db.User.include('posts', (posts) => + posts + .where((p) => p.priority.eq('low')) + .orderBy((p) => p.createdAt.desc()) + .take(1), +) + .where({ id: aliceId }) + .all(); +``` + +##### Reduce a relation to a count (PostgreSQL) + +```ts +const users = await db.User.include('posts', (posts) => posts.count()) + .where({ id: aliceId }) + .all(); +// users[0].posts is the number 2 +``` + +##### `sum()` / `avg()` over a numeric relation (PostgreSQL) + +```ts +const customers = await db.Customer.include('orders', (orders) => orders.sum('amount')) + .where({ id: acmeId }) + .all(); +// customers[0].orders is 1500 + +const avgCustomers = await db.Customer.include('orders', (orders) => orders.avg('amount')) + .where({ id: acmeId }) + .all(); +// avgCustomers[0].orders is 300 +``` + +A customer with no orders reduces to `null`: + +```ts +const rows = await db.Customer.include('orders', (orders) => orders.sum('amount')) + .where({ id: emptyCustomerId }) + .all(); +// rows[0].orders is null +``` + +##### `combine()` multiple sub-views (PostgreSQL) + +```ts +const users = await db.User.include('posts', (posts) => + posts.combine({ + recent: posts.orderBy((p) => p.createdAt.desc()).take(1), + total: posts.count(), + }), +) + .where({ id: aliceId }) + .all(); +// users[0].posts.total is 2; users[0].posts.recent is a one-element array +``` + +:::warning[`min()` / `max()` on date columns are type-restricted] +`min()` and `max()` are typed as `>`. If your model has no field with the numeric codec trait, calling `min('createdAt')` is a compile error, even though Postgres's `MIN`/`MAX` accept the underlying `timestamptz` column at runtime. For date and time columns, treat this as a type-only restriction. `sum()` and `avg()` are stricter: Postgres itself rejects `SUM`/`AVG` over a date column, so those fail at both the type and SQL levels. +::: + +### `orderBy()` + +Sort the result set. + +#### Remarks + +- Available for PostgreSQL and MongoDB, with different argument shapes. +- On PostgreSQL, `orderBy()` takes a callback returning a per-column `.asc()`/`.desc()` directive, or an array of such callbacks for multiple sort keys. +- On MongoDB, `orderBy()` takes a plain object spec, `{ field: 1 }` for ascending or `{ field: -1 }` for descending. +- On PostgreSQL, native enum columns sort in the enum's **declaration order**, not alphabetically. A `Priority` enum declared `Low`, `High`, `Urgent` sorts in that order under `.asc()`. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `sort` | Callback `(fields) => f.field.asc() \| .desc()`, an array of such callbacks (PostgreSQL), or a `{ field: 1 \| -1 }` object (MongoDB) | Yes | The sort key(s) and direction(s). | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `Collection` | `db.Post.orderBy(...)` | A collection with an ordering applied. | + +#### Examples + +##### Ascending and descending + + + + + +```ts +const newestFirst = await db.Post.where({ userId: aliceId }) + .orderBy((p) => p.createdAt.desc()) + .all(); +``` + + + + + +```ts +const newestFirst = await orm.posts.orderBy({ createdAt: -1 }).all(); +``` + + + + + +##### Multiple sort keys (PostgreSQL) + +```ts +const byPriorityThenDate = await db.Post.orderBy([ + (p) => p.priority.asc(), + (p) => p.createdAt.asc(), +]).all(); +// priority sorts in enum declaration order (low, high, urgent), not alphabetically +``` + +### `take()` + +Limit the number of returned rows. + +#### Remarks + +- Available for PostgreSQL and MongoDB. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `count` | `number` | Yes | Maximum number of rows to return. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `Collection` | `db.Post.take(2)` | A collection limited to `count` rows. | + +#### Examples + +##### Limit the result set + + + + + +```ts +const firstTwo = await db.Post.orderBy((p) => p.createdAt.asc()).take(2).all(); +``` + + + + + +```ts +const firstOne = await orm.posts.orderBy({ createdAt: 1 }).take(1).all(); +``` + + + + + +### `skip()` + +Offset into the ordered result set. + +#### Remarks + +- Available for PostgreSQL and MongoDB. +- Combine with `orderBy()` and `take()` for pagination. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `count` | `number` | Yes | Number of rows to skip. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `Collection` | `db.Post.skip(2)` | A collection offset by `count` rows. | + +#### Examples + +##### Offset into the result set + + + + + +```ts +const page2 = await db.Post.orderBy((p) => p.createdAt.asc()).skip(2).take(2).all(); +``` + + + + + +```ts +const secondPost = await orm.posts.orderBy({ createdAt: 1 }).skip(1).take(1).all(); +``` + + + + + +### `cursor()` + +Resume pagination from a known position. + +#### Remarks + +- PostgreSQL only. MongoDB's collection has no `cursor()`; calling it throws `TypeError`. +- `cursor()` is typed to require a preceding `orderBy()`. This is a **type-only** guard: at runtime there is no such check. If you bypass the type system and call `cursor()` without an `orderBy()`, the cursor value is silently ignored and the full unfiltered result is returned. It does not throw. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `values` | Object of the `orderBy()` key(s) and their values | Yes | The position to resume after. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `Collection` | `db.Post.cursor({ createdAt })` | A collection resuming after the cursor position. | + +#### Examples + +##### Resume pagination (PostgreSQL) + +```ts +const page1 = await db.Post.orderBy((p) => p.createdAt.asc()).take(2).all(); +const last = page1[page1.length - 1]; + +const page2 = await db.Post.orderBy((p) => p.createdAt.asc()) + .cursor({ createdAt: last.createdAt }) + .take(2) + .all(); +``` + +### `distinct()` + +Emit `SELECT DISTINCT` on the given fields. + +#### Remarks + +- PostgreSQL only. MongoDB's collection has no `distinct()`; calling it throws `TypeError`. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `...fields` | Field names (`string`) | Yes | The fields to deduplicate on. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `Collection` | `db.Post.distinct('priority')` | A collection with duplicate rows removed on the named fields. | + +#### Examples + +##### Deduplicate on a field (PostgreSQL) + +```ts +const priorities = await db.Post.select('priority').distinct('priority').all(); +``` + +### `distinctOn()` + +Keep the first row per key according to `orderBy()`. + +#### Remarks + +- PostgreSQL only. MongoDB's collection has no `distinctOn()`; calling it throws `TypeError`. +- Requires a prior `orderBy()` to be meaningful. Like `cursor()`, this ordering requirement is a type-level guard, not a runtime check. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `...fields` | Field names (`string`) | Yes | The key field(s) to keep the first row of. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `Collection` | `db.Post.distinctOn('userId')` | A collection keeping one row per key. | + +#### Examples + +##### First row per key (PostgreSQL) + +```ts +const latestPerUser = await db.Post.orderBy([(p) => p.userId.asc(), (p) => p.createdAt.desc()]) + .distinctOn('userId') + .all(); +``` + +### `variant()` + +Narrow a polymorphic (PostgreSQL) or discriminated (MongoDB) model to one variant. + +#### Remarks + +- Available for PostgreSQL and MongoDB. +- On PostgreSQL, variants use multi-table inheritance (a base table plus a variant table). This affects some mutations; see [`createCount()`](#createcount) and [`upsert()`](#upsert). +- On MongoDB, variants are a single document shape discriminated by a field value, in one collection. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `variantName` | `string` | Yes | The variant to narrow to. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `Collection` (variant-narrowed) | `db.Task.variant('Bug')` | A collection scoped to the variant. | + +#### Examples + +##### Narrow to a variant + + + + + +```ts +const bugs = await db.Task.variant('Bug').all(); +``` + + + + + +```ts +const tutorials = await orm.posts.variant('Tutorial').all(); +``` + + + + + +## Read terminals + +Read terminals resolve a query. `all()` and `first()` are available on both databases; `aggregate()` and `groupBy()` are PostgreSQL only and are documented under [Grouped aggregates](#grouped-aggregates). + +### `all()` + +Resolve the query to every matching row. + +#### Remarks + +- Available for PostgreSQL and MongoDB. +- `all()` returns an [`AsyncIterableResult`](#asynciterableresult): you can `await` it to collect an array, or use `for await` to stream rows one at a time. +- Re-`await`ing (or calling `.toArray()` on) an already-buffered result is safe: the cached array is returned, with no re-query and no throw. +- **Switching consumption mode** on a consumed result throws `RUNTIME.ITERATOR_CONSUMED` (`already been consumed`). See [Single consumption and mode switching](#single-consumption-and-mode-switching). + +#### Options + +`all()` takes no arguments. + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `AsyncIterableResult` | `await db.User.all()` | Awaitable to `Row[]`, or iterable with `for await` for streaming. | + +#### Examples + +##### Await to collect an array + + + + + +```ts +const users = await db.User.all(); +``` + + + + + +```ts +const users = await orm.users.all(); +``` + + + + + +##### Stream rows one at a time + + + + + +```ts +for await (const user of db.User.orderBy((u) => u.email.asc()).all()) { + console.log(user.email); +} +``` + + + + + +```ts +for await (const post of orm.posts.orderBy({ createdAt: 1 }).all()) { + console.log(post.title); +} +``` + + + + + +### `first()` + +Resolve the query to the first matching row, or `null` if none matches. + +#### Remarks + +- Available for PostgreSQL and MongoDB. +- On PostgreSQL, `first()` accepts an inline filter: a shorthand object or a callback (`first((p) => p.priority.eq('urgent'))`). +- On MongoDB, `first()` has no filter argument; filter first with `where(...)`, then call `first()`. +- Returns `null` when no row matches. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `filter` | Shorthand object or callback (PostgreSQL only) | No | An inline filter applied before resolving. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `Row \| null` | `await db.User.first(...)` | The first matching row, or `null`. | + +#### Examples + +##### Match by an inline filter (PostgreSQL) + +```ts +const alice = await db.User.first({ email: 'alice@example.com' }); +const urgentPost = await db.Post.first((p) => p.priority.eq('urgent')); +``` + +##### Match with a prior `where()` (MongoDB) + +```ts +const bob = await orm.users.where({ name: 'Bob' }).first(); +``` + +##### No match returns `null` + + + + + +```ts +const nobody = await db.User.first({ email: 'nobody@example.com' }); +// null +``` + + + + + +```ts +const nobody = await orm.users.where({ email: 'nobody@example.com' }).first(); +// null +``` + + + + + +### Custom `Collection` subclass + +On PostgreSQL you can subclass `Collection` to attach domain methods, and register the subclass when you create the client. + +#### Remarks + +- PostgreSQL only. The MongoDB ORM has no equivalent collection-subclassing mechanism. + +#### Examples + +##### Register a subclass with a domain method (PostgreSQL) + +```ts +import { Collection, orm } from '@prisma-next/sql-orm-client'; + +class TaskCollection extends Collection { + bugs() { + return this.variant('Bug'); + } + features() { + return this.variant('Feature'); + } +} + +const db = orm({ + runtime, + context: client.context, + collections: { Task: TaskCollection }, +}).public; + +const bugs = await db.Task.bugs().all(); +const features = await db.Task.features().all(); +``` + +## Mutation terminals + +Mutations write to the database. `create`, `createAll`, `createCount`, `update`, `updateAll`, `updateCount`, `delete`, `deleteAll`, `deleteCount`, and `upsert` are available on both databases, with several behavioral differences called out per method. + +:::warning[`where()` enforcement differs sharply between databases] +Always call `where()` before `update`, `updateAll`, `updateCount`, `delete`, `deleteAll`, `deleteCount`, or `upsert`. On **MongoDB** all seven enforce this at runtime: calling them with no filter throws `Error: () requires a .where() filter`. On **PostgreSQL** only `update()` and `delete()` are typed to require a prior `where()`, and even there it is a **type-only** guard with no runtime check. If you bypass the type system, `update()` and `delete()` do not throw and do not mass-mutate; they narrow to a single row by identity (a `SELECT ... LIMIT 1` over the current, possibly empty, filters, then act on that one row). The `*All`/`*Count` variants on PostgreSQL compile with no `WHERE` clause and affect every row. Always call `where()` first. +::: + +### `create()` + +Insert a single row and return it. + +#### Remarks + +- Available for PostgreSQL and MongoDB. +- On MongoDB, `create()` returns the input data plus the server-assigned `_id`. It does not re-read the stored document, unlike PostgreSQL's `RETURNING`-backed `create()`. +- On PostgreSQL, `create()` supports nested `create()` on a child-owned relation and nested `connect()` on a parent-owned relation, all within one transaction. These nested mutators are type-checked but their runtime behavior on MongoDB is unverified on the current test contract. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `data` | Object of field values, optionally with relation mutators (`create`, `connect`) | Yes | The row to insert. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `Row` | `await db.Tag.create(...)` | The inserted row. | + +#### Examples + +##### Insert a single row + + + + + +```ts +const tag = await db.Tag.create({ label: 'typescript-2' }); +``` + + + + + +```ts +const user = await orm.users.create({ + name: 'Carol', + email: 'carol@example.com', + bio: null, + role: 'reader', + address: null, +}); +// user._id is the server-assigned id +``` + + + + + +##### Nested `create()` on a child-owned relation (PostgreSQL) + +```ts +const author = await db.User.create({ + id: '00000000-0000-0000-0000-000000000099', + email: 'dana@example.com', + displayName: 'Dana', + kind: 'user', + posts: (posts) => + posts.create([{ id: '10000000-0000-0000-0000-000000000099', title: 'Dana post one' }]), +}); +``` + +##### Nested `connect()` on a parent-owned relation (PostgreSQL) + +```ts +const post = await db.Post.create({ + id: '10000000-0000-0000-0000-000000000098', + title: 'Connected to Bob', + user: (user) => user.connect({ id: bobId }), +}); +``` + +### `createAll()` + +Insert multiple rows and return them. + +#### Remarks + +- Available for PostgreSQL and MongoDB. +- Returns an [`AsyncIterableResult`](#asynciterableresult): `await` for an array, or `for await` to stream inserted rows. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `data` | Array of row objects | Yes | The rows to insert. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `AsyncIterableResult` | `await db.Tag.createAll([...])` | Awaitable to `Row[]`, or streamable. | + +#### Examples + +##### Insert and collect + + + + + +```ts +const created = await db.Tag.createAll([{ label: 'alpha' }, { label: 'beta' }]); +``` + + + + + +```ts +const created = await orm.users.createAll([ + { name: 'Dana', email: 'dana@example.com', bio: null, role: 'author', address: null }, + { name: 'Eve', email: 'eve@example.com', bio: null, role: 'reader', address: null }, +]); +``` + + + + + +##### Stream inserted rows (PostgreSQL) + +```ts +for await (const tag of db.Tag.createAll([{ label: 'gamma' }, { label: 'delta' }])) { + console.log(tag.label); +} +``` + +### `createCount()` + +Insert rows without materializing them, returning the count. + +#### Remarks + +- Available for PostgreSQL and MongoDB. +- On PostgreSQL, `createCount()` is **not supported on a multi-table-inheritance variant**. The base and variant rows live in separate tables, so a single `RETURNING`-less `INSERT` can't populate both. It throws `Error: createCount() is not supported for MTI variant "" ... Use createAll() instead.` +- On MongoDB, `createCount()` on a discriminated variant works normally. Mongo variants are one document shape in one collection, so there is no split-table restriction. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `data` | Array of row objects | Yes | The rows to insert. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `number` | `await db.Tag.createCount([...])` | The count of inserted rows. | + +#### Examples + +##### Insert and count + + + + + +```ts +const inserted = await db.Tag.createCount([{ label: 'epsilon' }, { label: 'zeta' }]); +// 2 +``` + + + + + +```ts +const inserted = await orm.posts.variant('Tutorial').createCount([ + { + title: 'Variant createCount', + content: 'body', + authorId: aliceId, + createdAt: new Date('2024-02-01T00:00:00.000Z'), + difficulty: 'beginner', + duration: 10, + }, +]); +// 1 — no split-table restriction on Mongo variants +``` + + + + + +### `update()` + +Update the matched row and return it, or `null` if none matches. + +#### Remarks + +- Available for PostgreSQL and MongoDB. Requires a prior `where()` (see the warning at the top of this section). +- On MongoDB, `update()` accepts a data object **or** a field-operations callback (`(t) => [t.field.inc(5), t.other.set(...)]`). The callback groups operators into one `findOneAndUpdate`; see [Field update operations](#field-update-operations). +- On PostgreSQL, `update()` accepts a data object only. It has **no** field-operations callback overload: passing a function is silently a no-op (it resolves to `null`), not an error. A bare function has no enumerable own properties, so no column is targeted. +- On PostgreSQL, `update()` supports nested `connect()` (relink a parent-owned foreign key) and nested `disconnect()` (unlink a many-to-many row without deleting it). +- On MongoDB, `update()` is backed by `findOneAndUpdate` and is an atomic single-document update. +- Returns `null` when no row matches. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `data` | Data object, or (MongoDB) a field-operations callback | Yes | The changes to apply. On PostgreSQL, relation mutators (`connect`, `disconnect`) may be included. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `Row \| null` | `await db.User.where(...).update(...)` | The updated row, or `null` if none matched. | + +#### Examples + +##### Update with a data object + + + + + +```ts +const updated = await db.User.where({ id: bobId }).update({ displayName: 'Bob Updated' }); +``` + + + + + +```ts +const updated = await orm.users.where({ _id: bobId }).update({ bio: 'Now with a bio' }); +``` + + + + + +##### Field-operations callback (MongoDB) + +```ts +const updated = await orm.posts + .variant('Tutorial') + .where({ _id: tutorialId }) + .update((t) => [t.duration.inc(5), t.content.set('Updated content')]); +``` + +##### Nested `connect()` relinks a foreign key (PostgreSQL) + +```ts +const relinked = await db.Post.where({ id: postId }).update({ + user: (user) => user.connect({ id: carolId }), +}); +``` + +##### Nested `disconnect()` unlinks a many-to-many row (PostgreSQL) + +```ts +const updated = await db.Post.where({ id: postId }) + .select('id', 'title') + .include('tags', (tag) => tag.select('id', 'label').orderBy((t) => t.label.asc())) + .update({ + tags: (tag) => tag.disconnect([{ id: ormTagId }]), + }); +// only the junction row is removed; the Tag row itself still exists +``` + +### `updateAll()` + +Update every matching row and collect the results. + +#### Remarks + +- Available for PostgreSQL and MongoDB. Call `where()` first — MongoDB enforces this at runtime; PostgreSQL does not (see the section warning above: without a filter these affect every row). +- On PostgreSQL, `updateAll()` is a single `UPDATE ... RETURNING` statement, and is atomic. +- On MongoDB, `updateAll()` is **not atomic**. It (1) reads the matching `_id`s, (2) runs an update against the original filter, then (3) re-reads by those captured `_id`s. A concurrent write between these steps could change which documents match or their values; the result reflects the `_id` set from step 1, not one atomic snapshot. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `data` | Data object, or (MongoDB) a field-operations callback | Yes | The changes to apply to every matched row. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `AsyncIterableResult` | `await db.Post.where(...).updateAll(...)` | The updated rows. | + +#### Examples + +##### Update all matching rows + + + + + +```ts +const updated = await db.Post.where({ userId: aliceId }).updateAll({ priority: 'urgent' }); +``` + + + + + +```ts +const updated = await orm.users.where({ role: 'author' }).updateAll({ role: 'admin' }); +// non-atomic: ids are captured, updated, then re-read +``` + + + + + +### `updateCount()` + +Update every matching row and return the count. + +#### Remarks + +- Available for PostgreSQL and MongoDB. Call `where()` first — MongoDB enforces this at runtime; PostgreSQL does not (see the section warning above: without a filter these affect every row). +- On MongoDB, `updateCount()` accepts a data object or a field-operations callback. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `data` | Data object, or (MongoDB) a field-operations callback | Yes | The changes to apply to every matched row. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `number` | `await db.Post.where(...).updateCount(...)` | The count of updated rows. | + +#### Examples + +##### Update and count + + + + + +```ts +const count = await db.Post.where({ userId: carolId }).updateCount({ priority: 'low' }); +``` + + + + + +```ts +const count = await orm.users.where({ role: 'author' }).updateCount({ role: 'admin' }); +``` + + + + + +##### Field-operations callback (MongoDB) + +```ts +const count = await orm.posts + .variant('Tutorial') + .where({ _id: tutorialId }) + .updateCount((t) => [t.duration.mul(2)]); +``` + +### `delete()` + +Remove the matched row and return it, or `null` if none matches. + +#### Remarks + +- Available for PostgreSQL and MongoDB. Requires a prior `where()` (see the section warning above). +- On MongoDB, `delete()` is backed by `findOneAndDelete`. +- Returns `null` when no row matches. + +#### Options + +`delete()` takes no arguments; filter with `where()` first. + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `Row \| null` | `await db.Tag.where(...).delete()` | The deleted row, or `null` if none matched. | + +#### Examples + +##### Delete a row + + + + + +```ts +const created = await db.Tag.create({ label: 'throwaway' }); +const deleted = await db.Tag.where({ id: created.id }).delete(); +``` + + + + + +```ts +const deleted = await orm.users.where({ _id: userId }).delete(); +``` + + + + + +### `deleteAll()` + +Remove every matching row and collect the results. + +#### Remarks + +- Available for PostgreSQL and MongoDB. Call `where()` first — MongoDB enforces this at runtime; PostgreSQL does not (see the section warning above: without a filter these affect every row). + +#### Options + +`deleteAll()` takes no arguments; filter with `where()` first. + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `AsyncIterableResult` | `await db.Post.where(...).deleteAll()` | The deleted rows. | + +#### Examples + +##### Delete all matching rows + + + + + +```ts +const deleted = await db.Post.where({ userId: carolId }).deleteAll(); +``` + + + + + +```ts +const deleted = await orm.users.where({ role: 'reader' }).deleteAll(); +``` + + + + + +### `deleteCount()` + +Remove every matching row and return the count. + +#### Remarks + +- Available for PostgreSQL and MongoDB. Call `where()` first — MongoDB enforces this at runtime; PostgreSQL does not (see the section warning above: without a filter these affect every row). +- Returns `0` when no row matches. + +#### Options + +`deleteCount()` takes no arguments; filter with `where()` first. + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `number` | `await db.Post.where(...).deleteCount()` | The count of deleted rows. | + +#### Examples + +##### Delete and count + + + + + +```ts +const count = await db.Post.where({ userId: carolId }).deleteCount(); +``` + + + + + +```ts +const count = await orm.users.where({ role: 'reader' }).deleteCount(); +``` + + + + + +### `upsert()` + +Insert a row if none matches, otherwise update the existing row. + +#### Remarks + +- Available for PostgreSQL and MongoDB. Requires a prior `where()` on MongoDB (see the section warning). +- On PostgreSQL, `upsert()` uses `conflictOn` to name the unique target. The `update` side is a plain data object. +- On MongoDB, the `update` side may be a data object **or** a field-operations callback. On insert, `update` fields apply via `$set` and the remaining create-only fields via `$setOnInsert`, so `update` values win over `create` values on any overlapping field. +- On PostgreSQL, `upsert()` is **not supported on a multi-table-inheritance variant** (same restriction as [`createCount()`](#createcount)): it throws `Error: upsert() is not supported for MTI variant "" ...`. +- On MongoDB, the returned `_id` on the update path comes back as a raw driver `ObjectId`, not a decoded hex string. Compare it with `String(...)`. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `create` | Data object | Yes | The row to insert if none matches. | +| `update` | Data object, or (MongoDB) a field-operations callback | Yes | The changes to apply if a row matches. | +| `conflictOn` | Object naming the unique target (PostgreSQL) | PostgreSQL | The conflict target that decides insert vs. update. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `Row` | `await db.Tag.upsert(...)` | The inserted or updated row. | + +#### Examples + +##### Insert or update (PostgreSQL) + +```ts +// Insert path: no existing row has label 'brand-new', so the create side wins. +const inserted = await db.Tag.upsert({ + create: { id: '30000000-0000-0000-0000-000000000099', label: 'brand-new' }, + update: { label: 'brand-new-updated' }, + conflictOn: { label: 'brand-new' }, +}); + +// Update path: 'typescript' already exists, so the update runs against it. +const updated = await db.Tag.upsert({ + create: { id: '30000000-0000-0000-0000-000000000098', label: 'typescript' }, + update: { label: 'typescript-renamed' }, + conflictOn: { label: 'typescript' }, +}); +``` + +##### Insert or update (MongoDB) + +```ts +const user = await orm.users.where({ email: 'newperson@example.com' }).upsert({ + create: { + name: 'New Person', + email: 'newperson@example.com', + bio: null, + role: 'reader', + address: null, + }, + update: { bio: 'set on upsert' }, +}); +// on insert, update fields win over create fields on overlap +``` + +##### Field-operations callback on the update side (MongoDB) + +```ts +const post = await orm.posts + .variant('Tutorial') + .where({ _id: tutorialId }) + .upsert({ + create: { + title: 'Should not be used', + content: 'unused', + authorId: bobId, + createdAt: new Date('2024-01-01T00:00:00.000Z'), + difficulty: 'beginner', + duration: 0, + }, + update: (t) => [t.duration.inc(1)], + }); +``` + +## Grouped aggregates + +PostgreSQL supports aggregation over a result set, both flat (`aggregate()`) and grouped (`groupBy().aggregate()`), with `having()` to filter groups. MongoDB has no equivalent: `aggregate()` and `groupBy()` do not exist on the Mongo collection and calling them throws `TypeError`. The examples below use a `Customer` / `Order` schema where `Order.amount` is a numeric column. + +### `aggregate()` + +Compute aggregates over the current result set. + +#### Remarks + +- PostgreSQL only. Calling `aggregate()` on a Mongo collection throws `TypeError`. +- `count()` needs no field argument and always resolves to a number (`0` over an empty set). +- `sum()` and `avg()` resolve to `null`, not `0`, over an empty result set. This is standard SQL: aggregating zero rows with `SUM`/`AVG` yields `NULL`. Their TypeScript return type is `number | null` for this reason. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `selector` | Callback `(agg) => ({ alias: agg.fn(...) })` | Yes | The aggregates to compute, keyed by output alias. | + +The selector's `agg` exposes `count()`, `sum(field)`, `avg(field)`, `min(field)`, `max(field)`. + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| Object of aggregate results | `{ total: 10 }` | One object with the requested aliases. `sum`/`avg` are `number \| null`. | + +#### Examples + +##### Count (PostgreSQL) + +```ts +const stats = await db.Order.aggregate((agg) => ({ total: agg.count() })); +// { total: 10 } +``` + +##### Sum and average (PostgreSQL) + +```ts +const stats = await db.Order.where({ customerId: acmeId }).aggregate((agg) => ({ + totalAmount: agg.sum('amount'), + avgAmount: agg.avg('amount'), +})); +// { totalAmount: 1500, avgAmount: 300 } +``` + +##### Min and max (PostgreSQL) + +```ts +const stats = await db.Order.aggregate((agg) => ({ + cheapest: agg.min('amount'), + priciest: agg.max('amount'), +})); +// { cheapest: 10, priciest: 500 } +``` + +##### `null` over an empty set (PostgreSQL) + +```ts +const stats = await db.Order.where((o) => o.amount.gt(999_999)).aggregate((agg) => ({ + total: agg.sum('amount'), + average: agg.avg('amount'), + count: agg.count(), +})); +// { total: null, average: null, count: 0 } +``` + +### `groupBy()` + +Group rows by one or more fields, then aggregate per group. + +#### Remarks + +- PostgreSQL only. Calling `groupBy()` on a Mongo collection throws `TypeError`. +- `groupBy(...fields)` returns a `GroupedCollection`. Call `.aggregate(...)` on it to produce one row per distinct group key, carrying both the key field(s) and the aggregate aliases. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `...fields` | Field names (`string`) | Yes | The grouping key(s). | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `GroupedCollection` | `db.Order.groupBy('customerId')` | A grouped collection, resolved via `aggregate()`. | + +#### Examples + +##### Group and aggregate (PostgreSQL) + +```ts +const perCustomer = await db.Order.groupBy('customerId').aggregate((agg) => ({ + orderCount: agg.count(), + totalAmount: agg.sum('amount'), +})); +// one row per customer, each { customerId, orderCount, totalAmount } +``` + +### `having()` + +Filter groups by an aggregate comparison. + +#### Remarks + +- PostgreSQL only. Chained on a `GroupedCollection` between `groupBy()` and `aggregate()`. +- The `having()` callback exposes `count()`, `sum(field)`, `avg(field)`, `min(field)`, `max(field)`, each returning comparison methods (`eq`, `neq`, `gt`, `lt`, `gte`, `lte`). It compiles to a SQL `HAVING` clause. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `predicate` | Callback `(h) => h.fn(field).cmp(value)` | Yes | The group-level condition. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `GroupedCollection` | `db.Order.groupBy('customerId').having(...)` | A grouped collection filtered by the aggregate predicate. | + +#### Examples + +##### Filter groups by a sum (PostgreSQL) + +```ts +const bigSpenders = await db.Order.groupBy('customerId') + .having((h) => h.sum('amount').gt(1000)) + .aggregate((agg) => ({ totalAmount: agg.sum('amount') })); +``` + +##### Filter groups by row count (PostgreSQL) + +```ts +const groupsWithAtLeastFive = await db.Order.groupBy('customerId') + .having((h) => h.count().gte(5)) + .aggregate((agg) => ({ orderCount: agg.count() })); +``` + +## Filter conditions and operators + +Filters build the predicate inside `where()` (and relation refinements). PostgreSQL and MongoDB use different filter surfaces. + +- **PostgreSQL** uses column-level comparison methods (`u.email.eq(...)`) inside a callback, plus standalone combinator functions imported from `@prisma-next/sql-orm-client`. +- **MongoDB** uses static factories on `MongoFieldFilter` imported from `@prisma-next/mongo-query-ast/execution`, plus `.and()`/`.not()` instance methods and the standalone `MongoOrExpr.of(...)`. + +### Scalar comparisons (PostgreSQL) + +Column-level comparison methods on a field accessor. + +#### Remarks + +- PostgreSQL. Each method is gated by a codec trait: `eq`/`neq`/`in`/`notIn` need equality; `gt`/`lt`/`gte`/`lte` need ordering; `like` needs the textual trait; `isNull`/`isNotNull` are available on every field. +- `like()` matches a SQL pattern case-sensitively. +- `ilike()` matches case-insensitively. It is a Postgres-adapter-registered operation attached to any textual-trait field, not a core comparison method. + +#### Options + +| Method | Type | Description | +|---|---|---| +| `eq(value)` / `neq(value)` | Exact value | Equality / inequality. | +| `gt(value)` / `lt(value)` / `gte(value)` / `lte(value)` | Ordered value | Ordered comparisons. | +| `like(pattern)` / `ilike(pattern)` | SQL `LIKE` pattern | Case-sensitive / case-insensitive pattern match. | +| `in(values)` / `notIn(values)` | Array of values | Membership / exclusion. | +| `isNull()` / `isNotNull()` | No argument | NULL checks. | + +#### Examples + +##### Equality and inequality (PostgreSQL) + +```ts +const alice = await db.User.where((u) => u.email.eq('alice@example.com')).first(); +const notAlice = await db.User.where((u) => u.email.neq('alice@example.com')).all(); +``` + +##### Ordered comparisons (PostgreSQL) + +```ts +const after = await db.Post.where((p) => p.createdAt.gt(new Date('2024-01-02T10:00:00.000Z'))).all(); +const inclusive = await db.Post.where((p) => p.createdAt.gte(new Date('2024-01-02T10:00:00.000Z'))).all(); +``` + +##### Pattern matching (PostgreSQL) + +```ts +const matches = await db.User.where((u) => u.email.like('%@example.com')).all(); // case-sensitive +const caseInsensitive = await db.User.where((u) => u.email.ilike('%@EXAMPLE.COM')).all(); +``` + +##### Membership and NULL checks (PostgreSQL) + +```ts +const lowOrHigh = await db.Post.where((p) => p.priority.in(['low', 'high'])).all(); +const notLowOrHigh = await db.Post.where((p) => p.priority.notIn(['low', 'high'])).all(); +const withoutEmbedding = await db.Post.where((p) => p.embedding.isNull()).all(); +``` + +### Combinators + +Combine or negate predicates. + +#### Remarks + +- On PostgreSQL, `and`, `or`, `not`, and `all` are **standalone functions** imported from `@prisma-next/sql-orm-client`. They build AST nodes directly and are independent of any model accessor. +- On MongoDB, `.and(other)` and `.not()` are **instance methods** on any `MongoFieldFilter`. There is **no `.or()` instance method** and no `$nor` support. To OR filters, construct `MongoOrExpr.of([...])`. +- On PostgreSQL, `all()` takes no arguments and returns a constant-true predicate, useful wherever a predicate is structurally required. + +#### Examples + +##### `and` / `or` / `not` (PostgreSQL) + +```ts +import { and, or, not } from '@prisma-next/sql-orm-client'; + +const both = await db.Post.where((p) => and(p.priority.eq('low'), p.userId.eq(carolId))).all(); +const either = await db.Post.where((p) => or(p.priority.eq('urgent'), p.priority.eq('high'))).all(); +const negated = await db.Post.where((p) => not(p.priority.eq('low'))).all(); +``` + +##### `and` / `or` / `not` (MongoDB) + +```ts +import { MongoFieldFilter, MongoOrExpr } from '@prisma-next/mongo-query-ast/execution'; + +const both = await orm.users + .where(MongoFieldFilter.eq('role', 'author').and(MongoFieldFilter.eq('name', 'Alice'))) + .all(); +const either = await orm.users + .where(MongoOrExpr.of([MongoFieldFilter.eq('name', 'Alice'), MongoFieldFilter.eq('name', 'Bob')])) + .all(); +const negated = await orm.users.where(MongoFieldFilter.eq('name', 'Alice').not()).all(); +``` + +:::note[No `.or()` instance method or `$nor` on MongoDB] +`MongoFieldFilter` expressions have `.and(...)` and `.not()` instance methods, but no `.or(...)`. Accessing `.or` is `undefined` and calling it throws `TypeError`. Use `MongoOrExpr.of([...])` for disjunction. There is no `$nor` combinator anywhere in the library. +::: + +### Relation filters (PostgreSQL) + +Filter parents by their related rows. + +#### Remarks + +- PostgreSQL. `some()`, `every()`, and `none()` are methods on a to-many relation accessor and compile to correlated `EXISTS` / `NOT EXISTS` subqueries. +- `some()` with no predicate matches parents with at least one related row. +- `every()` is vacuously true for a parent with zero related rows. +- A to-one relation is filtered from the child side with the same `some(...)` shape (the compiled SQL is a correlated subquery regardless of cardinality). + +#### Examples + +##### `some` / `every` / `none` (PostgreSQL) + +```ts +const withUrgentPost = await db.User.where((u) => u.posts.some((p) => p.priority.eq('urgent'))).all(); +const withAnyTag = await db.Tag.where((t) => t.posts.some()).all(); +const allLowPriority = await db.User.where((u) => u.posts.every((p) => p.priority.eq('low'))).all(); +const noUrgentPost = await db.User.where((u) => u.posts.none((p) => p.priority.eq('urgent'))).all(); +``` + +##### To-one relation predicate (PostgreSQL) + +```ts +const postsByAlice = await db.Post.where((p) => p.user.some({ email: 'alice@example.com' })).all(); +``` + +### Shorthand object filter + +A plain object of equality matches, ANDed together. + +#### Remarks + +- Available for PostgreSQL and MongoDB. +- Each key must support equality. Multiple keys are combined with implicit AND. + +#### Examples + +##### Shorthand object + + + + + +```ts +const row = await db.Post.where({ userId: aliceId, priority: 'high' }).first(); +``` + + + + + +```ts +const alice = await orm.users.where({ name: 'Alice', role: 'author' }).first(); +``` + + + + + +### `MongoFieldFilter` + +Static factories that build MongoDB filter expressions. + +#### Remarks + +- MongoDB. Imported from `@prisma-next/mongo-query-ast/execution`. +- Exactly ten factories exist: `of`, `eq`, `neq`, `gt`, `lt`, `gte`, `lte`, `in`, `nin`, `isNull`, `isNotNull`. The inequality factory is `neq`, **not** `ne` (`MongoFieldFilter.ne` is `undefined`). +- `isNull` / `isNotNull` are sugar for equals-null / not-equals-null, not a `$exists` check. +- The factories `exists`, `regex`, `elemMatch`, `all`, and `size` do **not** exist on `MongoFieldFilter`. There is a separate `MongoExistsExpr` class for `$exists`, but no regex, `elemMatch`, `$all`, or `$size` support anywhere in the library. + +#### Options + +| Method | Type | Description | +|---|---|---| +| `eq(field, value)` / `neq(field, value)` | Field + value | Equality / inequality. | +| `gt` / `lt` / `gte` / `lte` `(field, value)` | Field + ordered value | Ordered comparisons. | +| `in(field, values)` / `nin(field, values)` | Field + array | Membership / exclusion. | +| `isNull(field)` / `isNotNull(field)` | Field | Null / not-null (equals-null semantics). | + +#### Examples + +##### Comparison factories (MongoDB) + +```ts +import { MongoFieldFilter } from '@prisma-next/mongo-query-ast/execution'; + +const alice = await orm.users.where(MongoFieldFilter.eq('name', 'Alice')).first(); +const notAlice = await orm.users.where(MongoFieldFilter.neq('name', 'Alice')).all(); +const strictlyAfter = await orm.posts + .where(MongoFieldFilter.gt('createdAt', new Date('2024-01-01T10:00:00.000Z'))) + .all(); +``` + +##### Membership and null checks (MongoDB) + +```ts +const articlesOrTutorials = await orm.posts + .where(MongoFieldFilter.in('kind', ['article', 'tutorial'])) + .all(); +const notArticles = await orm.posts.where(MongoFieldFilter.nin('kind', ['article'])).all(); +const noBio = await orm.users.where(MongoFieldFilter.isNull('bio')).all(); +const hasBio = await orm.users.where(MongoFieldFilter.isNotNull('bio')).all(); +``` + +### Dot-notation into an embedded object (MongoDB) + +Filter into a field of an embedded value object using a dot path. + +#### Remarks + +- MongoDB. Runtime behavior is verified; the type surface does not declare embedded paths, so the object-shorthand form needs a cast. +- `MongoWhereFilter` is typed against the model's own top-level field keys, so `{ 'address.city': ... }` is a compile error without a cast. At runtime there is no such restriction: the object key is used verbatim as the Mongo field path. +- For a type-clean alternative, use `MongoFieldFilter.eq('address.city', ...)`, which takes a bare `string` and needs no cast. + +#### Examples + +##### Dot-notation path (MongoDB) + +```ts +import { MongoFieldFilter } from '@prisma-next/mongo-query-ast/execution'; + +// Type-clean form: MongoFieldFilter takes a bare string path. +const usersInSf = await orm.users.where(MongoFieldFilter.eq('address.city', 'San Francisco')).all(); +``` + +The object-shorthand form works at runtime but requires a cast, because the embedded path is not in the model's declared field keys: + +```ts +const usersInSf = await orm.users + .where({ 'address.city': 'San Francisco' } as unknown as Record) + .all(); +``` + +## Field update operations + +MongoDB field operations are accessed through a field accessor inside `update()`, `updateCount()`, and `upsert()` callbacks (for example `t.duration.inc(5)`). Each operation targets a field and produces a Mongo update operator. PostgreSQL's `update()` has no equivalent callback form (see [`update()`](#update)). + +### Available operations (MongoDB) + +#### Remarks + +- MongoDB only. +- `set()`, `unset()`, `inc()`, and `mul()` are available and verified. + - `set(value)` assigns a field. + - `unset()` removes a field. + - `inc(n)` increments a numeric field. Applied to a missing field, `$inc` initializes it to the increment. + - `mul(n)` multiplies a numeric field. Applied to a missing field, MongoDB sets it to `0` regardless of the multiplier. +- The field accessor is typed against the collection's **base** model, even after `variant(...)`. Accessing a variant-only field (such as `Tutorial.duration`) inside a callback is a compile error, though it works correctly at runtime because the accessor is a `Proxy` that resolves any field name. + +#### Examples + +##### `set()` (MongoDB) + +```ts +const updated = await orm.users + .where({ _id: bobId }) + .update((u) => [u.bio.set('Set via field op')]); +``` + +##### `inc()` and `mul()` (MongoDB) + +```ts +const incremented = await orm.posts + .variant('Tutorial') + .where({ _id: tutorialId }) + .update((t) => [t.duration.inc(10)]); + +const multiplied = await orm.posts + .variant('Tutorial') + .where({ _id: tutorialId }) + .update((t) => [t.duration.mul(3)]); +``` + +##### `unset()` (MongoDB) + +```ts +const updated = await orm.users.where({ _id: aliceId }).update((u) => [u.bio.unset()]); +``` + +### Operations that do not exist (MongoDB) + +#### Remarks + +- MongoDB. These are library gaps, not contract limitations. `min()`, `max()`, `rename()`, and `currentDate()` have **no accessor method at all**. The library never generates `$min`, `$max`, `$rename`, or `$currentDate`. + +:::warning[No `min`, `max`, `rename`, or `currentDate` field operations] +The field accessor implements `set`, `unset`, `inc`, `mul`, `push`, `pull`, `addToSet`, and `pop` only. `min`, `max`, `rename`, and `currentDate` are absent from the API. Because a field accessor is a `Proxy`, `u.bio.rename` resolves to `undefined` and only throws `TypeError` when you attempt to call it. Do not use these operations; they are not supported. +::: + +### Array operations (MongoDB, unverified) + +#### Remarks + +- MongoDB. `push()`, `pull()`, `addToSet()`, and `pop()` exist on the field accessor and compile, but they are **unverified** on the current test contract, which has no array (`many: true`) field to target. Applied to a non-array field, MongoDB rejects the write with a server-level error (`The field '...' must be an array but is of type ...`), not a library error. +- Treat these as available but exercise them against a real array field in your own schema before relying on them. + +## Result types + +### `AsyncIterableResult` + +The read terminals `all()` / `createAll()` / `updateAll()` / `deleteAll()` return an `AsyncIterableResult`, imported (if you need the type) from `@prisma-next/framework-components/runtime`. It has a dual interface: + +- **Buffered**: `await` the result (or call `.toArray()`) to collect every row into an array. +- **Streaming**: use `for await ... of` to iterate rows one at a time. + +#### Single consumption and mode switching + +A result is consumed once per mode: + +- Re-`await`ing (or calling `.toArray()` on) an already-buffered result is **safe**: it returns the cached array, with no re-query and no throw. +- **Switching modes** on a consumed result throws `RUNTIME.ITERATOR_CONSUMED` (message: `already been consumed`). Awaiting a result and then iterating it with `for await` (or the reverse) is the failure mode. + +```ts +const result = db.User.all(); +const first = await result; +const again = await result.toArray(); // safe: same cached array + +// but switching mode throws: +for await (const user of result) { + // throws RUNTIME.ITERATOR_CONSUMED +} +``` + +This behavior is shared by the PostgreSQL and MongoDB implementations; the error message and rule are identical. + +### Aggregate result shapes + +- `aggregate((agg) => ({ ... }))` resolves to a **single object** keyed by your aliases: `{ total: 10 }`. +- `groupBy(...).aggregate((agg) => ({ ... }))` resolves to an **array**, one object per group, carrying both the group key field(s) and the aggregate aliases: `[{ customerId, orderCount, totalAmount }, ...]`. +- `count()` is always a `number` (`0` over an empty set). `sum()` and `avg()` are `number | null`: they resolve to `null`, not `0`, over an empty result set. From 5b6c759a266aca5882820711e6bf69a1a72bbbbb Mon Sep 17 00:00:00 2001 From: Nurul Sundarani Date: Fri, 3 Jul 2026 20:45:11 +0530 Subject: [PATCH 03/15] docs(next): add SQL query builder reference Co-Authored-By: Claude Fable 5 --- .../next/reference/sql-query-builder.mdx | 842 +++++++++++++++++- 1 file changed, 840 insertions(+), 2 deletions(-) diff --git a/apps/docs/content/docs/(index)/next/reference/sql-query-builder.mdx b/apps/docs/content/docs/(index)/next/reference/sql-query-builder.mdx index 6b5b85e750..c404b89cbb 100644 --- a/apps/docs/content/docs/(index)/next/reference/sql-query-builder.mdx +++ b/apps/docs/content/docs/(index)/next/reference/sql-query-builder.mdx @@ -7,6 +7,844 @@ metaDescription: Reference for the Prisma Next SQL query builder's select, mutat badge: early-access --- -Content landing in this PR. +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](/next/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. -See the [Prisma Next API reference](/next/reference) for the availability matrix in the meantime. +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 one-screen summary of availability, see the [Prisma Next API reference](/next/reference). + +Every code example on this page is transcribed from a test that runs against a live PostgreSQL database. Select and mutation examples use a `user` / `post` / `post_tag` / `tag` schema; grouped-query examples use a `customer` / `order` schema whose `order` table has numeric `amount` and `quantity` columns. + +:::note[This is the SQL-family builder] +The SQL query builder targets SQL databases. PostgreSQL is supported today, with SQLite next on deck. MongoDB has no SQL builder: use the [ORM client](/next/reference/orm-client) for MongoDB queries today, and the MongoDB pipeline builder (planned) for aggregation pipelines. +::: + +## Entry points + +You build queries from a `sql` root that carries your contract's execution context. Table accessors hang off `.public` and use the contract's **mapped table names** (snake_case), so a `User` model mapped to `users` is reached as `sql.user`, and a `Post`/`Tag` junction mapped to `post_tag` is reached as `sql.post_tag`. + +### The `sql()` root + +Create the root by calling `sql(...)` with your execution `context` and a `rawCodecInferer`. The `context` comes from your database client; the `rawCodecInferer` supplies a fallback codec for raw expressions that have no column to infer one from. + +```ts +import { sql } from '@prisma-next/sql-builder/runtime'; + +const db = sql({ context, rawCodecInferer: { inferCodec: () => 'pg/text' } }).public; + +const users = await runtime.execute(db.user.select('id', 'email').build()); +``` + +Most projects don't call `sql(...)` inline at every call site. Instead they build the root once and re-expose it on their own database wrapper, so application code reaches tables through a convenience property such as `db.sql.public.user`. Both forms are the same entry style: `db.sql` just holds the result of an internal `sql({ ... }).public` call. This page uses the direct `sql({ ... }).public` form and refers to the root as `sql`. + +### Table access + +Every table on `.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.execute(...)`. + +```ts +sql.user.select('id', 'email'); // start a SELECT +sql.tag.insert([{ id, label: 'typescript' }]); // start an INSERT +``` + +### Aliasing a query with `.as()` + +A completed `SELECT` query can be used as a subquery source by calling `.as(alias)` on it. The aliased query becomes a join source you can pass to `innerJoin()`, `outerLeftJoin()`, and the other join methods. See [Subquery via `.as()`](#subquery-via-as). + +```ts +const highPriorityPosts = sql.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. +::: + +## SELECT queries + +These methods build and refine a `SELECT`. They chain, and you resolve the query with [`build()`](#build) followed by `runtime.execute(...)`. + +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)). + +### `select()` + +Project a row down to a chosen set of columns or computed expressions. + +#### Options + +`select()` has three forms: + +| 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`. | +| 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). + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `SelectQuery` | `sql.user.select('id', 'email')` | A query projected to the named columns or expressions, chainable and buildable. | + +#### Examples + +##### Project a subset of columns + +```ts +const plan = sql.user.select('id', 'email').build(); +const rows = await runtime.execute(plan); +// rows[0] is { id, email } — no displayName +``` + +##### Add an aliased computed column + +```ts +const plan = sql.user + .select('id', 'displayName') + .select('emailLength', (f, fns) => fns.raw`LENGTH(${f.email})`.returns('pg/int4@1')) + .where((f, fns) => fns.eq(f.id, aliceId)) + .build(); +const rows = await runtime.execute(plan); +``` + +##### Project multiple computed columns at once + +```ts +const plan = sql.user + .select((f, fns) => ({ + id: f.id, + upperEmail: fns.raw`UPPER(${f.email})`.returns('pg/text@1'), + emailLength: fns.raw`LENGTH(${f.email})`.returns('pg/int4@1'), + })) + .where((f, fns) => fns.eq(f.id, aliceId)) + .build(); +const rows = await runtime.execute(plan); +// rows === [{ id: aliceId, upperEmail: 'ALICE@EXAMPLE.COM', emailLength: 17 }] +``` + +### `where()` + +Restrict a query to rows matching an expression. + +#### Options + +| 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. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `SelectQuery` | `sql.post.where(...)` | A query narrowed by the predicate. | + +#### Examples + +##### Combine comparisons with `and()` and `or()` + +```ts +const plan = sql.post + .select('id', 'title', 'priority') + .where((f, fns) => + fns.or( + fns.and(fns.eq(f.userId, aliceId), fns.eq(f.priority, 'high')), + fns.eq(f.userId, carolId), + ), + ) + .build(); +const rows = await runtime.execute(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. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `other` | A table (`sql.`) or an aliased subquery ([`.as()`](#aliasing-a-query-with-as)) | Yes | The table or subquery to join. | +| `on` | `(f, fns) => Expression` | Yes | The join condition. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `SelectQuery` | `sql.post.innerJoin(sql.user, ...)` | A query over the joined tables, with both tables' columns in scope under their namespaces. | + +#### Examples + +##### Join posts to their authors + +```ts +const plan = sql.post + .innerJoin(sql.user, (f, fns) => fns.eq(f.post.userId, f.user.id)) + .select((f) => ({ postId: f.post.id, authorEmail: f.user.email })) + .where((f, fns) => fns.eq(f.post.id, helloWorldId)) + .build(); +const rows = await runtime.execute(plan); +// rows === [{ postId: helloWorldId, authorEmail: 'alice@example.com' }] +``` + +### `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. + +#### Examples + +##### Left join keeps rows with no match + +```ts +const plan = sql.post + .outerLeftJoin(sql.post_tag, (f, fns) => fns.eq(f.post.id, f.post_tag.postId)) + .select((f) => ({ postId: f.post.id, tagId: f.post_tag.tagId })) + .where((f, fns) => fns.eq(f.post.id, untaggedPostId)) + .build(); +const rows = await runtime.execute(plan); +// rows === [{ postId: untaggedPostId, tagId: null }] +``` + +### `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. + +#### Remarks + +- **Availability:** `lateralJoin()` requires the adapter to report the `sql.lateral` capability. Prisma Next'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. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `alias` | `string` | Yes | Names the derived table. 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 + +| Return type | Example | Description | +|---|---|---| +| `SelectQuery` | `sql.user.lateralJoin('latestPost', ...)` | A query with the lateral subquery's columns in scope under `alias`. | + +#### Examples + +##### Each user's most recent post + +```ts +const plan = sql.user + .lateralJoin('latestPost', (lateral) => + lateral + .from(sql.post) + .select((f) => ({ id: f.post.id, title: f.post.title })) + .where((f, fns) => fns.eq(f.post.userId, f.user.id)) + .orderBy((f) => f.post.createdAt, { direction: 'desc' }) + .limit(1), + ) + .select((f) => ({ userId: f.user.id, latestPostId: f.latestPost.id })) + .where((f, fns) => fns.eq(f.user.id, aliceId)) + .build(); +const rows = await runtime.execute(plan); +// rows === [{ userId: aliceId, latestPostId: typedSqlPostId }] +``` + +### `orderBy()` + +Sort the result set by a column, a computed expression, or with explicit direction and null placement. + +#### 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. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `SelectQuery` | `sql.user.orderBy('email', ...)` | A query with the sort key applied. Call `orderBy()` again to add secondary keys. | + +#### Examples + +##### Sort by a column + +```ts +const plan = sql.user.select('id', 'email').orderBy('email', { direction: 'asc' }).build(); +const rows = await runtime.execute(plan); +``` + +##### Sort by a computed value + +```ts +const plan = sql.user + .select('id', 'email') + .orderBy((f, fns) => fns.raw`LENGTH(${f.email})`.returns('pg/int4@1'), { direction: 'asc' }) + .build(); +const rows = await runtime.execute(plan); +``` + +##### Control direction and null placement + +```ts +const plan = sql.post + .select('id', 'embedding') + .orderBy('embedding', { direction: 'asc', nulls: 'first' }) + .build(); +const rows = await runtime.execute(plan); +``` + +### `distinct()` + +De-duplicate identical projected rows (`SELECT DISTINCT`). + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `SelectQuery` | `sql.post.select('priority').distinct()` | A query returning only distinct projected rows. | + +#### Examples + +##### De-duplicate projected rows + +```ts +const plan = sql.post.select('priority').distinct().build(); +const rows = await runtime.execute(plan); +// distinct priorities: ['high', 'low', 'urgent'] +``` + +### `distinctOn()` + +Keep the first row per distinct key, according to the query's `orderBy()` (`DISTINCT ON`). + +#### 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 Next'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. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `...keys` | Column names (`string`) | Yes | The columns whose distinct combinations are kept. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `SelectQuery` | `sql.post.distinctOn('userId')` | A query keeping the first row per distinct key. | + +#### Examples + +##### First post per user by date + +```ts +const plan = sql.post + .select('id', 'userId', 'createdAt') + .orderBy('userId', { direction: 'asc' }) + .orderBy('createdAt', { direction: 'asc' }) + .distinctOn('userId') + .build(); +const rows = await runtime.execute(plan); +// one row per user, each the earliest post by createdAt +``` + +### `limit()` and `offset()` + +Cap the number of returned rows and skip rows in the ordered result set. + +#### Options + +| Method | Argument | Description | +|---|---|---| +| `limit(n)` | `number` | Return at most `n` rows. | +| `offset(n)` | `number` | Skip the first `n` rows. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `SelectQuery` | `sql.user.limit(2)` | A query with the row cap or offset applied. | + +#### Examples + +##### Page through results + +```ts +const plan = sql.user + .select('id') + .orderBy('email', { direction: 'asc' }) + .limit(1) + .offset(1) + .build(); +const rows = await runtime.execute(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. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `alias` | `string` | Yes | Names the subquery. Address its columns as `f..` after the join. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| Join source | `sql.post.select(...).as('hp')` | A subquery usable as a join `other`. This is not a buildable query; join it into an outer query first. | + +#### Examples + +##### Join against a subquery + +```ts +const highPriorityPosts = sql.post + .select('id', 'userId') + .where((f, fns) => fns.eq(f.priority, 'high')) + .as('hp'); + +const plan = sql.user + .innerJoin(highPriorityPosts, (f, fns) => fns.eq(f.user.id, f.hp.userId)) + .select((f) => ({ userId: f.user.id, postId: f.hp.id })) + .build(); +const rows = await runtime.execute(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`. + +:::warning[Aggregate results can decode as strings] +At this raw SQL layer there is no ORM decode step, so the PostgreSQL driver's default parsers apply. `COUNT()` (bigint) and `SUM()` / `AVG()` over an integer column (PostgreSQL promotes these to `numeric`) decode as JavaScript **strings**, not numbers, to avoid precision loss. `MIN()` and `MAX()` over an integer column stay integers and decode as numbers. Call `Number(...)` on `COUNT` / `SUM` / `AVG` results before doing arithmetic. A `.returns(...)` codec annotation does not change this: it's a compile-time type declaration, not a runtime cast, so a raw `EXTRACT(...)` typed `.returns('pg/int4@1')` still decodes as a string. Comparisons inside `having()` and `where()` are unaffected, because PostgreSQL evaluates them server-side. +::: + +### `groupBy()` + +Group rows by one or more columns or by a computed expression, producing one row per distinct group. + +#### Options + +| Form | Signature | Description | +|---|---|---| +| Field names | `groupBy('col', 'col2', ...)` | Group by the named columns. | +| Expression | `groupBy((f, fns) => expr)` | Group by a computed value. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `GroupedQuery` | `sql.order.groupBy('customerId')` | A grouped query supporting `having()`, aggregates, ordering, and limits. | + +#### Examples + +##### Group by a column with a count + +```ts +const plan = sql.order + .select('customerId') + .select('orderCount', (f, fns) => fns.count(f.id)) + .groupBy('customerId') + .orderBy('customerId', { direction: 'asc' }) + .build(); +const rows = await runtime.execute(plan); +// orderCount decodes as a string: e.g. { customerId: acmeId, orderCount: '5' } +``` + +##### Group by a computed value + +```ts +const plan = sql.order + .select('yearPlaced', (f, fns) => fns.raw`EXTRACT(YEAR FROM ${f.placedAt})`.returns('pg/int4@1')) + .select('orderCount', (f, fns) => fns.count(f.id)) + .groupBy((f, fns) => fns.raw`EXTRACT(YEAR FROM ${f.placedAt})`.returns('pg/int4@1')) + .build(); +const rows = await runtime.execute(plan); +// rows === [{ yearPlaced: '2024', orderCount: '10' }] — both decode as strings +``` + +### `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, so it works correctly even though the aggregate value would decode as a string on the JavaScript side (see the warning above). + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `predicate` | `(f, fns) => Expression` | Yes | A boolean expression over aggregate functions, for example `fns.gt(fns.sum(f.amount), 1000)`. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `GroupedQuery` | `sql.order.groupBy('customerId').having(...)` | A grouped query filtered to the groups matching the predicate. | + +#### Examples + +##### Filter groups by a sum threshold + +```ts +const plan = sql.order + .select('customerId') + .select('totalAmount', (f, fns) => fns.sum(f.amount)) + .groupBy('customerId') + .having((f, fns) => fns.gt(fns.sum(f.amount), 1000)) + .build(); +const rows = await runtime.execute(plan); +// only the customer whose orders sum over 1000; totalAmount decodes as '1500' +``` + +##### Compare with `count()`, `avg()`, `min()`, `max()` + +```ts +const avgPlan = sql.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 = sql.order + .select('customerId') + .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)) + .build(); + +const avgRows = await runtime.execute(avgPlan); +const minMaxRows = await runtime.execute(minMaxPlan); +// avgAmount decodes as a string ('300.0000000000000000'); +// minAmount / maxAmount decode as numbers (100 / 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. + +#### Examples + +##### Order groups by total, keep the top one + +```ts +const plan = sql.order + .select('customerId') + .select('totalAmount', (f, fns) => fns.sum(f.amount)) + .groupBy('customerId') + .orderBy('totalAmount', { direction: 'desc' }) + .limit(1) + .build(); +const rows = await runtime.execute(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. + +### `insert()` + +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. + +#### 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. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `InsertQuery` | `sql.tag.insert([...])` | An insert query. Buildable directly, or chain [`returning()`](#returning). | + +#### Examples + +##### Insert a single row + +```ts +const plan = sql.tag.insert([{ id: crypto.randomUUID(), label: 'single-row-tag' }]).build(); +await runtime.execute(plan); +``` + +##### Insert multiple rows in one statement + +```ts +const plan = sql.tag + .insert([ + { id: crypto.randomUUID(), label: 'multi-row-a' }, + { id: crypto.randomUUID(), label: 'multi-row-b' }, + ]) + .build(); +await runtime.execute(plan); +``` + +### `returning()` + +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 Next's PostgreSQL adapter reports it. Adapters that don't (for example a SQL target without `RETURNING`) won't expose the method. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `...columns` | Column names (`string`) | Yes | The columns to return from each affected row. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| Mutation query | `sql.tag.insert([...]).returning('id', 'label')` | The mutation query, now typed to resolve to the returned rows. | + +#### Examples + +##### Return the inserted row + +```ts +const id = crypto.randomUUID(); +const plan = sql.tag.insert([{ id, label: 'returned-tag' }]).returning('id', 'label').build(); +const rows = await runtime.execute(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. + +#### Options + +`update()` accepts a values object or an expression callback. + +| 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. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `UpdateQuery` | `sql.user.update({ ... })` | An update query. Chain `where()` and optionally [`returning()`](#returning). | + +#### Examples + +##### Update with a values object + +```ts +const plan = sql.user + .update({ displayName: 'Bobby' }) + .where((f, fns) => fns.eq(f.id, bobId)) + .returning('id', 'displayName') + .build(); +const rows = await runtime.execute(plan); +// rows === [{ id: bobId, displayName: 'Bobby' }] +``` + +##### Derive a value from an existing column + +```ts +const plan = sql.user + .update((f, fns) => ({ displayName: fns.raw`UPPER(${f.displayName})`.returns('pg/text@1') })) + .where((f, fns) => fns.eq(f.id, carolId)) + .returning('id', 'displayName') + .build(); +const rows = await runtime.execute(plan); +// rows === [{ id: carolId, displayName: 'CAROL' }] +``` + +### `delete()` + +Delete matched rows. Gate the delete with `where()`, and use [`returning()`](#returning) to get the deleted rows back. + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `DeleteQuery` | `sql.tag.delete()` | A delete query. Chain `where()` and optionally [`returning()`](#returning). | + +#### Examples + +##### Delete and return the removed row + +```ts +const plan = sql.tag + .delete() + .where((f, fns) => fns.eq(f.id, id)) + .returning('id', 'label') + .build(); +const rows = await runtime.execute(plan); +// rows === [{ id, label: 'to-delete' }] +``` + +### `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`. + +#### Remarks + +- Import `param` from `@prisma-next/sql-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). + +#### 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'`. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `ParamRef` | `param('%@example.com', { codecId: 'pg/text@1' })` | A bound-parameter reference usable inside `fns.raw`. | + +#### Examples + +##### Bind a literal inside a raw fragment + +```ts +import { param } from '@prisma-next/sql-relational-core/expression'; + +const targetEmailDomain = param('%@example.com', { codecId: 'pg/text@1' }); +const plan = sql.user + .select('id', 'email') + .where((f, fns) => fns.raw`${f.email} LIKE ${targetEmailDomain}`.returns('pg/bool@1')) + .orderBy('email', { direction: 'asc' }) + .build(); +const rows = await runtime.execute(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. + +### Built-in functions + +The complete built-in set is fixed: + +| Category | Functions | +|---|---| +| Comparison | `eq`, `ne`, `gt`, `gte`, `lt`, `lte` | +| Membership | `in`, `notIn` | +| Boolean | `and`, `or` | +| Existence | `exists`, `notExists` | +| Raw SQL | `raw` | +| Aggregate (grouped queries) | `count`, `sum`, `avg`, `min`, `max` | + +Nothing else is built in. Any further function, such as `ilike` or `cosineDistance`, comes from an extension pack registered for your contract; without the matching 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(...)` codec. +::: + +### `fns.raw` and `.returns()` + +Write a raw SQL fragment as a tagged template. Interpolate columns and values with `${...}`; each interpolation is parameterized. Call `.returns(codecId)` to declare the fragment's result type. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| SQL fragment | Tagged template | Yes | The raw SQL, with `${...}` interpolations for columns and values. | +| `.returns(codecId)` | `string` | Yes for a projected or compared value | Declares the result codec, for example `'pg/int4@1'`, `'pg/text@1'`, `'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 decodes as a string even when annotated `'pg/int4@1'` (see the [grouped-query warning](#grouped-queries)). +- Use `fns.raw` for any SQL feature without a dedicated helper: `COALESCE`, `CAST`, `LENGTH`, `UPPER`, `EXTRACT`, and so on. + +#### Examples + +##### Compute a column with a SQL function + +```ts +const plan = sql.user + .select('emailLength', (f, fns) => fns.raw`LENGTH(${f.email})`.returns('pg/int4@1')) + .build(); +const rows = await runtime.execute(plan); +``` + +##### `COALESCE` via `fns.raw` + +```ts +const plan = sql.order + .select('amountOrZero', (f, fns) => fns.raw`COALESCE(${f.amount}, 0)`.returns('pg/int4@1')) + .build(); +const rows = await runtime.execute(plan); +``` + +## Compiling and executing + +### `build()` + +Compile a query into an executable plan. + +#### 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. + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| Query plan | `sql.user.select('id').build()` | A plan you run with `runtime.execute(...)`. Its per-row type is recoverable with [`ResultType`](#resulttype). | + +### Executing a plan + +Run a plan with `runtime.execute(plan)`, where `runtime` is the runtime you got from connecting your database client. It resolves to an array of rows (`Row[]`). + +```ts +const plan = sql.user.select('id', 'email').build(); +const rows = await runtime.execute(plan); // Row[] +``` + +### `ResultType` + +Recover a plan's row type at the type level. + +#### Remarks + +- Import `ResultType` from `@prisma-next/framework-components/runtime`. It's a family-agnostic utility, not specific to the SQL builder. +- `ResultType` is the **per-row** shape, not an array. `runtime.execute(plan)` resolves to `Row[]`, but `ResultType` is `Row`. + +#### Examples + +##### Recover the row type from a plan + +```ts +import type { ResultType } from '@prisma-next/framework-components/runtime'; + +const plan = sql.user.select('id', 'email').build(); +type Row = ResultType; // { id: string; email: string } + +const rows = await runtime.execute(plan); // Row[] +``` + +### Streaming vs. collecting + +`runtime.execute(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`](/next/reference/orm-client#asynciterableresult). From c4b217c69907a1d6dfd50d0765d098b090d466b3 Mon Sep 17 00:00:00 2001 From: Nurul Sundarani Date: Fri, 3 Jul 2026 20:59:06 +0530 Subject: [PATCH 04/15] docs(next): address final review findings in reference pages Co-Authored-By: Claude Fable 5 --- .../docs/content/docs/(index)/next/reference/index.mdx | 10 +++++----- .../content/docs/(index)/next/reference/orm-client.mdx | 8 ++++---- .../docs/(index)/next/reference/sql-query-builder.mdx | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/docs/content/docs/(index)/next/reference/index.mdx b/apps/docs/content/docs/(index)/next/reference/index.mdx index 4f61805f5d..090fdd3b38 100644 --- a/apps/docs/content/docs/(index)/next/reference/index.mdx +++ b/apps/docs/content/docs/(index)/next/reference/index.mdx @@ -18,7 +18,7 @@ Each method's reference page includes a Remarks note when its behavior differs b The tables use this legend: - **✅ Available**: verified working against a live database. -- **❌ Not available**: the method doesn't exist on this database's client. Calling it throws `TypeError`. +- **❌ Not available**: the method doesn't exist on this database's client. Calling it usually throws `TypeError`; where misuse instead fails silently (a no-op), the method's reference page says so. - **⚠️ Unverified**: the method is expected to work but hasn't been verified against a live database on the current test contract, or is a type-only restriction rather than a runtime one. See the method's reference page for details. - **— Not applicable**: this database has no equivalent surface by design (rather than a missing or unverified method). @@ -51,7 +51,7 @@ The tables use this legend: | `all()` awaited (collect to array) | ✅ | ✅ | | `all()` as an async iterable (stream) | ✅ | ✅ | | `first()` | ✅ | ✅ | -| `first()` inline callback filter | ✅ | ⚠️ | +| `first()` inline callback filter | ✅ | — | | Custom `Collection` subclass with domain methods | ✅ | — | ### Mutations @@ -67,10 +67,10 @@ The tables use this legend: | `update()` requires a prior `where()` | ⚠️ | ✅ | | `update()` nested `connect()` | ✅ | ⚠️ | | `update()` nested `disconnect()` | ✅ | ⚠️ | -| `update()` field-operations callback form | ❌ | ✅ | +| `update()` field-operations callback form | — | ✅ | | `updateAll()` | ✅ | ✅ | | `updateCount()` | ✅ | ✅ | -| `updateCount()` field-operations callback form | ⚠️ | ✅ | +| `updateCount()` field-operations callback form | — | ✅ | | `delete()` | ✅ | ✅ | | `delete()` requires a prior `where()` | ⚠️ | ✅ | | `deleteAll()` | ✅ | ✅ | @@ -156,7 +156,7 @@ The SQL query builder targets PostgreSQL only, so these tables have no MongoDB c | `having()` with `count()` / `sum()` / `avg()` / `min()` / `max()` | ✅ | | `orderBy()` / `limit()` on a grouped query | ✅ | -`returning()` and `lateralJoin()` require the adapter to report the `sql.returning` and `sql.lateral` capabilities. `distinctOn()` requires the `postgres.distinctOn` capability. All three are available on Prisma Next's Postgres adapter. `COUNT()`, `SUM()`, and `AVG()` results decode as strings, not numbers, at this layer; call `Number(...)` before doing arithmetic on them. `MIN()` and `MAX()` decode as numbers. +`returning()` and `lateralJoin()` require the adapter to report the `sql.returning` and `sql.lateral` capabilities. `distinctOn()` requires the `postgres.distinctOn` capability. All three are available on Prisma Next's Postgres adapter. `COUNT()`, `SUM()`, and `AVG()` results decode as strings, not numbers, at this layer; call `Number(...)` before doing arithmetic on them. `MIN()` and `MAX()` decode as numbers over integer columns. :::note[SQLite is next on deck] Prisma Next ships first-class support for PostgreSQL and MongoDB today. SQLite is the next SQL target on deck, with MySQL to follow. diff --git a/apps/docs/content/docs/(index)/next/reference/orm-client.mdx b/apps/docs/content/docs/(index)/next/reference/orm-client.mdx index ecde36c12b..75672c8a96 100644 --- a/apps/docs/content/docs/(index)/next/reference/orm-client.mdx +++ b/apps/docs/content/docs/(index)/next/reference/orm-client.mdx @@ -19,7 +19,7 @@ You create an ORM client by connecting a runtime and calling `orm(...)`. The two ### PostgreSQL -Create a Postgres client, connect it to get a runtime, then pass that runtime to `orm(...)`. Model accessors hang off `.public` and use the contract's **root names**, which match your PSL model names (`orm.User`, `orm.Post`). +Create a Postgres client, connect it to get a runtime, then pass that runtime to `orm(...)`. Model accessors hang off `.public` and use the contract's **root names**, which match your Prisma Schema Language (PSL) model names (`orm.User`, `orm.Post`). ```ts import postgres from '@prisma-next/postgres/runtime'; @@ -238,7 +238,7 @@ On PostgreSQL, `include()`'s refinement callback receives the nested relation as - PostgreSQL only. MongoDB's `include()` has no refinement callback; the scalar reducers `count`/`sum`/`avg`/`min`/`max`/`combine` do not exist on the Mongo collection at all (calling them throws `TypeError`). - The reducers are only callable **inside** an `include()` refinement callback. Called elsewhere on a PostgreSQL collection, they throw `Error` (the method exists but asserts refinement mode). -- `sum()` and `avg()` require a field with the numeric codec trait. Over a to-many relation with no rows, they resolve to `null`, not `0`. +- `sum()` and `avg()` require a field with the numeric codec trait (codec traits are capability tags on a column's type mapping; they gate which comparison and aggregate methods a field offers). Over a to-many relation with no rows, they resolve to `null`, not `0`. - `min()` and `max()` are typed for numeric fields (`NumericFieldNames`). At the SQL level, Postgres's `MIN`/`MAX` also accept date/time columns, so `min('createdAt')` returns a real value at runtime even though it fails the type check. Treat the numeric-only typing as a type-level restriction, not a runtime one, for date and time columns. Do not rely on this: it requires bypassing the type system. #### Options @@ -1051,7 +1051,7 @@ const updated = await db.Post.where({ id: postId }) .select('id', 'title') .include('tags', (tag) => tag.select('id', 'label').orderBy((t) => t.label.asc())) .update({ - tags: (tag) => tag.disconnect([{ id: ormTagId }]), + tags: (tag) => tag.disconnect([{ id: tagId }]), }); // only the junction row is removed; the Tag row itself still exists ``` @@ -1674,7 +1674,7 @@ Static factories that build MongoDB filter expressions. #### Remarks - MongoDB. Imported from `@prisma-next/mongo-query-ast/execution`. -- Exactly ten factories exist: `of`, `eq`, `neq`, `gt`, `lt`, `gte`, `lte`, `in`, `nin`, `isNull`, `isNotNull`. The inequality factory is `neq`, **not** `ne` (`MongoFieldFilter.ne` is `undefined`). +- Exactly eleven factories exist: `of`, `eq`, `neq`, `gt`, `lt`, `gte`, `lte`, `in`, `nin`, `isNull`, `isNotNull`. The inequality factory is `neq`, **not** `ne` (`MongoFieldFilter.ne` is `undefined`). - `isNull` / `isNotNull` are sugar for equals-null / not-equals-null, not a `$exists` check. - The factories `exists`, `regex`, `elemMatch`, `all`, and `size` do **not** exist on `MongoFieldFilter`. There is a separate `MongoExistsExpr` class for `$exists`, but no regex, `elemMatch`, `$all`, or `$size` support anywhere in the library. diff --git a/apps/docs/content/docs/(index)/next/reference/sql-query-builder.mdx b/apps/docs/content/docs/(index)/next/reference/sql-query-builder.mdx index c404b89cbb..80debe729f 100644 --- a/apps/docs/content/docs/(index)/next/reference/sql-query-builder.mdx +++ b/apps/docs/content/docs/(index)/next/reference/sql-query-builder.mdx @@ -251,7 +251,7 @@ const plan = sql.user .where((f, fns) => fns.eq(f.user.id, aliceId)) .build(); const rows = await runtime.execute(plan); -// rows === [{ userId: aliceId, latestPostId: typedSqlPostId }] +// rows === [{ userId: aliceId, latestPostId: newestPostId }] ``` ### `orderBy()` From e8524411db1ba75a6ef9d0ea80651b29aa7a7ac2 Mon Sep 17 00:00:00 2001 From: Nurul Sundarani Date: Tue, 7 Jul 2026 17:25:15 +0530 Subject: [PATCH 05/15] docs(next): move reference under /orm/next and slim the index Co-Authored-By: Claude Fable 5 --- apps/docs/content/docs/(index)/meta.json | 1 - .../docs/(index)/next/reference/index.mdx | 183 ------------------ apps/docs/content/docs/orm/next/meta.json | 5 +- .../content/docs/orm/next/reference/index.mdx | 48 +++++ .../{(index) => orm}/next/reference/meta.json | 0 .../next/reference/orm-client.mdx | 4 +- .../next/reference/sql-query-builder.mdx | 10 +- 7 files changed, 59 insertions(+), 192 deletions(-) delete mode 100644 apps/docs/content/docs/(index)/next/reference/index.mdx create mode 100644 apps/docs/content/docs/orm/next/reference/index.mdx rename apps/docs/content/docs/{(index) => orm}/next/reference/meta.json (100%) rename apps/docs/content/docs/{(index) => orm}/next/reference/orm-client.mdx (99%) rename apps/docs/content/docs/{(index) => orm}/next/reference/sql-query-builder.mdx (98%) diff --git a/apps/docs/content/docs/(index)/meta.json b/apps/docs/content/docs/(index)/meta.json index dcd7b18ac7..c7375e791a 100644 --- a/apps/docs/content/docs/(index)/meta.json +++ b/apps/docs/content/docs/(index)/meta.json @@ -11,7 +11,6 @@ "---Prisma Next---", "next/quickstart", "next/add-to-existing-project", - "next/reference", "---Prisma ORM---", "...prisma-orm", "---Prisma Postgres---", diff --git a/apps/docs/content/docs/(index)/next/reference/index.mdx b/apps/docs/content/docs/(index)/next/reference/index.mdx deleted file mode 100644 index 090fdd3b38..0000000000 --- a/apps/docs/content/docs/(index)/next/reference/index.mdx +++ /dev/null @@ -1,183 +0,0 @@ ---- -title: Prisma Next API reference -description: Availability matrix and reference index for the Prisma Next ORM client and SQL query builder. -url: /next/reference -metaTitle: Prisma Next API reference -metaDescription: Availability matrix and reference index for the Prisma Next ORM client and SQL query builder. -badge: early-access ---- - -Prisma Next has two 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. - -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, or when you want direct control over the generated SQL. - -## How availability is documented - -Each method's reference page includes a Remarks note when its behavior differs between PostgreSQL and MongoDB, or when it's type-checked but not enforced at runtime. The table below summarizes availability across both databases. Read the linked method page for the full behavior, including any type-vs-runtime gaps. - -The tables use this legend: - -- **✅ Available**: verified working against a live database. -- **❌ Not available**: the method doesn't exist on this database's client. Calling it usually throws `TypeError`; where misuse instead fails silently (a no-op), the method's reference page says so. -- **⚠️ Unverified**: the method is expected to work but hasn't been verified against a live database on the current test contract, or is a type-only restriction rather than a runtime one. See the method's reference page for details. -- **— Not applicable**: this database has no equivalent surface by design (rather than a missing or unverified method). - -### Query building - -| Method | PostgreSQL | MongoDB | -|---|---|---| -| `where()` (callback form) | ✅ | ⚠️ | -| `where()` (shorthand object form) | ✅ | ✅ | -| `where()` (chained calls, ANDed) | ✅ | ⚠️ | -| `select()` | ✅ | ✅ | -| `include()` (to-one relation) | ✅ | ⚠️ | -| `include()` (to-many relation) | ✅ | ⚠️ | -| `include()` (reference relation) | ⚠️ | ✅ | -| `include()` refinement (filter, take, orderBy) | ✅ | ❌ | -| `include()` refinement scalar reducers (`count`, `sum`, `avg`, `min`, `max`, `combine`) | ✅ | ❌ | -| `orderBy()` | ✅ | ✅ | -| `take()` | ✅ | ✅ | -| `skip()` | ✅ | ✅ | -| `cursor()` | ✅ | ❌ | -| `distinct()` | ✅ | ❌ | -| `distinctOn()` | ✅ | ❌ | -| `groupBy()` | ✅ | ❌ | -| `variant()` | ✅ | ✅ | - -### Read terminals - -| Method | PostgreSQL | MongoDB | -|---|---|---| -| `all()` awaited (collect to array) | ✅ | ✅ | -| `all()` as an async iterable (stream) | ✅ | ✅ | -| `first()` | ✅ | ✅ | -| `first()` inline callback filter | ✅ | — | -| Custom `Collection` subclass with domain methods | ✅ | — | - -### Mutations - -| Method | PostgreSQL | MongoDB | -|---|---|---| -| `create()` | ✅ | ✅ | -| `create()` nested `create()` (child-owned relation) | ✅ | ⚠️ | -| `create()` nested `connect()` (parent-owned relation) | ✅ | ⚠️ | -| `createAll()` awaited and streamed | ✅ | ✅ | -| `createCount()` | ✅ | ✅ | -| `update()` (data object) | ✅ | ✅ | -| `update()` requires a prior `where()` | ⚠️ | ✅ | -| `update()` nested `connect()` | ✅ | ⚠️ | -| `update()` nested `disconnect()` | ✅ | ⚠️ | -| `update()` field-operations callback form | — | ✅ | -| `updateAll()` | ✅ | ✅ | -| `updateCount()` | ✅ | ✅ | -| `updateCount()` field-operations callback form | — | ✅ | -| `delete()` | ✅ | ✅ | -| `delete()` requires a prior `where()` | ⚠️ | ✅ | -| `deleteAll()` | ✅ | ✅ | -| `deleteCount()` | ✅ | ✅ | -| `upsert()` | ✅ | ✅ | -| `upsert()` field-operations callback form (update side) | — | ✅ | - -### Field update operations - -Mongo field operations are accessed through a field accessor inside `update()`/`updateCount()`/`upsert()` callbacks (for example `t.field.inc(...)`). PostgreSQL's `update()` has no equivalent callback form; see the mutations table above. - -| Field operation | MongoDB | -|---|---| -| `set()` | ✅ | -| `inc()` | ✅ | -| `mul()` | ✅ | -| `unset()` | ✅ | -| `push()` / `pull()` / `addToSet()` / `pop()` | ⚠️ | -| `rename()` | ❌ | -| `min()` / `max()` / `currentDate()` | ❌ | - -### Filters & operators - -| Filter or operator | PostgreSQL | MongoDB | -|---|---|---| -| `eq()` / `neq()` | ✅ | ✅ | -| `gt()` / `lt()` / `gte()` / `lte()` | ✅ | ✅ | -| `like()` | ✅ | — | -| `ilike()` | ✅ | — | -| `in()` / `notIn()` | ✅ | ✅ | -| `isNull()` / `isNotNull()` | ✅ | ✅ | -| `and()` / `not()` (combinators) | ✅ | ✅ | -| `or()` (combinator) | ✅ | ❌ (use `MongoOrExpr.of(...)`) | -| `all()` (constant-true predicate) | ✅ | — | -| `some()` / `every()` / `none()` (relation filters) | ✅ | — | -| Dot-notation path into an embedded object | — | ⚠️ | -| `exists` / `regex` / `elemMatch` / `all` / `size` filters | — | ❌ | -| `$nor` combinator | — | ❌ | - -MongoDB filters are built with `MongoFieldFilter` static factories (`eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`, `isNull`, `isNotNull`) plus `and()`/`not()` instance methods and the standalone `MongoOrExpr.of(...)` for OR. There's no `.or()` instance method and no `$nor` support. - -### Aggregates - -| Method | PostgreSQL | MongoDB | -|---|---|---| -| `aggregate()` with `count()` | ✅ | ❌ | -| `aggregate()` with `sum()` / `avg()` | ✅ | ❌ | -| `aggregate()` with `min()` / `max()` | ✅ | ❌ | -| `groupBy().aggregate()` | ✅ | ❌ | -| `groupBy().having()` | ✅ | ❌ | - -`sum()` and `avg()` resolve to `null`, not `0`, when the aggregated result set is empty. `count()` resolves to `0` in the same case. - -### SQL builder - -The SQL query builder targets PostgreSQL only, so these tables have no MongoDB column. - -| Method | PostgreSQL | -|---|---| -| `select()` (columns, aliased expression, object-of-expressions) | ✅ | -| `where()` expression callback (`and()`/`or()`) | ✅ | -| `innerJoin()` | ✅ | -| `outerLeftJoin()` / `outerRightJoin()` / `outerFullJoin()` | ✅ | -| `lateralJoin()` | ✅ | -| `orderBy()` (field name, expression, or options) | ✅ | -| `distinct()` | ✅ | -| `distinctOn()` | ✅ | -| `limit()` / `offset()` | ✅ | -| Subquery via `.as()` | ✅ | -| `build()` + `runtime.execute()` | ✅ | - -| Mutation method | PostgreSQL | -|---|---| -| `insert()` (single or multi-row) | ✅ | -| `insert().returning()` | ✅ | -| `update()` (values object or expression callback) | ✅ | -| `delete()` with `where()` and `returning()` | ✅ | -| `param()` (explicit codec on a raw value) | ✅ | - -| Grouped query method | PostgreSQL | -|---|---| -| `groupBy()` (field names or expression) | ✅ | -| `having()` with `count()` / `sum()` / `avg()` / `min()` / `max()` | ✅ | -| `orderBy()` / `limit()` on a grouped query | ✅ | - -`returning()` and `lateralJoin()` require the adapter to report the `sql.returning` and `sql.lateral` capabilities. `distinctOn()` requires the `postgres.distinctOn` capability. All three are available on Prisma Next's Postgres adapter. `COUNT()`, `SUM()`, and `AVG()` results decode as strings, not numbers, at this layer; call `Number(...)` before doing arithmetic on them. `MIN()` and `MAX()` decode as numbers over integer columns. - -:::note[SQLite is next on deck] -Prisma Next ships first-class support for PostgreSQL and MongoDB today. SQLite is the next SQL target on deck, with MySQL to follow. -::: - -## Coming to this reference - -These surfaces are planned but not yet documented here: - -:::note -- The MongoDB pipeline builder (`db.query`) -- Raw query escape hatches -- Transaction and runtime APIs -- Middleware -::: - - - }> - 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. - - diff --git a/apps/docs/content/docs/orm/next/meta.json b/apps/docs/content/docs/orm/next/meta.json index 83bff2fb31..858c65633e 100644 --- a/apps/docs/content/docs/orm/next/meta.json +++ b/apps/docs/content/docs/orm/next/meta.json @@ -15,6 +15,9 @@ "...middleware", "---Extensions---", - "...extensions" + "...extensions", + + "---Reference---", + "...reference" ] } diff --git a/apps/docs/content/docs/orm/next/reference/index.mdx b/apps/docs/content/docs/orm/next/reference/index.mdx new file mode 100644 index 0000000000..c454228cab --- /dev/null +++ b/apps/docs/content/docs/orm/next/reference/index.mdx @@ -0,0 +1,48 @@ +--- +title: Prisma Next API reference +description: Reference index for the Prisma Next ORM client and SQL query builder. +url: /orm/next/reference +metaTitle: Prisma Next API reference +metaDescription: Reference index for the Prisma Next ORM client and SQL query builder. +badge: early-access +--- + +Prisma Next has two 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. + +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, or when you want direct control over the generated SQL. + +## How database differences are documented + +Both reference pages document every method with the classic Remarks / Options / Return type / Examples structure. 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 — there's no separate availability matrix to cross-reference. + +For a conceptual walkthrough of reading, writing, and querying data (rather than an exhaustive method-by-method reference), see the [Fundamentals](/orm/next/fundamentals/reading-data) section: + +- [Reading data](/orm/next/fundamentals/reading-data) +- [Writing data](/orm/next/fundamentals/writing-data) +- [Relations and joins](/orm/next/fundamentals/relations-and-joins) +- [Transactions](/orm/next/fundamentals/transactions) +- [Advanced queries](/orm/next/fundamentals/advanced-queries) + +:::note[SQLite is next on deck] +Prisma Next ships first-class support for PostgreSQL and MongoDB today. SQLite is the next SQL target on deck, with MySQL to follow. +::: + +## Coming to this reference + +These surfaces are planned but not yet documented here: + +:::note +- The MongoDB pipeline builder (`db.query`) +- Raw query escape hatches +- Transaction and runtime APIs +- Middleware hook APIs (for concepts and the built-in middleware, see [How middleware works](/orm/next/middleware/how-middleware-works)) +::: + + + }> + 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. + + diff --git a/apps/docs/content/docs/(index)/next/reference/meta.json b/apps/docs/content/docs/orm/next/reference/meta.json similarity index 100% rename from apps/docs/content/docs/(index)/next/reference/meta.json rename to apps/docs/content/docs/orm/next/reference/meta.json diff --git a/apps/docs/content/docs/(index)/next/reference/orm-client.mdx b/apps/docs/content/docs/orm/next/reference/orm-client.mdx similarity index 99% rename from apps/docs/content/docs/(index)/next/reference/orm-client.mdx rename to apps/docs/content/docs/orm/next/reference/orm-client.mdx index 75672c8a96..e74641f14a 100644 --- a/apps/docs/content/docs/(index)/next/reference/orm-client.mdx +++ b/apps/docs/content/docs/orm/next/reference/orm-client.mdx @@ -1,7 +1,7 @@ --- title: ORM client reference description: Reference for the Prisma Next ORM client's query, mutation, filter, and aggregate methods. -url: /next/reference/orm-client +url: /orm/next/reference/orm-client metaTitle: Prisma Next ORM client reference metaDescription: Reference for the Prisma Next ORM client's query, mutation, filter, and aggregate methods. badge: early-access @@ -9,7 +9,7 @@ badge: early-access The ORM client gives you model-level methods for reading and writing data across PostgreSQL and MongoDB. This page documents every method, its availability on each database, and the behavior that differs between the two. -Availability is stated per method. When a method behaves the same on both databases, the Remarks say so; when it differs, exists on only one, or is type-checked but not enforced at runtime, the Remarks call that out. For a one-screen summary of availability across every method, see the [Prisma Next API reference](/next/reference). +Availability is stated per method. When a method behaves the same on both databases, the Remarks say so; when it differs, exists on only one, or is type-checked but not enforced at runtime, the Remarks call that out. For a one-screen summary of availability across every method, see the [Prisma Next API reference](/orm/next/reference). Every code example on this page is transcribed from a test that runs against a live database. PostgreSQL examples use a `User` / `Post` / `Tag` / `Task` schema; MongoDB examples use a `users` / `posts` schema; numeric aggregate examples use a `Customer` / `Order` schema. diff --git a/apps/docs/content/docs/(index)/next/reference/sql-query-builder.mdx b/apps/docs/content/docs/orm/next/reference/sql-query-builder.mdx similarity index 98% rename from apps/docs/content/docs/(index)/next/reference/sql-query-builder.mdx rename to apps/docs/content/docs/orm/next/reference/sql-query-builder.mdx index 80debe729f..358e0888cf 100644 --- a/apps/docs/content/docs/(index)/next/reference/sql-query-builder.mdx +++ b/apps/docs/content/docs/orm/next/reference/sql-query-builder.mdx @@ -1,20 +1,20 @@ --- title: SQL query builder reference description: Reference for the Prisma Next SQL query builder's select, mutation, and grouped query methods. -url: /next/reference/sql-query-builder +url: /orm/next/reference/sql-query-builder metaTitle: Prisma Next SQL query builder reference metaDescription: Reference for the Prisma Next SQL query builder's select, mutation, and grouped query methods. badge: early-access --- -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](/next/reference/orm-client). Reach for it when you need a join, aggregate, or SQL feature the ORM client doesn't expose, or when you want direct control over the generated SQL. +The SQL query builder gives you table-level, SQL-shaped methods for building typed queries: `select()`, `innerJoin()`, `groupBy()`, and the rest. It's the lower-level escape hatch that sits alongside the [ORM client](/orm/next/reference/orm-client). Reach for it when you need a join, aggregate, or SQL feature the ORM client doesn't expose, or when you want direct control over the generated SQL. -This page documents every method, its options, and its return type. Where a method is capability-gated or has a runtime caveat, the Remarks call that out. For a one-screen summary of availability, see the [Prisma Next API reference](/next/reference). +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 one-screen summary of availability, see the [Prisma Next API reference](/orm/next/reference). Every code example on this page is transcribed from a test that runs against a live PostgreSQL database. Select and mutation examples use a `user` / `post` / `post_tag` / `tag` schema; grouped-query examples use a `customer` / `order` schema whose `order` table has numeric `amount` and `quantity` columns. :::note[This is the SQL-family builder] -The SQL query builder targets SQL databases. PostgreSQL is supported today, with SQLite next on deck. MongoDB has no SQL builder: use the [ORM client](/next/reference/orm-client) for MongoDB queries today, and the MongoDB pipeline builder (planned) for aggregation pipelines. +The SQL query builder targets SQL databases. PostgreSQL is supported today, with SQLite next on deck. MongoDB has no SQL builder: use the [ORM client](/orm/next/reference/orm-client) for MongoDB queries today, and the MongoDB pipeline builder (planned) for aggregation pipelines. ::: ## Entry points @@ -847,4 +847,4 @@ const rows = await runtime.execute(plan); // Row[] ### Streaming vs. collecting -`runtime.execute(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`](/next/reference/orm-client#asynciterableresult). +`runtime.execute(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/next/reference/orm-client#asynciterableresult). From 6bab58261726935feff78136ed6471380c0db384 Mon Sep 17 00:00:00 2001 From: Nurul Sundarani Date: Tue, 7 Jul 2026 17:37:17 +0530 Subject: [PATCH 06/15] docs(next): schema-first ORM client reference Co-Authored-By: Claude Fable 5 --- .../docs/orm/next/reference/orm-client.mdx | 983 +++++++++--------- 1 file changed, 515 insertions(+), 468 deletions(-) diff --git a/apps/docs/content/docs/orm/next/reference/orm-client.mdx b/apps/docs/content/docs/orm/next/reference/orm-client.mdx index e74641f14a..f8a01898fa 100644 --- a/apps/docs/content/docs/orm/next/reference/orm-client.mdx +++ b/apps/docs/content/docs/orm/next/reference/orm-client.mdx @@ -9,51 +9,212 @@ badge: early-access The ORM client gives you model-level methods for reading and writing data across PostgreSQL and MongoDB. This page documents every method, its availability on each database, and the behavior that differs between the two. -Availability is stated per method. When a method behaves the same on both databases, the Remarks say so; when it differs, exists on only one, or is type-checked but not enforced at runtime, the Remarks call that out. For a one-screen summary of availability across every method, see the [Prisma Next API reference](/orm/next/reference). +Availability is stated per method. When a method behaves the same on both databases, the Remarks say so; when it differs, exists on only one, or is type-checked but not enforced at runtime, the Remarks call that out. -Every code example on this page is transcribed from a test that runs against a live database. PostgreSQL examples use a `User` / `Post` / `Tag` / `Task` schema; MongoDB examples use a `users` / `posts` schema; numeric aggregate examples use a `Customer` / `Order` schema. +For task-oriented walkthroughs, see the Fundamentals guides: [Reading data](/orm/next/fundamentals/reading-data), [Writing data](/orm/next/fundamentals/writing-data), [Relations and joins](/orm/next/fundamentals/relations-and-joins), and [Transactions](/orm/next/fundamentals/transactions). + +## Example schema + +All examples on this page run against the following schema, and every example is transcribed from an executable test suite that runs against a live database. The grouped-aggregate examples use a separate `Customer` / `Order` schema, shown under [Grouped aggregates](#grouped-aggregates). + +
+ +Expand for the example schema + +```prisma tab="PostgreSQL" +types { + Embedding1536 = pgvector.Vector(1536) + Uuid = String @db.Uuid +} + +type Address { + street String + city String + zip String? + country String +} + +enum user_type { + @@type("pg/text@1") + admin + user +} + +enum Priority { + @@type("pg/text@1") + Low = "low" + High = "high" + Urgent = "urgent" +} + +model User { + id Uuid @id @default(uuid()) + email String + displayName String + createdAt DateTime @default(now()) + kind user_type + address Address? + posts Post[] + tasks Task[] + + @@map("user") +} + +model Post { + id Uuid @id @default(uuid()) + title String + userId Uuid + priority Priority @default(Low) + createdAt DateTime @default(now()) + embedding Embedding1536? + + user User @relation(fields: [userId], references: [id]) + tags Tag[] + + @@map("post") +} + +model Tag { + id Uuid @id @default(uuid()) + label String @unique + + posts Post[] + + @@map("tag") +} + +model PostTag { + postId Uuid + tagId Uuid + + post Post @relation(fields: [postId], references: [id]) + tag Tag @relation(fields: [tagId], references: [id]) + + @@id([postId, tagId]) + @@map("post_tag") +} + +model Task { + id Uuid @id @default(uuid()) + title String + description String? + status String @default("open") + type String + userId Uuid + createdAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id]) + + @@discriminator(type) + @@map("task") +} + +model Bug { + severity String + stepsToRepro String? + @@base(Task, "bug") + @@map("bug") +} + +model Feature { + priority String + targetRelease String? + @@base(Task, "feature") + @@map("feature") +} +``` + +```prisma tab="MongoDB" +enum UserRole { + @@type("mongo/string@1") + Admin = "admin" + Author = "author" + Reader = "reader" +} + +type Address { + street String + city String + zip String? + country String +} + +model User { + id ObjectId @id @map("_id") + name String + email String + bio String? + role UserRole + address Address? + posts Post[] + @@map("users") +} + +model Post { + id ObjectId @id @map("_id") + title String + content String + kind String + authorId ObjectId + createdAt DateTime + author User @relation(fields: [authorId], references: [id]) + @@discriminator(kind) + @@index([authorId]) + @@index([createdAt(sort: Desc), authorId]) + @@map("posts") +} + +model Article { + summary String + @@base(Post, "article") + @@unique([summary]) +} + +model Tutorial { + difficulty String + duration Int + @@base(Post, "tutorial") +} +``` + +
## Setting up the client -You create an ORM client by connecting a runtime and calling `orm(...)`. The two databases have different entry points and different accessor conventions. +Create an ORM client with `postgres(...)` or `mongo(...)`, then query models through the client's `.orm` facet. The two databases have different entry points and different accessor conventions. ### PostgreSQL -Create a Postgres client, connect it to get a runtime, then pass that runtime to `orm(...)`. Model accessors hang off `.public` and use the contract's **root names**, which match your Prisma Schema Language (PSL) model names (`orm.User`, `orm.Post`). +Create a Postgres client with `postgres(...)`. Models hang off `db.orm.public` and use the contract's **root names**, which match your Prisma Schema Language (PSL) model names (`db.orm.public.User`, `db.orm.public.Post`). `public` is the default PostgreSQL schema namespace. -```ts +```typescript import postgres from '@prisma-next/postgres/runtime'; -import { orm } from '@prisma-next/sql-orm-client'; import type { Contract } from './contract.d'; import contractJson from './contract.json' with { type: 'json' }; -const client = postgres({ contractJson, url: process.env.DATABASE_URL }); -const runtime = await client.connect(); +const db = postgres({ contractJson, url: process.env.DATABASE_URL }); -const db = orm({ runtime, context: client.context, collections: {} }).public; - -const users = await db.User.all(); +const users = await db.orm.public.User.all(); ``` -You can register a custom `Collection` subclass to attach domain methods to a model. See [Custom `Collection` subclass](#custom-collection-subclass). +To attach domain methods to a model, register a custom `Collection` subclass when you build the client. That path uses the `orm(...)` factory instead of the client's built-in facet; see [Custom `Collection` subclass](#custom-collection-subclass). ### MongoDB -Create a Mongo client with `mongo(...)`; its `.orm` property is the client. MongoDB accessors use the contract's **root names too, but those are the lowercase plural collection names** from the contract's `roots` map, not the PSL model names (`orm.users`, `orm.posts`, not `orm.User`). +Create a Mongo client with `mongo(...)`. Models hang off `db.orm` and use the contract's **root names too, but those are the lowercase plural collection names** from the contract's `roots` map, not the PSL model names (`db.orm.users`, `db.orm.posts`, not `db.orm.User`). -```ts +```typescript import mongo from '@prisma-next/mongo/runtime'; import type { Contract } from './contract.d'; import contractJson from './contract.json' with { type: 'json' }; -const client = mongo({ contractJson, url: process.env.MONGODB_URL, dbName: 'app' }); -const orm = client.orm; +const db = mongo({ contractJson, url: process.env.MONGODB_URL, dbName: 'app' }); -const users = await orm.users.all(); +const users = await db.orm.users.all(); ``` :::note[Accessor naming differs by database] -PostgreSQL exposes models under their contract root names (matching PSL model names): `orm.User`. MongoDB exposes them under the registered collection root names (lowercase plural): `orm.users`. The examples below follow each database's convention. +PostgreSQL exposes models under their contract root names (matching PSL model names) and namespaced by schema: `db.orm.public.User`. MongoDB exposes them under the registered collection root names (lowercase plural), with no namespace: `db.orm.users`. The examples below follow each database's convention. ::: ## Query-building methods @@ -83,58 +244,48 @@ Restrict a query to rows matching a filter. | Return type | Example | Description | |---|---|---| -| `Collection` | `db.User.where(...)` | A collection narrowed by the filter, chainable and awaitable through a terminal. | +| `Collection` | `db.orm.public.User.where(...)` | A collection narrowed by the filter, chainable and awaitable through a terminal. | #### Examples ##### Callback form with a column operator (PostgreSQL) -```ts -const admins = await db.User.where((u) => u.kind.eq('admin')).all(); +```typescript +const admins = await db.orm.public.User.where((u) => u.kind.eq('admin')).all(); ``` ##### Shorthand object form - - - - -```ts -const bob = await db.User.where({ email: 'bob@example.com' }).first(); +```typescript tab="PostgreSQL" +const bob = await db.orm.public.User.where({ email: 'bob@example.com' }).first(); ``` - - - - -```ts -const authors = await orm.users.where({ role: 'author' }).all(); +```typescript tab="MongoDB" +const authors = await db.orm.users.where({ role: 'author' }).all(); ``` - - - - ##### Chaining `where()` calls (ANDed) -```ts -const carolUrgentPosts = await db.Post.where({ userId: carolId }) +```typescript +const carolUrgentPosts = await db.orm.public.Post.where({ userId: carolId }) .where((p) => p.priority.eq('urgent')) .all(); ``` ##### `MongoFieldFilter` expression (MongoDB) -```ts +```typescript import { MongoFieldFilter } from '@prisma-next/mongo-query-ast/execution'; -const alice = await orm.users.where(MongoFieldFilter.eq('email', 'alice@example.com')).first(); +const alice = await db.orm.users.where(MongoFieldFilter.eq('email', 'alice@example.com')).first(); -const recentPosts = await orm.posts +const recentPosts = await db.orm.posts .where(MongoFieldFilter.gte('createdAt', new Date('2024-01-02T00:00:00.000Z'))) .all(); ``` +For a task-oriented guide to filtering, see [Reading data](/orm/next/fundamentals/reading-data#filter-records). + ### `select()` Project a row down to a subset of scalar fields. @@ -155,33 +306,21 @@ Project a row down to a subset of scalar fields. | Return type | Example | Description | |---|---|---| -| `Collection` | `db.User.select('id', 'email')` | A collection projected to the named fields. | +| `Collection` | `db.orm.public.User.select('id', 'email')` | A collection projected to the named fields. | #### Examples ##### Project to a subset of fields - - - - -```ts -const summaries = await db.User.select('id', 'email').orderBy((u) => u.email.asc()).all(); +```typescript tab="PostgreSQL" +const summaries = await db.orm.public.User.select('id', 'email').orderBy((u) => u.email.asc()).all(); // summaries[0] is { id, email } — no displayName ``` - - - - -```ts -const summaries = await orm.users.select('name', 'email').all(); +```typescript tab="MongoDB" +const summaries = await db.orm.users.select('name', 'email').all(); ``` - - - - ### `include()` Eagerly load a relation onto the returned rows. @@ -205,31 +344,33 @@ Eagerly load a relation onto the returned rows. | Return type | Example | Description | |---|---|---| -| `Collection` | `db.User.include('posts')` | A collection whose rows carry the loaded relation. | +| `Collection` | `db.orm.public.User.include('posts')` | A collection whose rows carry the loaded relation. | #### Examples ##### To-one relation (PostgreSQL) -```ts -const posts = await db.Post.include('user').where({ id: postId }).all(); +```typescript +const posts = await db.orm.public.Post.include('user').where({ id: postId }).all(); // posts[0].user is the related User ``` ##### To-many relation (PostgreSQL) -```ts -const users = await db.User.include('posts').where({ id: aliceId }).all(); +```typescript +const users = await db.orm.public.User.include('posts').where({ id: aliceId }).all(); // users[0].posts is an array of the user's posts ``` ##### Reference relation (MongoDB) -```ts -const posts = await orm.posts.include('author').where({ title: 'Hello world' }).all(); +```typescript +const posts = await db.orm.posts.include('author').where({ title: 'Hello world' }).all(); // posts[0].author._id is a raw ObjectId — compare with String(posts[0].author._id) ``` +For a task-oriented guide to loading related records, see [Relations and joins](/orm/next/fundamentals/relations-and-joins). + ### Refinements, reducers, and combine On PostgreSQL, `include()`'s refinement callback receives the nested relation as a full `Collection`. You can filter, order, and paginate it, reduce it to a scalar, or `combine()` several sub-views into one shape. @@ -261,8 +402,8 @@ On PostgreSQL, `include()`'s refinement callback receives the nested relation as ##### Filter, order, and take within a relation (PostgreSQL) -```ts -const users = await db.User.include('posts', (posts) => +```typescript +const users = await db.orm.public.User.include('posts', (posts) => posts .where((p) => p.priority.eq('low')) .orderBy((p) => p.createdAt.desc()) @@ -274,8 +415,8 @@ const users = await db.User.include('posts', (posts) => ##### Reduce a relation to a count (PostgreSQL) -```ts -const users = await db.User.include('posts', (posts) => posts.count()) +```typescript +const users = await db.orm.public.User.include('posts', (posts) => posts.count()) .where({ id: aliceId }) .all(); // users[0].posts is the number 2 @@ -283,13 +424,15 @@ const users = await db.User.include('posts', (posts) => posts.count()) ##### `sum()` / `avg()` over a numeric relation (PostgreSQL) -```ts -const customers = await db.Customer.include('orders', (orders) => orders.sum('amount')) +This example uses the `Customer` / `Order` models from the aggregate example schema shown in [Grouped aggregates](#grouped-aggregates). + +```typescript +const customers = await db.orm.public.Customer.include('orders', (orders) => orders.sum('amount')) .where({ id: acmeId }) .all(); // customers[0].orders is 1500 -const avgCustomers = await db.Customer.include('orders', (orders) => orders.avg('amount')) +const avgCustomers = await db.orm.public.Customer.include('orders', (orders) => orders.avg('amount')) .where({ id: acmeId }) .all(); // avgCustomers[0].orders is 300 @@ -297,8 +440,8 @@ const avgCustomers = await db.Customer.include('orders', (orders) => orders.avg( A customer with no orders reduces to `null`: -```ts -const rows = await db.Customer.include('orders', (orders) => orders.sum('amount')) +```typescript +const rows = await db.orm.public.Customer.include('orders', (orders) => orders.sum('amount')) .where({ id: emptyCustomerId }) .all(); // rows[0].orders is null @@ -306,8 +449,8 @@ const rows = await db.Customer.include('orders', (orders) => orders.sum('amount' ##### `combine()` multiple sub-views (PostgreSQL) -```ts -const users = await db.User.include('posts', (posts) => +```typescript +const users = await db.orm.public.User.include('posts', (posts) => posts.combine({ recent: posts.orderBy((p) => p.createdAt.desc()).take(1), total: posts.count(), @@ -343,38 +486,26 @@ Sort the result set. | Return type | Example | Description | |---|---|---| -| `Collection` | `db.Post.orderBy(...)` | A collection with an ordering applied. | +| `Collection` | `db.orm.public.Post.orderBy(...)` | A collection with an ordering applied. | #### Examples ##### Ascending and descending - - - - -```ts -const newestFirst = await db.Post.where({ userId: aliceId }) +```typescript tab="PostgreSQL" +const newestFirst = await db.orm.public.Post.where({ userId: aliceId }) .orderBy((p) => p.createdAt.desc()) .all(); ``` - - - - -```ts -const newestFirst = await orm.posts.orderBy({ createdAt: -1 }).all(); +```typescript tab="MongoDB" +const newestFirst = await db.orm.posts.orderBy({ createdAt: -1 }).all(); ``` - - - - ##### Multiple sort keys (PostgreSQL) -```ts -const byPriorityThenDate = await db.Post.orderBy([ +```typescript +const byPriorityThenDate = await db.orm.public.Post.orderBy([ (p) => p.priority.asc(), (p) => p.createdAt.asc(), ]).all(); @@ -399,32 +530,20 @@ Limit the number of returned rows. | Return type | Example | Description | |---|---|---| -| `Collection` | `db.Post.take(2)` | A collection limited to `count` rows. | +| `Collection` | `db.orm.public.Post.take(2)` | A collection limited to `count` rows. | #### Examples ##### Limit the result set - - - - -```ts -const firstTwo = await db.Post.orderBy((p) => p.createdAt.asc()).take(2).all(); +```typescript tab="PostgreSQL" +const firstTwo = await db.orm.public.Post.orderBy((p) => p.createdAt.asc()).take(2).all(); ``` - - - - -```ts -const firstOne = await orm.posts.orderBy({ createdAt: 1 }).take(1).all(); +```typescript tab="MongoDB" +const firstOne = await db.orm.posts.orderBy({ createdAt: 1 }).take(1).all(); ``` - - - - ### `skip()` Offset into the ordered result set. @@ -444,32 +563,20 @@ Offset into the ordered result set. | Return type | Example | Description | |---|---|---| -| `Collection` | `db.Post.skip(2)` | A collection offset by `count` rows. | +| `Collection` | `db.orm.public.Post.skip(2)` | A collection offset by `count` rows. | #### Examples ##### Offset into the result set - - - - -```ts -const page2 = await db.Post.orderBy((p) => p.createdAt.asc()).skip(2).take(2).all(); +```typescript tab="PostgreSQL" +const page2 = await db.orm.public.Post.orderBy((p) => p.createdAt.asc()).skip(2).take(2).all(); ``` - - - - -```ts -const secondPost = await orm.posts.orderBy({ createdAt: 1 }).skip(1).take(1).all(); +```typescript tab="MongoDB" +const secondPost = await db.orm.posts.orderBy({ createdAt: 1 }).skip(1).take(1).all(); ``` - - - - ### `cursor()` Resume pagination from a known position. @@ -489,17 +596,17 @@ Resume pagination from a known position. | Return type | Example | Description | |---|---|---| -| `Collection` | `db.Post.cursor({ createdAt })` | A collection resuming after the cursor position. | +| `Collection` | `db.orm.public.Post.cursor({ createdAt })` | A collection resuming after the cursor position. | #### Examples ##### Resume pagination (PostgreSQL) -```ts -const page1 = await db.Post.orderBy((p) => p.createdAt.asc()).take(2).all(); +```typescript +const page1 = await db.orm.public.Post.orderBy((p) => p.createdAt.asc()).take(2).all(); const last = page1[page1.length - 1]; -const page2 = await db.Post.orderBy((p) => p.createdAt.asc()) +const page2 = await db.orm.public.Post.orderBy((p) => p.createdAt.asc()) .cursor({ createdAt: last.createdAt }) .take(2) .all(); @@ -523,14 +630,14 @@ Emit `SELECT DISTINCT` on the given fields. | Return type | Example | Description | |---|---|---| -| `Collection` | `db.Post.distinct('priority')` | A collection with duplicate rows removed on the named fields. | +| `Collection` | `db.orm.public.Post.distinct('priority')` | A collection with duplicate rows removed on the named fields. | #### Examples ##### Deduplicate on a field (PostgreSQL) -```ts -const priorities = await db.Post.select('priority').distinct('priority').all(); +```typescript +const priorities = await db.orm.public.Post.select('priority').distinct('priority').all(); ``` ### `distinctOn()` @@ -552,14 +659,14 @@ Keep the first row per key according to `orderBy()`. | Return type | Example | Description | |---|---|---| -| `Collection` | `db.Post.distinctOn('userId')` | A collection keeping one row per key. | +| `Collection` | `db.orm.public.Post.distinctOn('userId')` | A collection keeping one row per key. | #### Examples ##### First row per key (PostgreSQL) -```ts -const latestPerUser = await db.Post.orderBy([(p) => p.userId.asc(), (p) => p.createdAt.desc()]) +```typescript +const latestPerUser = await db.orm.public.Post.orderBy([(p) => p.userId.asc(), (p) => p.createdAt.desc()]) .distinctOn('userId') .all(); ``` @@ -584,35 +691,23 @@ Narrow a polymorphic (PostgreSQL) or discriminated (MongoDB) model to one varian | Return type | Example | Description | |---|---|---| -| `Collection` (variant-narrowed) | `db.Task.variant('Bug')` | A collection scoped to the variant. | +| `Collection` (variant-narrowed) | `db.orm.public.Task.variant('Bug')` | A collection scoped to the variant. | #### Examples ##### Narrow to a variant - - - - -```ts -const bugs = await db.Task.variant('Bug').all(); +```typescript tab="PostgreSQL" +const bugs = await db.orm.public.Task.variant('Bug').all(); ``` - - - - -```ts -const tutorials = await orm.posts.variant('Tutorial').all(); +```typescript tab="MongoDB" +const tutorials = await db.orm.posts.variant('Tutorial').all(); ``` - - - - ## Read terminals -Read terminals resolve a query. `all()` and `first()` are available on both databases; `aggregate()` and `groupBy()` are PostgreSQL only and are documented under [Grouped aggregates](#grouped-aggregates). +Read terminals resolve a query. `all()` and `first()` are available on both databases; `aggregate()` and `groupBy()` are PostgreSQL only and are documented under [Grouped aggregates](#grouped-aggregates). For a task-oriented walkthrough, see [Reading data](/orm/next/fundamentals/reading-data). ### `all()` @@ -633,57 +728,40 @@ Resolve the query to every matching row. | Return type | Example | Description | |---|---|---| -| `AsyncIterableResult` | `await db.User.all()` | Awaitable to `Row[]`, or iterable with `for await` for streaming. | +| `AsyncIterableResult` | `await db.orm.public.User.all()` | Awaitable to `Row[]`, or iterable with `for await` for streaming. | #### Examples ##### Await to collect an array - - - - -```ts -const users = await db.User.all(); +```typescript tab="PostgreSQL" +const users = await db.orm.public.User.all(); ``` - - - - -```ts -const users = await orm.users.all(); +```typescript tab="MongoDB" +const users = await db.orm.users.all(); ``` - - - - ##### Stream rows one at a time - - - - -```ts -for await (const user of db.User.orderBy((u) => u.email.asc()).all()) { +```typescript tab="PostgreSQL" +for await (const user of db.orm.public.User.orderBy((u) => u.email.asc()).all()) { console.log(user.email); } ``` - - - - -```ts -for await (const post of orm.posts.orderBy({ createdAt: 1 }).all()) { +```typescript tab="MongoDB" +for await (const post of db.orm.posts.orderBy({ createdAt: 1 }).all()) { console.log(post.title); } ``` - +For Prisma 7 users, `findMany` maps onto `all()`: - +```diff +- const users = await prisma.user.findMany({ where: { kind: 'admin' } }); ++ const users = await db.orm.public.User.where({ kind: 'admin' }).all(); +``` ### `first()` @@ -706,46 +784,41 @@ Resolve the query to the first matching row, or `null` if none matches. | Return type | Example | Description | |---|---|---| -| `Row \| null` | `await db.User.first(...)` | The first matching row, or `null`. | +| `Row \| null` | `await db.orm.public.User.first(...)` | The first matching row, or `null`. | #### Examples ##### Match by an inline filter (PostgreSQL) -```ts -const alice = await db.User.first({ email: 'alice@example.com' }); -const urgentPost = await db.Post.first((p) => p.priority.eq('urgent')); +```typescript +const alice = await db.orm.public.User.first({ email: 'alice@example.com' }); +const urgentPost = await db.orm.public.Post.first((p) => p.priority.eq('urgent')); ``` ##### Match with a prior `where()` (MongoDB) -```ts -const bob = await orm.users.where({ name: 'Bob' }).first(); +```typescript +const bob = await db.orm.users.where({ name: 'Bob' }).first(); ``` ##### No match returns `null` - - - - -```ts -const nobody = await db.User.first({ email: 'nobody@example.com' }); +```typescript tab="PostgreSQL" +const nobody = await db.orm.public.User.first({ email: 'nobody@example.com' }); // null ``` - - - - -```ts -const nobody = await orm.users.where({ email: 'nobody@example.com' }).first(); +```typescript tab="MongoDB" +const nobody = await db.orm.users.where({ email: 'nobody@example.com' }).first(); // null ``` - +For Prisma 7 users, `findUnique` and `findFirst` map onto `first()`: - +```diff +- const alice = await prisma.user.findUnique({ where: { email } }); ++ const alice = await db.orm.public.User.first({ email }); +``` ### Custom `Collection` subclass @@ -754,13 +827,17 @@ On PostgreSQL you can subclass `Collection` to attach domain methods, and regist #### Remarks - PostgreSQL only. The MongoDB ORM has no equivalent collection-subclassing mechanism. +- Registering custom collections requires building the client with the `orm(...)` factory, not the `postgres()` client's built-in `.orm` facet. The `postgres()` client has no `collections` option, so its facet always resolves models to the base `Collection`. Pass a `collections` map to `orm({ runtime, context, collections })` to register subclasses. #### Examples ##### Register a subclass with a domain method (PostgreSQL) -```ts +```typescript +import postgres from '@prisma-next/postgres/runtime'; import { Collection, orm } from '@prisma-next/sql-orm-client'; +import type { Contract } from './contract.d'; +import contractJson from './contract.json' with { type: 'json' }; class TaskCollection extends Collection { bugs() { @@ -771,6 +848,10 @@ class TaskCollection extends Collection { } } +const client = postgres({ contractJson, url: process.env.DATABASE_URL }); +const runtime = await client.connect(); + +// The orm() factory accepts a `collections` map; the client's `.orm` facet does not. const db = orm({ runtime, context: client.context, @@ -783,7 +864,7 @@ const features = await db.Task.features().all(); ## Mutation terminals -Mutations write to the database. `create`, `createAll`, `createCount`, `update`, `updateAll`, `updateCount`, `delete`, `deleteAll`, `deleteCount`, and `upsert` are available on both databases, with several behavioral differences called out per method. +Mutations write to the database. `create`, `createAll`, `createCount`, `update`, `updateAll`, `updateCount`, `delete`, `deleteAll`, `deleteCount`, and `upsert` are available on both databases, with several behavioral differences called out per method. For a task-oriented walkthrough, see [Writing data](/orm/next/fundamentals/writing-data); for grouping several writes into one unit, see [Transactions](/orm/next/fundamentals/transactions). :::warning[`where()` enforcement differs sharply between databases] Always call `where()` before `update`, `updateAll`, `updateCount`, `delete`, `deleteAll`, `deleteCount`, or `upsert`. On **MongoDB** all seven enforce this at runtime: calling them with no filter throws `Error: () requires a .where() filter`. On **PostgreSQL** only `update()` and `delete()` are typed to require a prior `where()`, and even there it is a **type-only** guard with no runtime check. If you bypass the type system, `update()` and `delete()` do not throw and do not mass-mutate; they narrow to a single row by identity (a `SELECT ... LIMIT 1` over the current, possibly empty, filters, then act on that one row). The `*All`/`*Count` variants on PostgreSQL compile with no `WHERE` clause and affect every row. Always call `where()` first. @@ -809,26 +890,18 @@ Insert a single row and return it. | Return type | Example | Description | |---|---|---| -| `Row` | `await db.Tag.create(...)` | The inserted row. | +| `Row` | `await db.orm.public.Tag.create(...)` | The inserted row. | #### Examples ##### Insert a single row - - - - -```ts -const tag = await db.Tag.create({ label: 'typescript-2' }); +```typescript tab="PostgreSQL" +const tag = await db.orm.public.Tag.create({ label: 'typescript-2' }); ``` - - - - -```ts -const user = await orm.users.create({ +```typescript tab="MongoDB" +const user = await db.orm.users.create({ name: 'Carol', email: 'carol@example.com', bio: null, @@ -838,14 +911,10 @@ const user = await orm.users.create({ // user._id is the server-assigned id ``` - - - - ##### Nested `create()` on a child-owned relation (PostgreSQL) -```ts -const author = await db.User.create({ +```typescript +const author = await db.orm.public.User.create({ id: '00000000-0000-0000-0000-000000000099', email: 'dana@example.com', displayName: 'Dana', @@ -857,14 +926,21 @@ const author = await db.User.create({ ##### Nested `connect()` on a parent-owned relation (PostgreSQL) -```ts -const post = await db.Post.create({ +```typescript +const post = await db.orm.public.Post.create({ id: '10000000-0000-0000-0000-000000000098', title: 'Connected to Bob', user: (user) => user.connect({ id: bobId }), }); ``` +For Prisma 7 users, `create()` drops the `data` wrapper: + +```diff +- const tag = await prisma.tag.create({ data: { label: 'typescript-2' } }); ++ const tag = await db.orm.public.Tag.create({ label: 'typescript-2' }); +``` + ### `createAll()` Insert multiple rows and return them. @@ -884,39 +960,27 @@ Insert multiple rows and return them. | Return type | Example | Description | |---|---|---| -| `AsyncIterableResult` | `await db.Tag.createAll([...])` | Awaitable to `Row[]`, or streamable. | +| `AsyncIterableResult` | `await db.orm.public.Tag.createAll([...])` | Awaitable to `Row[]`, or streamable. | #### Examples ##### Insert and collect - - - - -```ts -const created = await db.Tag.createAll([{ label: 'alpha' }, { label: 'beta' }]); +```typescript tab="PostgreSQL" +const created = await db.orm.public.Tag.createAll([{ label: 'alpha' }, { label: 'beta' }]); ``` - - - - -```ts -const created = await orm.users.createAll([ +```typescript tab="MongoDB" +const created = await db.orm.users.createAll([ { name: 'Dana', email: 'dana@example.com', bio: null, role: 'author', address: null }, { name: 'Eve', email: 'eve@example.com', bio: null, role: 'reader', address: null }, ]); ``` - - - - ##### Stream inserted rows (PostgreSQL) -```ts -for await (const tag of db.Tag.createAll([{ label: 'gamma' }, { label: 'delta' }])) { +```typescript +for await (const tag of db.orm.public.Tag.createAll([{ label: 'gamma' }, { label: 'delta' }])) { console.log(tag.label); } ``` @@ -941,27 +1005,19 @@ Insert rows without materializing them, returning the count. | Return type | Example | Description | |---|---|---| -| `number` | `await db.Tag.createCount([...])` | The count of inserted rows. | +| `number` | `await db.orm.public.Tag.createCount([...])` | The count of inserted rows. | #### Examples ##### Insert and count - - - - -```ts -const inserted = await db.Tag.createCount([{ label: 'epsilon' }, { label: 'zeta' }]); +```typescript tab="PostgreSQL" +const inserted = await db.orm.public.Tag.createCount([{ label: 'epsilon' }, { label: 'zeta' }]); // 2 ``` - - - - -```ts -const inserted = await orm.posts.variant('Tutorial').createCount([ +```typescript tab="MongoDB" +const inserted = await db.orm.posts.variant('Tutorial').createCount([ { title: 'Variant createCount', content: 'body', @@ -974,10 +1030,6 @@ const inserted = await orm.posts.variant('Tutorial').createCount([ // 1 — no split-table restriction on Mongo variants ``` - - - - ### `update()` Update the matched row and return it, or `null` if none matches. @@ -1001,36 +1053,24 @@ Update the matched row and return it, or `null` if none matches. | Return type | Example | Description | |---|---|---| -| `Row \| null` | `await db.User.where(...).update(...)` | The updated row, or `null` if none matched. | +| `Row \| null` | `await db.orm.public.User.where(...).update(...)` | The updated row, or `null` if none matched. | #### Examples ##### Update with a data object - - - - -```ts -const updated = await db.User.where({ id: bobId }).update({ displayName: 'Bob Updated' }); +```typescript tab="PostgreSQL" +const updated = await db.orm.public.User.where({ id: bobId }).update({ displayName: 'Bob Updated' }); ``` - - - - -```ts -const updated = await orm.users.where({ _id: bobId }).update({ bio: 'Now with a bio' }); +```typescript tab="MongoDB" +const updated = await db.orm.users.where({ _id: bobId }).update({ bio: 'Now with a bio' }); ``` - - - - ##### Field-operations callback (MongoDB) -```ts -const updated = await orm.posts +```typescript +const updated = await db.orm.posts .variant('Tutorial') .where({ _id: tutorialId }) .update((t) => [t.duration.inc(5), t.content.set('Updated content')]); @@ -1038,16 +1078,16 @@ const updated = await orm.posts ##### Nested `connect()` relinks a foreign key (PostgreSQL) -```ts -const relinked = await db.Post.where({ id: postId }).update({ +```typescript +const relinked = await db.orm.public.Post.where({ id: postId }).update({ user: (user) => user.connect({ id: carolId }), }); ``` ##### Nested `disconnect()` unlinks a many-to-many row (PostgreSQL) -```ts -const updated = await db.Post.where({ id: postId }) +```typescript +const updated = await db.orm.public.Post.where({ id: postId }) .select('id', 'title') .include('tags', (tag) => tag.select('id', 'label').orderBy((t) => t.label.asc())) .update({ @@ -1056,6 +1096,13 @@ const updated = await db.Post.where({ id: postId }) // only the junction row is removed; the Tag row itself still exists ``` +For Prisma 7 users, `update()` moves the filter into `where()` and drops the `data` wrapper: + +```diff +- const updated = await prisma.user.update({ where: { id: bobId }, data: { displayName: 'Bob Updated' } }); ++ const updated = await db.orm.public.User.where({ id: bobId }).update({ displayName: 'Bob Updated' }); +``` + ### `updateAll()` Update every matching row and collect the results. @@ -1076,33 +1123,21 @@ Update every matching row and collect the results. | Return type | Example | Description | |---|---|---| -| `AsyncIterableResult` | `await db.Post.where(...).updateAll(...)` | The updated rows. | +| `AsyncIterableResult` | `await db.orm.public.Post.where(...).updateAll(...)` | The updated rows. | #### Examples ##### Update all matching rows - - - - -```ts -const updated = await db.Post.where({ userId: aliceId }).updateAll({ priority: 'urgent' }); +```typescript tab="PostgreSQL" +const updated = await db.orm.public.Post.where({ userId: aliceId }).updateAll({ priority: 'urgent' }); ``` - - - - -```ts -const updated = await orm.users.where({ role: 'author' }).updateAll({ role: 'admin' }); +```typescript tab="MongoDB" +const updated = await db.orm.users.where({ role: 'author' }).updateAll({ role: 'admin' }); // non-atomic: ids are captured, updated, then re-read ``` - - - - ### `updateCount()` Update every matching row and return the count. @@ -1122,36 +1157,24 @@ Update every matching row and return the count. | Return type | Example | Description | |---|---|---| -| `number` | `await db.Post.where(...).updateCount(...)` | The count of updated rows. | +| `number` | `await db.orm.public.Post.where(...).updateCount(...)` | The count of updated rows. | #### Examples ##### Update and count - - - - -```ts -const count = await db.Post.where({ userId: carolId }).updateCount({ priority: 'low' }); +```typescript tab="PostgreSQL" +const count = await db.orm.public.Post.where({ userId: carolId }).updateCount({ priority: 'low' }); ``` - - - - -```ts -const count = await orm.users.where({ role: 'author' }).updateCount({ role: 'admin' }); +```typescript tab="MongoDB" +const count = await db.orm.users.where({ role: 'author' }).updateCount({ role: 'admin' }); ``` - - - - ##### Field-operations callback (MongoDB) -```ts -const count = await orm.posts +```typescript +const count = await db.orm.posts .variant('Tutorial') .where({ _id: tutorialId }) .updateCount((t) => [t.duration.mul(2)]); @@ -1175,32 +1198,27 @@ Remove the matched row and return it, or `null` if none matches. | Return type | Example | Description | |---|---|---| -| `Row \| null` | `await db.Tag.where(...).delete()` | The deleted row, or `null` if none matched. | +| `Row \| null` | `await db.orm.public.Tag.where(...).delete()` | The deleted row, or `null` if none matched. | #### Examples ##### Delete a row - - - - -```ts -const created = await db.Tag.create({ label: 'throwaway' }); -const deleted = await db.Tag.where({ id: created.id }).delete(); +```typescript tab="PostgreSQL" +const created = await db.orm.public.Tag.create({ label: 'throwaway' }); +const deleted = await db.orm.public.Tag.where({ id: created.id }).delete(); ``` - - - - -```ts -const deleted = await orm.users.where({ _id: userId }).delete(); +```typescript tab="MongoDB" +const deleted = await db.orm.users.where({ _id: userId }).delete(); ``` - +For Prisma 7 users, `delete()` moves the filter into `where()`: - +```diff +- const deleted = await prisma.tag.delete({ where: { id } }); ++ const deleted = await db.orm.public.Tag.where({ id }).delete(); +``` ### `deleteAll()` @@ -1218,32 +1236,20 @@ Remove every matching row and collect the results. | Return type | Example | Description | |---|---|---| -| `AsyncIterableResult` | `await db.Post.where(...).deleteAll()` | The deleted rows. | +| `AsyncIterableResult` | `await db.orm.public.Post.where(...).deleteAll()` | The deleted rows. | #### Examples ##### Delete all matching rows - - - - -```ts -const deleted = await db.Post.where({ userId: carolId }).deleteAll(); +```typescript tab="PostgreSQL" +const deleted = await db.orm.public.Post.where({ userId: carolId }).deleteAll(); ``` - - - - -```ts -const deleted = await orm.users.where({ role: 'reader' }).deleteAll(); +```typescript tab="MongoDB" +const deleted = await db.orm.users.where({ role: 'reader' }).deleteAll(); ``` - - - - ### `deleteCount()` Remove every matching row and return the count. @@ -1261,32 +1267,20 @@ Remove every matching row and return the count. | Return type | Example | Description | |---|---|---| -| `number` | `await db.Post.where(...).deleteCount()` | The count of deleted rows. | +| `number` | `await db.orm.public.Post.where(...).deleteCount()` | The count of deleted rows. | #### Examples ##### Delete and count - - - - -```ts -const count = await db.Post.where({ userId: carolId }).deleteCount(); +```typescript tab="PostgreSQL" +const count = await db.orm.public.Post.where({ userId: carolId }).deleteCount(); ``` - - - - -```ts -const count = await orm.users.where({ role: 'reader' }).deleteCount(); +```typescript tab="MongoDB" +const count = await db.orm.users.where({ role: 'reader' }).deleteCount(); ``` - - - - ### `upsert()` Insert a row if none matches, otherwise update the existing row. @@ -1311,22 +1305,22 @@ Insert a row if none matches, otherwise update the existing row. | Return type | Example | Description | |---|---|---| -| `Row` | `await db.Tag.upsert(...)` | The inserted or updated row. | +| `Row` | `await db.orm.public.Tag.upsert(...)` | The inserted or updated row. | #### Examples ##### Insert or update (PostgreSQL) -```ts +```typescript // Insert path: no existing row has label 'brand-new', so the create side wins. -const inserted = await db.Tag.upsert({ +const inserted = await db.orm.public.Tag.upsert({ create: { id: '30000000-0000-0000-0000-000000000099', label: 'brand-new' }, update: { label: 'brand-new-updated' }, conflictOn: { label: 'brand-new' }, }); // Update path: 'typescript' already exists, so the update runs against it. -const updated = await db.Tag.upsert({ +const updated = await db.orm.public.Tag.upsert({ create: { id: '30000000-0000-0000-0000-000000000098', label: 'typescript' }, update: { label: 'typescript-renamed' }, conflictOn: { label: 'typescript' }, @@ -1335,8 +1329,8 @@ const updated = await db.Tag.upsert({ ##### Insert or update (MongoDB) -```ts -const user = await orm.users.where({ email: 'newperson@example.com' }).upsert({ +```typescript +const user = await db.orm.users.where({ email: 'newperson@example.com' }).upsert({ create: { name: 'New Person', email: 'newperson@example.com', @@ -1351,8 +1345,8 @@ const user = await orm.users.where({ email: 'newperson@example.com' }).upsert({ ##### Field-operations callback on the update side (MongoDB) -```ts -const post = await orm.posts +```typescript +const post = await db.orm.posts .variant('Tutorial') .where({ _id: tutorialId }) .upsert({ @@ -1368,9 +1362,60 @@ const post = await orm.posts }); ``` +For Prisma 7 users, `upsert()` replaces the `where` conflict target with `conflictOn` (PostgreSQL): + +```diff +- const tag = await prisma.tag.upsert({ +- where: { label: 'typescript' }, +- update: { label: 'typescript-renamed' }, +- create: { label: 'typescript' }, +- }); ++ const tag = await db.orm.public.Tag.upsert({ ++ update: { label: 'typescript-renamed' }, ++ create: { id, label: 'typescript' }, ++ conflictOn: { label: 'typescript' }, ++ }); +``` + ## Grouped aggregates -PostgreSQL supports aggregation over a result set, both flat (`aggregate()`) and grouped (`groupBy().aggregate()`), with `having()` to filter groups. MongoDB has no equivalent: `aggregate()` and `groupBy()` do not exist on the Mongo collection and calling them throws `TypeError`. The examples below use a `Customer` / `Order` schema where `Order.amount` is a numeric column. +PostgreSQL supports aggregation over a result set, both flat (`aggregate()`) and grouped (`groupBy().aggregate()`), with `having()` to filter groups. MongoDB has no equivalent: `aggregate()` and `groupBy()` do not exist on the Mongo collection and calling them throws `TypeError`. + +The examples below use a separate `Customer` / `Order` schema, where `Order.amount` is a numeric column: + +
+ +Expand for the aggregate example schema + +```prisma +types { + Uuid = String @db.Uuid +} + +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") +} +``` + +
### `aggregate()` @@ -1400,15 +1445,15 @@ The selector's `agg` exposes `count()`, `sum(field)`, `avg(field)`, `min(field)` ##### Count (PostgreSQL) -```ts -const stats = await db.Order.aggregate((agg) => ({ total: agg.count() })); +```typescript +const stats = await db.orm.public.Order.aggregate((agg) => ({ total: agg.count() })); // { total: 10 } ``` ##### Sum and average (PostgreSQL) -```ts -const stats = await db.Order.where({ customerId: acmeId }).aggregate((agg) => ({ +```typescript +const stats = await db.orm.public.Order.where({ customerId: acmeId }).aggregate((agg) => ({ totalAmount: agg.sum('amount'), avgAmount: agg.avg('amount'), })); @@ -1417,8 +1462,8 @@ const stats = await db.Order.where({ customerId: acmeId }).aggregate((agg) => ({ ##### Min and max (PostgreSQL) -```ts -const stats = await db.Order.aggregate((agg) => ({ +```typescript +const stats = await db.orm.public.Order.aggregate((agg) => ({ cheapest: agg.min('amount'), priciest: agg.max('amount'), })); @@ -1427,8 +1472,8 @@ const stats = await db.Order.aggregate((agg) => ({ ##### `null` over an empty set (PostgreSQL) -```ts -const stats = await db.Order.where((o) => o.amount.gt(999_999)).aggregate((agg) => ({ +```typescript +const stats = await db.orm.public.Order.where((o) => o.amount.gt(999_999)).aggregate((agg) => ({ total: agg.sum('amount'), average: agg.avg('amount'), count: agg.count(), @@ -1436,6 +1481,13 @@ const stats = await db.Order.where((o) => o.amount.gt(999_999)).aggregate((agg) // { total: null, average: null, count: 0 } ``` +For Prisma 7 users, `aggregate()` takes a selector callback instead of `_sum`/`_avg` keys: + +```diff +- const stats = await prisma.order.aggregate({ _sum: { amount: true }, _avg: { amount: true } }); ++ const stats = await db.orm.public.Order.aggregate((agg) => ({ total: agg.sum('amount'), average: agg.avg('amount') })); +``` + ### `groupBy()` Group rows by one or more fields, then aggregate per group. @@ -1455,20 +1507,27 @@ Group rows by one or more fields, then aggregate per group. | Return type | Example | Description | |---|---|---| -| `GroupedCollection` | `db.Order.groupBy('customerId')` | A grouped collection, resolved via `aggregate()`. | +| `GroupedCollection` | `db.orm.public.Order.groupBy('customerId')` | A grouped collection, resolved via `aggregate()`. | #### Examples ##### Group and aggregate (PostgreSQL) -```ts -const perCustomer = await db.Order.groupBy('customerId').aggregate((agg) => ({ +```typescript +const perCustomer = await db.orm.public.Order.groupBy('customerId').aggregate((agg) => ({ orderCount: agg.count(), totalAmount: agg.sum('amount'), })); // one row per customer, each { customerId, orderCount, totalAmount } ``` +For Prisma 7 users, `groupBy()` chains into `aggregate()` instead of taking a `by` array with aggregate keys: + +```diff +- const perCustomer = await prisma.order.groupBy({ by: ['customerId'], _sum: { amount: true } }); ++ const perCustomer = await db.orm.public.Order.groupBy('customerId').aggregate((agg) => ({ totalAmount: agg.sum('amount') })); +``` + ### `having()` Filter groups by an aggregate comparison. @@ -1488,22 +1547,22 @@ Filter groups by an aggregate comparison. | Return type | Example | Description | |---|---|---| -| `GroupedCollection` | `db.Order.groupBy('customerId').having(...)` | A grouped collection filtered by the aggregate predicate. | +| `GroupedCollection` | `db.orm.public.Order.groupBy('customerId').having(...)` | A grouped collection filtered by the aggregate predicate. | #### Examples ##### Filter groups by a sum (PostgreSQL) -```ts -const bigSpenders = await db.Order.groupBy('customerId') +```typescript +const bigSpenders = await db.orm.public.Order.groupBy('customerId') .having((h) => h.sum('amount').gt(1000)) .aggregate((agg) => ({ totalAmount: agg.sum('amount') })); ``` ##### Filter groups by row count (PostgreSQL) -```ts -const groupsWithAtLeastFive = await db.Order.groupBy('customerId') +```typescript +const groupsWithAtLeastFive = await db.orm.public.Order.groupBy('customerId') .having((h) => h.count().gte(5)) .aggregate((agg) => ({ orderCount: agg.count() })); ``` @@ -1539,31 +1598,31 @@ Column-level comparison methods on a field accessor. ##### Equality and inequality (PostgreSQL) -```ts -const alice = await db.User.where((u) => u.email.eq('alice@example.com')).first(); -const notAlice = await db.User.where((u) => u.email.neq('alice@example.com')).all(); +```typescript +const alice = await db.orm.public.User.where((u) => u.email.eq('alice@example.com')).first(); +const notAlice = await db.orm.public.User.where((u) => u.email.neq('alice@example.com')).all(); ``` ##### Ordered comparisons (PostgreSQL) -```ts -const after = await db.Post.where((p) => p.createdAt.gt(new Date('2024-01-02T10:00:00.000Z'))).all(); -const inclusive = await db.Post.where((p) => p.createdAt.gte(new Date('2024-01-02T10:00:00.000Z'))).all(); +```typescript +const after = await db.orm.public.Post.where((p) => p.createdAt.gt(new Date('2024-01-02T10:00:00.000Z'))).all(); +const inclusive = await db.orm.public.Post.where((p) => p.createdAt.gte(new Date('2024-01-02T10:00:00.000Z'))).all(); ``` ##### Pattern matching (PostgreSQL) -```ts -const matches = await db.User.where((u) => u.email.like('%@example.com')).all(); // case-sensitive -const caseInsensitive = await db.User.where((u) => u.email.ilike('%@EXAMPLE.COM')).all(); +```typescript +const matches = await db.orm.public.User.where((u) => u.email.like('%@example.com')).all(); // case-sensitive +const caseInsensitive = await db.orm.public.User.where((u) => u.email.ilike('%@EXAMPLE.COM')).all(); ``` ##### Membership and NULL checks (PostgreSQL) -```ts -const lowOrHigh = await db.Post.where((p) => p.priority.in(['low', 'high'])).all(); -const notLowOrHigh = await db.Post.where((p) => p.priority.notIn(['low', 'high'])).all(); -const withoutEmbedding = await db.Post.where((p) => p.embedding.isNull()).all(); +```typescript +const lowOrHigh = await db.orm.public.Post.where((p) => p.priority.in(['low', 'high'])).all(); +const notLowOrHigh = await db.orm.public.Post.where((p) => p.priority.notIn(['low', 'high'])).all(); +const withoutEmbedding = await db.orm.public.Post.where((p) => p.embedding.isNull()).all(); ``` ### Combinators @@ -1580,26 +1639,26 @@ Combine or negate predicates. ##### `and` / `or` / `not` (PostgreSQL) -```ts +```typescript import { and, or, not } from '@prisma-next/sql-orm-client'; -const both = await db.Post.where((p) => and(p.priority.eq('low'), p.userId.eq(carolId))).all(); -const either = await db.Post.where((p) => or(p.priority.eq('urgent'), p.priority.eq('high'))).all(); -const negated = await db.Post.where((p) => not(p.priority.eq('low'))).all(); +const both = await db.orm.public.Post.where((p) => and(p.priority.eq('low'), p.userId.eq(carolId))).all(); +const either = await db.orm.public.Post.where((p) => or(p.priority.eq('urgent'), p.priority.eq('high'))).all(); +const negated = await db.orm.public.Post.where((p) => not(p.priority.eq('low'))).all(); ``` ##### `and` / `or` / `not` (MongoDB) -```ts +```typescript import { MongoFieldFilter, MongoOrExpr } from '@prisma-next/mongo-query-ast/execution'; -const both = await orm.users +const both = await db.orm.users .where(MongoFieldFilter.eq('role', 'author').and(MongoFieldFilter.eq('name', 'Alice'))) .all(); -const either = await orm.users +const either = await db.orm.users .where(MongoOrExpr.of([MongoFieldFilter.eq('name', 'Alice'), MongoFieldFilter.eq('name', 'Bob')])) .all(); -const negated = await orm.users.where(MongoFieldFilter.eq('name', 'Alice').not()).all(); +const negated = await db.orm.users.where(MongoFieldFilter.eq('name', 'Alice').not()).all(); ``` :::note[No `.or()` instance method or `$nor` on MongoDB] @@ -1621,17 +1680,17 @@ Filter parents by their related rows. ##### `some` / `every` / `none` (PostgreSQL) -```ts -const withUrgentPost = await db.User.where((u) => u.posts.some((p) => p.priority.eq('urgent'))).all(); -const withAnyTag = await db.Tag.where((t) => t.posts.some()).all(); -const allLowPriority = await db.User.where((u) => u.posts.every((p) => p.priority.eq('low'))).all(); -const noUrgentPost = await db.User.where((u) => u.posts.none((p) => p.priority.eq('urgent'))).all(); +```typescript +const withUrgentPost = await db.orm.public.User.where((u) => u.posts.some((p) => p.priority.eq('urgent'))).all(); +const withAnyTag = await db.orm.public.Tag.where((t) => t.posts.some()).all(); +const allLowPriority = await db.orm.public.User.where((u) => u.posts.every((p) => p.priority.eq('low'))).all(); +const noUrgentPost = await db.orm.public.User.where((u) => u.posts.none((p) => p.priority.eq('urgent'))).all(); ``` ##### To-one relation predicate (PostgreSQL) -```ts -const postsByAlice = await db.Post.where((p) => p.user.some({ email: 'alice@example.com' })).all(); +```typescript +const postsByAlice = await db.orm.public.Post.where((p) => p.user.some({ email: 'alice@example.com' })).all(); ``` ### Shorthand object filter @@ -1647,26 +1706,14 @@ A plain object of equality matches, ANDed together. ##### Shorthand object - - - - -```ts -const row = await db.Post.where({ userId: aliceId, priority: 'high' }).first(); +```typescript tab="PostgreSQL" +const row = await db.orm.public.Post.where({ userId: aliceId, priority: 'high' }).first(); ``` - - - - -```ts -const alice = await orm.users.where({ name: 'Alice', role: 'author' }).first(); +```typescript tab="MongoDB" +const alice = await db.orm.users.where({ name: 'Alice', role: 'author' }).first(); ``` - - - - ### `MongoFieldFilter` Static factories that build MongoDB filter expressions. @@ -1691,25 +1738,25 @@ Static factories that build MongoDB filter expressions. ##### Comparison factories (MongoDB) -```ts +```typescript import { MongoFieldFilter } from '@prisma-next/mongo-query-ast/execution'; -const alice = await orm.users.where(MongoFieldFilter.eq('name', 'Alice')).first(); -const notAlice = await orm.users.where(MongoFieldFilter.neq('name', 'Alice')).all(); -const strictlyAfter = await orm.posts +const alice = await db.orm.users.where(MongoFieldFilter.eq('name', 'Alice')).first(); +const notAlice = await db.orm.users.where(MongoFieldFilter.neq('name', 'Alice')).all(); +const strictlyAfter = await db.orm.posts .where(MongoFieldFilter.gt('createdAt', new Date('2024-01-01T10:00:00.000Z'))) .all(); ``` ##### Membership and null checks (MongoDB) -```ts -const articlesOrTutorials = await orm.posts +```typescript +const articlesOrTutorials = await db.orm.posts .where(MongoFieldFilter.in('kind', ['article', 'tutorial'])) .all(); -const notArticles = await orm.posts.where(MongoFieldFilter.nin('kind', ['article'])).all(); -const noBio = await orm.users.where(MongoFieldFilter.isNull('bio')).all(); -const hasBio = await orm.users.where(MongoFieldFilter.isNotNull('bio')).all(); +const notArticles = await db.orm.posts.where(MongoFieldFilter.nin('kind', ['article'])).all(); +const noBio = await db.orm.users.where(MongoFieldFilter.isNull('bio')).all(); +const hasBio = await db.orm.users.where(MongoFieldFilter.isNotNull('bio')).all(); ``` ### Dot-notation into an embedded object (MongoDB) @@ -1726,17 +1773,17 @@ Filter into a field of an embedded value object using a dot path. ##### Dot-notation path (MongoDB) -```ts +```typescript import { MongoFieldFilter } from '@prisma-next/mongo-query-ast/execution'; // Type-clean form: MongoFieldFilter takes a bare string path. -const usersInSf = await orm.users.where(MongoFieldFilter.eq('address.city', 'San Francisco')).all(); +const usersInSf = await db.orm.users.where(MongoFieldFilter.eq('address.city', 'San Francisco')).all(); ``` The object-shorthand form works at runtime but requires a cast, because the embedded path is not in the model's declared field keys: -```ts -const usersInSf = await orm.users +```typescript +const usersInSf = await db.orm.users .where({ 'address.city': 'San Francisco' } as unknown as Record) .all(); ``` @@ -1761,21 +1808,21 @@ MongoDB field operations are accessed through a field accessor inside `update()` ##### `set()` (MongoDB) -```ts -const updated = await orm.users +```typescript +const updated = await db.orm.users .where({ _id: bobId }) .update((u) => [u.bio.set('Set via field op')]); ``` ##### `inc()` and `mul()` (MongoDB) -```ts -const incremented = await orm.posts +```typescript +const incremented = await db.orm.posts .variant('Tutorial') .where({ _id: tutorialId }) .update((t) => [t.duration.inc(10)]); -const multiplied = await orm.posts +const multiplied = await db.orm.posts .variant('Tutorial') .where({ _id: tutorialId }) .update((t) => [t.duration.mul(3)]); @@ -1783,8 +1830,8 @@ const multiplied = await orm.posts ##### `unset()` (MongoDB) -```ts -const updated = await orm.users.where({ _id: aliceId }).update((u) => [u.bio.unset()]); +```typescript +const updated = await db.orm.users.where({ _id: aliceId }).update((u) => [u.bio.unset()]); ``` ### Operations that do not exist (MongoDB) @@ -1820,8 +1867,8 @@ A result is consumed once per mode: - Re-`await`ing (or calling `.toArray()` on) an already-buffered result is **safe**: it returns the cached array, with no re-query and no throw. - **Switching modes** on a consumed result throws `RUNTIME.ITERATOR_CONSUMED` (message: `already been consumed`). Awaiting a result and then iterating it with `for await` (or the reverse) is the failure mode. -```ts -const result = db.User.all(); +```typescript +const result = db.orm.public.User.all(); const first = await result; const again = await result.toArray(); // safe: same cached array From 3aaf136add2aae6aa8c79b1ad283dbeb6711dece Mon Sep 17 00:00:00 2001 From: Nurul Sundarani Date: Tue, 7 Jul 2026 17:49:34 +0530 Subject: [PATCH 07/15] docs(next): schema-first SQL query builder reference Co-Authored-By: Claude Fable 5 --- .../orm/next/reference/sql-query-builder.mdx | 293 ++++++++++++++---- 1 file changed, 226 insertions(+), 67 deletions(-) diff --git a/apps/docs/content/docs/orm/next/reference/sql-query-builder.mdx b/apps/docs/content/docs/orm/next/reference/sql-query-builder.mdx index 358e0888cf..993b4b6e84 100644 --- a/apps/docs/content/docs/orm/next/reference/sql-query-builder.mdx +++ b/apps/docs/content/docs/orm/next/reference/sql-query-builder.mdx @@ -9,39 +9,162 @@ badge: early-access 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/next/reference/orm-client). Reach for it when you need a join, aggregate, or SQL feature the ORM client doesn't expose, or when you want direct control over the generated SQL. -This page documents every method, its options, and its return type. Where a method is capability-gated or has a runtime caveat, the Remarks call that out. For a one-screen summary of availability, see the [Prisma Next API reference](/orm/next/reference). - -Every code example on this page is transcribed from a test that runs against a live PostgreSQL database. Select and mutation examples use a `user` / `post` / `post_tag` / `tag` schema; grouped-query examples use a `customer` / `order` schema whose `order` table has numeric `amount` and `quantity` columns. +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/next/fundamentals/advanced-queries). :::note[This is the SQL-family builder] The SQL query builder targets SQL databases. PostgreSQL is supported today, with SQLite next on deck. MongoDB has no SQL builder: use the [ORM client](/orm/next/reference/orm-client) for MongoDB queries today, and the MongoDB pipeline builder (planned) 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). + +
+ +Expand for the example schema + +```prisma +types { + Embedding1536 = pgvector.Vector(1536) + Uuid = String @db.Uuid +} + +type Address { + street String + city String + zip String? + country String +} + +enum user_type { + @@type("pg/text@1") + admin + user +} + +enum Priority { + @@type("pg/text@1") + Low = "low" + High = "high" + Urgent = "urgent" +} + +model User { + id Uuid @id @default(uuid()) + email String + displayName String + createdAt DateTime @default(now()) + kind user_type + address Address? + posts Post[] + tasks Task[] + + @@map("user") +} + +model Post { + id Uuid @id @default(uuid()) + title String + userId Uuid + priority Priority @default(Low) + createdAt DateTime @default(now()) + embedding Embedding1536? + + user User @relation(fields: [userId], references: [id]) + tags Tag[] + + @@map("post") +} + +model Tag { + id Uuid @id @default(uuid()) + label String @unique + + posts Post[] + + @@map("tag") +} + +model PostTag { + postId Uuid + tagId Uuid + + post Post @relation(fields: [postId], references: [id]) + tag Tag @relation(fields: [tagId], references: [id]) + + @@id([postId, tagId]) + @@map("post_tag") +} + +model Task { + id Uuid @id @default(uuid()) + title String + description String? + status String @default("open") + type String + userId Uuid + createdAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id]) + + @@discriminator(type) + @@map("task") +} + +model Bug { + severity String + stepsToRepro String? + @@base(Task, "bug") + @@map("bug") +} + +model Feature { + priority String + targetRelease String? + @@base(Task, "feature") + @@map("feature") +} +``` + +
+ ## Entry points -You build queries from a `sql` root that carries your contract's execution context. Table accessors hang off `.public` and use the contract's **mapped table names** (snake_case), so a `User` model mapped to `users` is reached as `sql.user`, and a `Post`/`Tag` junction mapped to `post_tag` is reached as `sql.post_tag`. +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. -### The `sql()` root +### The `db.sql` facet -Create the root by calling `sql(...)` with your execution `context` and a `rawCodecInferer`. The `context` comes from your database client; the `rawCodecInferer` supplies a fallback codec for raw expressions that have no column to infer one from. +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.execute(...)`. ```ts -import { sql } from '@prisma-next/sql-builder/runtime'; +import postgres from '@prisma-next/postgres/runtime'; +import type { Contract } from './contract.d'; +import contractJson from './contract.json' with { type: 'json' }; -const db = sql({ context, rawCodecInferer: { inferCodec: () => 'pg/text' } }).public; +const db = postgres({ contractJson, url: process.env.DATABASE_URL }); +const runtime = await db.connect(); -const users = await runtime.execute(db.user.select('id', 'email').build()); +const plan = db.sql.public.user.select('id', 'email').build(); +const users = await runtime.execute(plan); ``` -Most projects don't call `sql(...)` inline at every call site. Instead they build the root once and re-expose it on their own database wrapper, so application code reaches tables through a convenience property such as `db.sql.public.user`. Both forms are the same entry style: `db.sql` just holds the result of an internal `sql({ ... }).public` call. This page uses the direct `sql({ ... }).public` form and refers to the root as `sql`. +The facet is built from the `sql()` factory: `db.sql` holds the result of an internal `sql({ context, rawCodecInferer }).public` 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-next/sql-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`. ### Table access -Every table on `.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.execute(...)`. +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.execute(...)`. ```ts -sql.user.select('id', 'email'); // start a SELECT -sql.tag.insert([{ id, label: 'typescript' }]); // start an INSERT +db.sql.public.user.select('id', 'email'); // start a SELECT +db.sql.public.tag.insert([{ id, label: 'typescript' }]); // start an INSERT ``` ### Aliasing a query with `.as()` @@ -49,7 +172,7 @@ sql.tag.insert([{ id, label: 'typescript' }]); // start an INSERT 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 = sql.post +const highPriorityPosts = db.sql.public.post .select('id', 'userId') .where((f, fns) => fns.eq(f.priority, 'high')) .as('hp'); @@ -85,14 +208,14 @@ A computed expression's result type comes from `.returns(...)` on `fns.raw`, or | Return type | Example | Description | |---|---|---| -| `SelectQuery` | `sql.user.select('id', 'email')` | A query projected to the named columns or expressions, chainable and buildable. | +| `SelectQuery` | `db.sql.public.user.select('id', 'email')` | A query projected to the named columns or expressions, chainable and buildable. | #### Examples ##### Project a subset of columns ```ts -const plan = sql.user.select('id', 'email').build(); +const plan = db.sql.public.user.select('id', 'email').build(); const rows = await runtime.execute(plan); // rows[0] is { id, email } — no displayName ``` @@ -100,7 +223,7 @@ const rows = await runtime.execute(plan); ##### Add an aliased computed column ```ts -const plan = sql.user +const plan = db.sql.public.user .select('id', 'displayName') .select('emailLength', (f, fns) => fns.raw`LENGTH(${f.email})`.returns('pg/int4@1')) .where((f, fns) => fns.eq(f.id, aliceId)) @@ -111,7 +234,7 @@ const rows = await runtime.execute(plan); ##### Project multiple computed columns at once ```ts -const plan = sql.user +const plan = db.sql.public.user .select((f, fns) => ({ id: f.id, upperEmail: fns.raw`UPPER(${f.email})`.returns('pg/text@1'), @@ -137,14 +260,14 @@ Restrict a query to rows matching an expression. | Return type | Example | Description | |---|---|---| -| `SelectQuery` | `sql.post.where(...)` | A query narrowed by the predicate. | +| `SelectQuery` | `db.sql.public.post.where(...)` | A query narrowed by the predicate. | #### Examples ##### Combine comparisons with `and()` and `or()` ```ts -const plan = sql.post +const plan = db.sql.public.post .select('id', 'title', 'priority') .where((f, fns) => fns.or( @@ -164,22 +287,22 @@ Combine matching rows from two tables. After a join, address columns with their | Name | Type | Required | Description | |---|---|---|---| -| `other` | A table (`sql.
`) or an aliased subquery ([`.as()`](#aliasing-a-query-with-as)) | Yes | The table or subquery to join. | +| `other` | A table (`db.sql.public.
`) or an aliased subquery ([`.as()`](#aliasing-a-query-with-as)) | Yes | The table or subquery to join. | | `on` | `(f, fns) => Expression` | Yes | The join condition. | #### Return type | Return type | Example | Description | |---|---|---| -| `SelectQuery` | `sql.post.innerJoin(sql.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 namespaces. | #### Examples ##### Join posts to their authors ```ts -const plan = sql.post - .innerJoin(sql.user, (f, fns) => fns.eq(f.post.userId, f.user.id)) +const plan = db.sql.public.post + .innerJoin(db.sql.public.user, (f, fns) => fns.eq(f.post.userId, f.user.id)) .select((f) => ({ postId: f.post.id, authorEmail: f.user.email })) .where((f, fns) => fns.eq(f.post.id, helloWorldId)) .build(); @@ -200,8 +323,8 @@ Keep unmatched rows from one or both sides, filling the missing side's columns w ##### Left join keeps rows with no match ```ts -const plan = sql.post - .outerLeftJoin(sql.post_tag, (f, fns) => fns.eq(f.post.id, f.post_tag.postId)) +const plan = db.sql.public.post + .outerLeftJoin(db.sql.public.post_tag, (f, fns) => fns.eq(f.post.id, f.post_tag.postId)) .select((f) => ({ postId: f.post.id, tagId: f.post_tag.tagId })) .where((f, fns) => fns.eq(f.post.id, untaggedPostId)) .build(); @@ -231,17 +354,17 @@ Correlate a per-row subquery against the outer row: for each outer row, the join | Return type | Example | Description | |---|---|---| -| `SelectQuery` | `sql.user.lateralJoin('latestPost', ...)` | A query with the lateral subquery's columns in scope under `alias`. | +| `SelectQuery` | `db.sql.public.user.lateralJoin('latestPost', ...)` | A query with the lateral subquery's columns in scope under `alias`. | #### Examples ##### Each user's most recent post ```ts -const plan = sql.user +const plan = db.sql.public.user .lateralJoin('latestPost', (lateral) => lateral - .from(sql.post) + .from(db.sql.public.post) .select((f) => ({ id: f.post.id, title: f.post.title })) .where((f, fns) => fns.eq(f.post.userId, f.user.id)) .orderBy((f) => f.post.createdAt, { direction: 'desc' }) @@ -272,21 +395,21 @@ Sort the result set by a column, a computed expression, or with explicit directi | Return type | Example | Description | |---|---|---| -| `SelectQuery` | `sql.user.orderBy('email', ...)` | A query with the sort key applied. Call `orderBy()` again to add secondary keys. | +| `SelectQuery` | `db.sql.public.user.orderBy('email', ...)` | A query with the sort key applied. Call `orderBy()` again to add secondary keys. | #### Examples ##### Sort by a column ```ts -const plan = sql.user.select('id', 'email').orderBy('email', { direction: 'asc' }).build(); +const plan = db.sql.public.user.select('id', 'email').orderBy('email', { direction: 'asc' }).build(); const rows = await runtime.execute(plan); ``` ##### Sort by a computed value ```ts -const plan = sql.user +const plan = db.sql.public.user .select('id', 'email') .orderBy((f, fns) => fns.raw`LENGTH(${f.email})`.returns('pg/int4@1'), { direction: 'asc' }) .build(); @@ -296,7 +419,7 @@ const rows = await runtime.execute(plan); ##### Control direction and null placement ```ts -const plan = sql.post +const plan = db.sql.public.post .select('id', 'embedding') .orderBy('embedding', { direction: 'asc', nulls: 'first' }) .build(); @@ -311,14 +434,14 @@ De-duplicate identical projected rows (`SELECT DISTINCT`). | Return type | Example | Description | |---|---|---| -| `SelectQuery` | `sql.post.select('priority').distinct()` | A query returning only distinct projected rows. | +| `SelectQuery` | `db.sql.public.post.select('priority').distinct()` | A query returning only distinct projected rows. | #### Examples ##### De-duplicate projected rows ```ts -const plan = sql.post.select('priority').distinct().build(); +const plan = db.sql.public.post.select('priority').distinct().build(); const rows = await runtime.execute(plan); // distinct priorities: ['high', 'low', 'urgent'] ``` @@ -342,14 +465,14 @@ Keep the first row per distinct key, according to the query's `orderBy()` (`DIST | Return type | Example | Description | |---|---|---| -| `SelectQuery` | `sql.post.distinctOn('userId')` | A query keeping the first row per distinct key. | +| `SelectQuery` | `db.sql.public.post.distinctOn('userId')` | A query keeping the first row per distinct key. | #### Examples ##### First post per user by date ```ts -const plan = sql.post +const plan = db.sql.public.post .select('id', 'userId', 'createdAt') .orderBy('userId', { direction: 'asc' }) .orderBy('createdAt', { direction: 'asc' }) @@ -374,14 +497,14 @@ Cap the number of returned rows and skip rows in the ordered result set. | Return type | Example | Description | |---|---|---| -| `SelectQuery` | `sql.user.limit(2)` | A query with the row cap or offset applied. | +| `SelectQuery` | `db.sql.public.user.limit(2)` | A query with the row cap or offset applied. | #### Examples ##### Page through results ```ts -const plan = sql.user +const plan = db.sql.public.user .select('id') .orderBy('email', { direction: 'asc' }) .limit(1) @@ -404,19 +527,19 @@ Alias a completed `SELECT` query with `.as(alias)` to use it as a join source. T | Return type | Example | Description | |---|---|---| -| Join source | `sql.post.select(...).as('hp')` | A subquery usable as a join `other`. This is not a buildable query; join it into an outer query first. | +| 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. | #### Examples ##### Join against a subquery ```ts -const highPriorityPosts = sql.post +const highPriorityPosts = db.sql.public.post .select('id', 'userId') .where((f, fns) => fns.eq(f.priority, 'high')) .as('hp'); -const plan = sql.user +const plan = db.sql.public.user .innerJoin(highPriorityPosts, (f, fns) => fns.eq(f.user.id, f.hp.userId)) .select((f) => ({ userId: f.user.id, postId: f.hp.id })) .build(); @@ -427,6 +550,42 @@ const rows = await runtime.execute(plan); 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`. +The examples below use a separate `customer` / `order` schema, where `order.amount` and `order.quantity` are numeric columns: + +
+ +Expand for the aggregate example schema + +```prisma +types { + Uuid = String @db.Uuid +} + +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") +} +``` + +
+ :::warning[Aggregate results can decode as strings] At this raw SQL layer there is no ORM decode step, so the PostgreSQL driver's default parsers apply. `COUNT()` (bigint) and `SUM()` / `AVG()` over an integer column (PostgreSQL promotes these to `numeric`) decode as JavaScript **strings**, not numbers, to avoid precision loss. `MIN()` and `MAX()` over an integer column stay integers and decode as numbers. Call `Number(...)` on `COUNT` / `SUM` / `AVG` results before doing arithmetic. A `.returns(...)` codec annotation does not change this: it's a compile-time type declaration, not a runtime cast, so a raw `EXTRACT(...)` typed `.returns('pg/int4@1')` still decodes as a string. Comparisons inside `having()` and `where()` are unaffected, because PostgreSQL evaluates them server-side. ::: @@ -446,14 +605,14 @@ Group rows by one or more columns or by a computed expression, producing one row | Return type | Example | Description | |---|---|---| -| `GroupedQuery` | `sql.order.groupBy('customerId')` | A grouped query supporting `having()`, aggregates, ordering, and limits. | +| `GroupedQuery` | `db.sql.public.order.groupBy('customerId')` | A grouped query supporting `having()`, aggregates, ordering, and limits. | #### Examples ##### Group by a column with a count ```ts -const plan = sql.order +const plan = db.sql.public.order .select('customerId') .select('orderCount', (f, fns) => fns.count(f.id)) .groupBy('customerId') @@ -466,7 +625,7 @@ const rows = await runtime.execute(plan); ##### Group by a computed value ```ts -const plan = sql.order +const plan = db.sql.public.order .select('yearPlaced', (f, fns) => fns.raw`EXTRACT(YEAR FROM ${f.placedAt})`.returns('pg/int4@1')) .select('orderCount', (f, fns) => fns.count(f.id)) .groupBy((f, fns) => fns.raw`EXTRACT(YEAR FROM ${f.placedAt})`.returns('pg/int4@1')) @@ -493,14 +652,14 @@ Filter groups by an aggregate comparison. Build the predicate from the aggregate | Return type | Example | Description | |---|---|---| -| `GroupedQuery` | `sql.order.groupBy('customerId').having(...)` | A grouped query filtered to the groups matching the predicate. | +| `GroupedQuery` | `db.sql.public.order.groupBy('customerId').having(...)` | A grouped query filtered to the groups matching the predicate. | #### Examples ##### Filter groups by a sum threshold ```ts -const plan = sql.order +const plan = db.sql.public.order .select('customerId') .select('totalAmount', (f, fns) => fns.sum(f.amount)) .groupBy('customerId') @@ -513,14 +672,14 @@ const rows = await runtime.execute(plan); ##### Compare with `count()`, `avg()`, `min()`, `max()` ```ts -const avgPlan = sql.order +const avgPlan = 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 = sql.order +const minMaxPlan = db.sql.public.order .select('customerId') .select('minAmount', (f, fns) => fns.min(f.amount)) .select('maxAmount', (f, fns) => fns.max(f.amount)) @@ -543,7 +702,7 @@ A `GroupedQuery` supports the same `orderBy()`, `limit()`, `offset()`, `distinct ##### Order groups by total, keep the top one ```ts -const plan = sql.order +const plan = db.sql.public.order .select('customerId') .select('totalAmount', (f, fns) => fns.sum(f.amount)) .groupBy('customerId') @@ -577,21 +736,21 @@ Insert one or more rows in a single statement. | Return type | Example | Description | |---|---|---| -| `InsertQuery` | `sql.tag.insert([...])` | An insert query. Buildable directly, or chain [`returning()`](#returning). | +| `InsertQuery` | `db.sql.public.tag.insert([...])` | An insert query. Buildable directly, or chain [`returning()`](#returning). | #### Examples ##### Insert a single row ```ts -const plan = sql.tag.insert([{ id: crypto.randomUUID(), label: 'single-row-tag' }]).build(); +const plan = db.sql.public.tag.insert([{ id: crypto.randomUUID(), label: 'single-row-tag' }]).build(); await runtime.execute(plan); ``` ##### Insert multiple rows in one statement ```ts -const plan = sql.tag +const plan = db.sql.public.tag .insert([ { id: crypto.randomUUID(), label: 'multi-row-a' }, { id: crypto.randomUUID(), label: 'multi-row-b' }, @@ -618,7 +777,7 @@ Return columns from the rows affected by an `insert()`, `update()`, or `delete() | Return type | Example | Description | |---|---|---| -| Mutation query | `sql.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, now typed to resolve to the returned rows. | #### Examples @@ -626,7 +785,7 @@ Return columns from the rows affected by an `insert()`, `update()`, or `delete() ```ts const id = crypto.randomUUID(); -const plan = sql.tag.insert([{ id, label: 'returned-tag' }]).returning('id', 'label').build(); +const plan = db.sql.public.tag.insert([{ id, label: 'returned-tag' }]).returning('id', 'label').build(); const rows = await runtime.execute(plan); // rows === [{ id, label: 'returned-tag' }] ``` @@ -648,14 +807,14 @@ Update matched rows. Set columns with a values object, or derive new values from | Return type | Example | Description | |---|---|---| -| `UpdateQuery` | `sql.user.update({ ... })` | An update query. Chain `where()` and optionally [`returning()`](#returning). | +| `UpdateQuery` | `db.sql.public.user.update({ ... })` | An update query. Chain `where()` and optionally [`returning()`](#returning). | #### Examples ##### Update with a values object ```ts -const plan = sql.user +const plan = db.sql.public.user .update({ displayName: 'Bobby' }) .where((f, fns) => fns.eq(f.id, bobId)) .returning('id', 'displayName') @@ -667,7 +826,7 @@ const rows = await runtime.execute(plan); ##### Derive a value from an existing column ```ts -const plan = sql.user +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)) .returning('id', 'displayName') @@ -684,14 +843,14 @@ Delete matched rows. Gate the delete with `where()`, and use [`returning()`](#re | Return type | Example | Description | |---|---|---| -| `DeleteQuery` | `sql.tag.delete()` | A delete query. Chain `where()` and optionally [`returning()`](#returning). | +| `DeleteQuery` | `db.sql.public.tag.delete()` | A delete query. Chain `where()` and optionally [`returning()`](#returning). | #### Examples ##### Delete and return the removed row ```ts -const plan = sql.tag +const plan = db.sql.public.tag .delete() .where((f, fns) => fns.eq(f.id, id)) .returning('id', 'label') @@ -731,7 +890,7 @@ Force an explicit codec on a raw value that has no column context to infer one f import { param } from '@prisma-next/sql-relational-core/expression'; const targetEmailDomain = param('%@example.com', { codecId: 'pg/text@1' }); -const plan = sql.user +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' }) @@ -783,7 +942,7 @@ Write a raw SQL fragment as a tagged template. Interpolate columns and values wi ##### Compute a column with a SQL function ```ts -const plan = sql.user +const plan = db.sql.public.user .select('emailLength', (f, fns) => fns.raw`LENGTH(${f.email})`.returns('pg/int4@1')) .build(); const rows = await runtime.execute(plan); @@ -792,7 +951,7 @@ const rows = await runtime.execute(plan); ##### `COALESCE` via `fns.raw` ```ts -const plan = sql.order +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.execute(plan); @@ -812,14 +971,14 @@ Compile a query into an executable plan. | Return type | Example | Description | |---|---|---| -| Query plan | `sql.user.select('id').build()` | A plan you run with `runtime.execute(...)`. Its per-row type is recoverable with [`ResultType`](#resulttype). | +| Query plan | `db.sql.public.user.select('id').build()` | A plan you run with `runtime.execute(...)`. Its per-row type is recoverable with [`ResultType`](#resulttype). | ### Executing a plan -Run a plan with `runtime.execute(plan)`, where `runtime` is the runtime you got from connecting your database client. It resolves to an array of rows (`Row[]`). +Run a plan with `runtime.execute(plan)`, where `runtime` is the runtime you got from connecting your database client (`const runtime = await db.connect()`). It resolves to an array of rows (`Row[]`). ```ts -const plan = sql.user.select('id', 'email').build(); +const plan = db.sql.public.user.select('id', 'email').build(); const rows = await runtime.execute(plan); // Row[] ``` @@ -839,7 +998,7 @@ Recover a plan's row type at the type level. ```ts import type { ResultType } from '@prisma-next/framework-components/runtime'; -const plan = sql.user.select('id', 'email').build(); +const plan = db.sql.public.user.select('id', 'email').build(); type Row = ResultType; // { id: string; email: string } const rows = await runtime.execute(plan); // Row[] From 4e0d7a152da7f63a0064b17fba569d6d91295414 Mon Sep 17 00:00:00 2001 From: Nurul Sundarani Date: Tue, 7 Jul 2026 18:04:20 +0530 Subject: [PATCH 08/15] docs(next): address final rework review findings Co-Authored-By: Claude Fable 5 --- apps/docs/content/docs/orm/next/reference/index.mdx | 2 +- apps/docs/content/docs/orm/next/reference/orm-client.mdx | 2 ++ .../content/docs/orm/next/reference/sql-query-builder.mdx | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/docs/content/docs/orm/next/reference/index.mdx b/apps/docs/content/docs/orm/next/reference/index.mdx index c454228cab..d738ad664a 100644 --- a/apps/docs/content/docs/orm/next/reference/index.mdx +++ b/apps/docs/content/docs/orm/next/reference/index.mdx @@ -13,7 +13,7 @@ Use the ORM client for everyday application queries across models and relations. ## How database differences are documented -Both reference pages document every method with the classic Remarks / Options / Return type / Examples structure. 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 — there's no separate availability matrix to cross-reference. +Both reference pages document every method with the classic Remarks / Options / Return type / Examples structure. 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. For a conceptual walkthrough of reading, writing, and querying data (rather than an exhaustive method-by-method reference), see the [Fundamentals](/orm/next/fundamentals/reading-data) section: diff --git a/apps/docs/content/docs/orm/next/reference/orm-client.mdx b/apps/docs/content/docs/orm/next/reference/orm-client.mdx index f8a01898fa..50ce6c46e8 100644 --- a/apps/docs/content/docs/orm/next/reference/orm-client.mdx +++ b/apps/docs/content/docs/orm/next/reference/orm-client.mdx @@ -197,6 +197,8 @@ const db = postgres({ contractJson, url: process.env.DATABASE_URL }); const users = await db.orm.public.User.all(); ``` +If your contract uses types from an extension pack, pass the pack when you create the client — the example schema's `Embedding1536` (pgvector) type needs `extensions: [pgvector]`, with `pgvector` imported from `@prisma-next/extension-pgvector/runtime`. + To attach domain methods to a model, register a custom `Collection` subclass when you build the client. That path uses the `orm(...)` factory instead of the client's built-in facet; see [Custom `Collection` subclass](#custom-collection-subclass). ### MongoDB diff --git a/apps/docs/content/docs/orm/next/reference/sql-query-builder.mdx b/apps/docs/content/docs/orm/next/reference/sql-query-builder.mdx index 993b4b6e84..6cfe19829b 100644 --- a/apps/docs/content/docs/orm/next/reference/sql-query-builder.mdx +++ b/apps/docs/content/docs/orm/next/reference/sql-query-builder.mdx @@ -148,7 +148,7 @@ const plan = db.sql.public.user.select('id', 'email').build(); const users = await runtime.execute(plan); ``` -The facet is built from the `sql()` factory: `db.sql` holds the result of an internal `sql({ context, rawCodecInferer }).public` 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): +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-next/sql-builder/runtime'; @@ -915,7 +915,7 @@ The complete built-in set is fixed: | Raw SQL | `raw` | | Aggregate (grouped queries) | `count`, `sum`, `avg`, `min`, `max` | -Nothing else is built in. Any further function, such as `ilike` or `cosineDistance`, comes from an extension pack registered for your contract; without the matching extension pack, those functions are absent. +Nothing else is built in. 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(...)` codec. From 88485578bbae6ff3569c4123cb2e2d36a8369513 Mon Sep 17 00:00:00 2001 From: Nurul Sundarani Date: Tue, 7 Jul 2026 20:46:53 +0530 Subject: [PATCH 09/15] docs(next): add pipeline builder reference Co-Authored-By: Claude Fable 5 --- .../content/docs/orm/next/reference/index.mdx | 22 +- .../content/docs/orm/next/reference/meta.json | 9 +- .../orm/next/reference/pipeline-builder.mdx | 1136 +++++++++++++++++ .../docs/orm/next/reference/raw-queries.mdx | 10 + .../reference/transactions-and-runtime.mdx | 10 + 5 files changed, 1178 insertions(+), 9 deletions(-) create mode 100644 apps/docs/content/docs/orm/next/reference/pipeline-builder.mdx create mode 100644 apps/docs/content/docs/orm/next/reference/raw-queries.mdx create mode 100644 apps/docs/content/docs/orm/next/reference/transactions-and-runtime.mdx diff --git a/apps/docs/content/docs/orm/next/reference/index.mdx b/apps/docs/content/docs/orm/next/reference/index.mdx index d738ad664a..e3ec8e886a 100644 --- a/apps/docs/content/docs/orm/next/reference/index.mdx +++ b/apps/docs/content/docs/orm/next/reference/index.mdx @@ -1,19 +1,19 @@ --- title: Prisma Next API reference -description: Reference index for the Prisma Next ORM client and SQL query builder. +description: Reference index for the Prisma Next ORM client, SQL query builder, pipeline builder, raw queries, and runtime APIs. url: /orm/next/reference metaTitle: Prisma Next API reference -metaDescription: Reference index for the Prisma Next ORM client and SQL query builder. +metaDescription: Reference index for the Prisma Next ORM client, SQL query builder, pipeline builder, raw queries, and runtime APIs. badge: early-access --- -Prisma Next has two 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. +Prisma Next 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`. -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, or when you want direct control over the generated SQL. +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/next/reference/raw-queries); for client lifecycle, transactions, and prepared statements, see [Transactions and runtime](/orm/next/reference/transactions-and-runtime). ## How database differences are documented -Both reference pages document every method with the classic Remarks / Options / Return type / Examples structure. 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. +The reference pages document every method with the classic Remarks / Options / Return type / Examples structure. 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. For a conceptual walkthrough of reading, writing, and querying data (rather than an exhaustive method-by-method reference), see the [Fundamentals](/orm/next/fundamentals/reading-data) section: @@ -32,9 +32,6 @@ Prisma Next ships first-class support for PostgreSQL and MongoDB today. SQLite i These surfaces are planned but not yet documented here: :::note -- The MongoDB pipeline builder (`db.query`) -- Raw query escape hatches -- Transaction and runtime APIs - Middleware hook APIs (for concepts and the built-in middleware, see [How middleware works](/orm/next/middleware/how-middleware-works)) ::: @@ -45,4 +42,13 @@ These surfaces are planned but not yet documented here: }> 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. + diff --git a/apps/docs/content/docs/orm/next/reference/meta.json b/apps/docs/content/docs/orm/next/reference/meta.json index d281dd7e68..08f9416e2d 100644 --- a/apps/docs/content/docs/orm/next/reference/meta.json +++ b/apps/docs/content/docs/orm/next/reference/meta.json @@ -1,4 +1,11 @@ { "title": "Reference", - "pages": ["index", "orm-client", "sql-query-builder"] + "pages": [ + "index", + "orm-client", + "sql-query-builder", + "pipeline-builder", + "raw-queries", + "transactions-and-runtime" + ] } diff --git a/apps/docs/content/docs/orm/next/reference/pipeline-builder.mdx b/apps/docs/content/docs/orm/next/reference/pipeline-builder.mdx new file mode 100644 index 0000000000..9dec335f93 --- /dev/null +++ b/apps/docs/content/docs/orm/next/reference/pipeline-builder.mdx @@ -0,0 +1,1136 @@ +--- +title: Pipeline builder reference +description: Reference for the Prisma Next MongoDB pipeline builder's stages, accumulators, expression helpers, and write terminals. +url: /orm/next/reference/pipeline-builder +metaTitle: Prisma Next MongoDB pipeline builder reference +metaDescription: Reference for the Prisma Next MongoDB pipeline builder's stages, accumulators, expression helpers, and write terminals. +badge: early-access +--- + +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/next/reference/sql-query-builder): a lower-level surface for the queries the [ORM client](/orm/next/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. + +:::note[This is the MongoDB pipeline builder] +The pipeline builder targets MongoDB only. PostgreSQL has no pipeline builder: use the [SQL query builder](/orm/next/reference/sql-query-builder) for PostgreSQL joins and aggregates, and the [ORM client](/orm/next/reference/orm-client) for everyday queries on either database. +::: + +## 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`). + +
+ +Expand for the example schema + +```prisma +enum UserRole { + @@type("mongo/string@1") + Admin = "admin" + Author = "author" + Reader = "reader" +} + +type Address { + street String + city String + zip String? + country String +} + +model User { + id ObjectId @id @map("_id") + name String + email String + bio String? + role UserRole + address Address? + posts Post[] + @@map("users") +} + +model Post { + id ObjectId @id @map("_id") + title String + content String + kind String + authorId ObjectId + createdAt DateTime + author User @relation(fields: [authorId], references: [id]) + @@discriminator(kind) + @@index([authorId]) + @@index([createdAt(sort: Desc), authorId]) + @@map("posts") +} + +model Article { + summary String + @@base(Post, "article") + @@unique([summary]) +} + +model Tutorial { + difficulty String + duration Int + @@base(Post, "tutorial") +} +``` + +
+ +## Entry points + +Every pipeline starts with `db.query.from(root)` and ends with a terminal. Between them you chain stages. + +### `from()` + +Enter the pipeline builder on a collection. + +#### Remarks + +- MongoDB only. `db.query` is the pipeline-builder entry point on the client, the same object the standalone `mongoQuery(...)` returns. +- `from(root)` takes a contract **root name**: the 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: `Error: Unknown root: "". Valid roots: ...`. This is an `Error`, not a `TypeError`. +- `from()` returns a builder you chain stages onto. The builder moves through three states as you chain: a starting collection, a filtered collection after `match()`, and a pipeline chain after any other stage. Each state exposes the methods that are valid at that point (for example, the root-level writes `insertOne` / `insertMany` are only available before you add stages). + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `root` | Contract root 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. | + +#### Examples + +##### Enter the builder on a collection + +```typescript +const plan = db.query.from('posts').build(); +const posts = await db.execute(plan); +``` + +### Building and executing a pipeline + +A pipeline is inert until you execute it. `build()` (or its alias `aggregate()`) turns the chain into a plan; `db.execute(plan)` runs it. + +#### Remarks + +- `db.execute(plan)` is the client-level executor. It accepts any plan, including one built by the pipeline builder. If you hold a connected runtime directly, `runtime.execute(plan)` is the equivalent lower-level call. +- **Read results are codec-decoded.** Unlike [raw queries](/orm/next/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. + +#### Examples + +##### Execute through the client + +```typescript +const plan = db.query.from('posts').sort({ createdAt: 1 }).build(); +const posts = await db.execute(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. + +### `match()` + +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). +- For an aggregation-expression predicate (comparing computed values rather than a field against a constant), wrap it in `expr(...)`: `match((f) => expr(fn.gt(fn.year(f.createdAt), fn.literal(2023))))`. See [Expression helpers](#expression-helpers). + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `predicate` | Callback `(f) => FilterExpression` | Yes | The condition documents must satisfy. | + +#### Return type + +| 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. | + +#### Examples + +##### Filter by a field + +```typescript +const plan = db.query + .from('posts') + .match((f) => f.kind.eq('tutorial')) + .build(); +const tutorials = await db.execute(plan); +``` + +##### Aggregation-expression predicate + +```typescript +import { fn, expr } from '@prisma-next/mongo-query-builder'; + +const plan = db.query + .from('posts') + .match((f) => expr(fn.gt(fn.year(f.createdAt), fn.literal(2023)))) + .build(); +const recent = await db.execute(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`. The type system already steers you away (the value type does not include `ObjectId`), so reaching this at all requires a cast. + +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/next/reference/orm-client), which handles `_id` encoding for you (`db.orm.posts.where({ _id })`). +::: + +### `sort()` + +Order documents by a field spec. + +#### Remarks + +- `sort()` takes a plain object spec: `{ field: 1 }` ascending, `{ field: -1 }` descending. Multiple keys sort in the order they appear. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `spec` | `{ [field]: 1 \| -1 }` | Yes | The sort key(s) and direction(s). | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| Pipeline builder | `db.query.from('posts').sort({ createdAt: -1 })` | A builder with an ordering applied. | + +#### Examples + +##### Sort descending + +```typescript +const plan = db.query.from('posts').sort({ createdAt: -1 }).build(); +const newestFirst = await db.execute(plan); +``` + +### `limit()` + +Cap the number of documents. + +#### Remarks + +- Combine `sort()` then `limit(1)` to fetch a single document: the pipeline builder has no `first()`. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `count` | `number` | Yes | Maximum number of documents to return. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| Pipeline builder | `db.query.from('posts').limit(1)` | A builder limited to `count` documents. | + +#### Examples + +##### Fetch a single document + +```typescript +const plan = db.query.from('posts').sort({ createdAt: 1 }).limit(1).build(); +const [oldest] = await db.execute(plan); +``` + +### `skip()` + +Offset into the sorted result set. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `count` | `number` | Yes | Number of documents to skip. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| Pipeline builder | `db.query.from('posts').skip(1)` | A builder offset by `count` documents. | + +#### Examples + +##### Paginate + +```typescript +const plan = db.query.from('posts').sort({ createdAt: 1 }).skip(1).build(); +const afterFirst = await db.execute(plan); +``` + +### `sample()` + +Draw a random subset of documents. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `size` | `number` | Yes | Number of documents to sample. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| Pipeline builder | `db.query.from('posts').sample(1)` | A builder emitting a random subset. | + +#### Examples + +##### Random subset + +```typescript +const plan = db.query.from('posts').sample(1).build(); +const oneRandom = await db.execute(plan); +``` + +### `addFields()` + +Compute new fields and attach them to each document. + +#### Remarks + +- `addFields()` takes a callback returning an object of new field names to computed [expression-helper](#expression-helpers) values. Existing fields are preserved. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `spec` | Callback `(f) => ({ [field]: Expression })` | Yes | The new fields to compute. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| Pipeline builder | `db.query.from('posts').addFields(...)` | A builder whose documents carry the new fields. | + +#### Examples + +##### Attach a computed field + +```typescript +import { fn } from '@prisma-next/mongo-query-builder'; + +const plan = db.query + .from('posts') + .addFields((f) => ({ shoutTitle: fn.toUpper(f.title) })) + .build(); +const withShout = await db.execute(plan); +``` + +### `lookup()` + +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(...)`. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `builder` | Callback `(from) => from(root).on(...).as(name)` | Yes | The join specification. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| Pipeline builder | `db.query.from('posts').lookup(...)` | A builder whose documents carry the joined array. | + +#### Examples + +##### Join a foreign collection + +```typescript +const plan = db.query + .from('posts') + .match((f) => f.title.eq('Hello world')) + .lookup((from) => + from('users') + .on((local, foreign) => ({ local: local.authorId, foreign: foreign._id })) + .as('author'), + ) + .build(); +const withAuthor = await db.execute(plan); +// withAuthor[0].author is an array; author[0]._id is a raw ObjectId — compare with String(...) +``` + +### `project()` + +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. + +#### 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. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| Pipeline builder | `db.query.from('posts').project('title', 'kind')` | A builder projected to the given shape. | + +#### Examples + +##### Key-list form + +```typescript +const plan = db.query.from('posts').project('title', 'kind').build(); +const trimmed = await db.execute(plan); +// each document has title, kind, and _id — no content +``` + +##### Callback form + +```typescript +import { fn } from '@prisma-next/mongo-query-builder'; + +const plan = db.query + .from('posts') + .project((f) => ({ title: 1, shout: fn.toUpper(f.title) })) + .build(); +const projected = await db.execute(plan); +``` + +### `unwind()` + +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. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `field` | Field name (`string`) | Yes | The array field to unroll. | +| `options.preserveNullAndEmptyArrays` | `boolean` | No | Keep documents whose array is null, missing, or empty. Defaults to `false`. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| Pipeline builder | `db.query.from(root).unwind('items')` | A builder emitting one document per array element. | + +### `group()` + +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-next/mongo-query-builder'`, then `acc.count()`, `acc.push(f.title)`. The callback signature is single-argument; there is no `(f, acc) => ...` form. (Some older internal material shows a two-argument callback. That is incorrect.) +- A `_id: null` key groups the whole collection into a single bucket. +- A non-accumulator value for a non-`_id` key is a compile error. Forced past the type system, `group()` throws at build time: `... must use an accumulator (e.g. acc.sum(), acc.count()) ...`. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `spec` | Callback `(f) => ({ _id: keyExpression \| null, [alias]: acc.* })` | Yes | The grouping key and per-group accumulators. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| Pipeline builder | `db.query.from('posts').group(...)` | A builder emitting one document per group. | + +#### Examples + +##### Group by a field + +```typescript +import { acc } from '@prisma-next/mongo-query-builder'; + +const plan = db.query + .from('posts') + .group((f) => ({ + _id: f.authorId, + postCount: acc.count(), + titles: acc.push(f.title), + })) + .build(); +const perAuthor = await db.execute(plan); +``` + +##### Group the whole collection + +```typescript +import { acc } from '@prisma-next/mongo-query-builder'; + +const plan = db.query + .from('posts') + .group((f) => ({ _id: null, total: acc.count(), latest: acc.max(f.createdAt) })) + .build(); +const [summary] = await db.execute(plan); +// { _id: null, total: 2, latest: } +``` + +For Prisma 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 db.execute( ++ db.query.from('posts').group((f) => ({ _id: f.authorId, postCount: acc.count() })).build(), ++ ); +``` + +### `replaceRoot()` + +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`). +- A common pattern is to promote the first element of a `lookup()` array with `fn.arrayElemAt(f.author, fn.literal(0))`. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `spec` | Callback `(f) => documentExpression` | Yes | The document to promote to the root. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| Pipeline builder | `db.query.from('posts').replaceRoot(...)` | A builder whose documents are the promoted sub-document. | + +#### Examples + +##### Promote a looked-up document + +```typescript +import { fn } from '@prisma-next/mongo-query-builder'; + +const plan = db.query + .from('posts') + .match((f) => f.title.eq('Hello world')) + .lookup((from) => + from('users') + .on((local, foreign) => ({ local: local.authorId, foreign: foreign._id })) + .as('author'), + ) + .replaceRoot((f) => fn.arrayElemAt(f.author, fn.literal(0))) + .build(); +const authors = await db.execute(plan); +// each document is the promoted author sub-document +``` + +### `count()` + +Reduce the pipeline to a single document holding the count. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `field` | `string` | Yes | The output field name for the count. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| Pipeline builder | `db.query.from('posts').count('total')` | A builder emitting one `{ [field]: n }` document. | + +#### Examples + +##### Count documents + +```typescript +const plan = db.query.from('posts').count('total').build(); +const [{ total }] = await db.execute(plan); +// { total: 2 } +``` + +### `sortByCount()` + +Group by an expression and sort descending by group size. + +#### Remarks + +- `sortByCount()` takes a callback returning the grouping expression. It emits one document per distinct value, each `{ _id, count }`, sorted by `count` descending. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `expression` | Callback `(f) => expression` | Yes | The value to group and count by. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| Pipeline builder | `db.query.from('posts').sortByCount((f) => f.kind)` | A builder emitting `{ _id, count }` documents. | + +#### Examples + +##### Count occurrences of a field value + +```typescript +const plan = db.query + .from('posts') + .sortByCount((f) => f.kind) + .build(); +const byKind = await db.execute(plan); +// two buckets, each { _id: , count: 1 } — order of tied counts is not guaranteed +``` + +### `redact()` + +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. + +### 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. + +#### Remarks + +- The following stages take an options object: `unionWith`, `bucket`, `bucketAuto`, `geoNear`, `facet`, `graphLookup`, `setWindowFields`, `densify`, and `fill`. +- **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. + +#### Examples + +##### Concatenate another collection with `unionWith()` + +```typescript +const plan = db.query.from('posts').unionWith('users').build(); +const combined = await db.execute(plan); +// posts followed by users +``` + +`bucket()` and `graphLookup()` are executable but require hand-built expression nodes for their opaque fields (`bucket.groupBy`, `graphLookup.startWith`). Build those nodes with expression helpers and pass `.node`. Because the value bypasses the typed accessor, verify the stage output against your own data before relying on it. + +### Atlas-only stages + +`search()`, `searchMeta()`, and `vectorSearch()` build MongoDB Atlas Search stages (`$search`, `$searchMeta`, `$vectorSearch`). + +#### Remarks + +- These stages **require MongoDB Atlas** (they need Atlas Search's `mongot` process) and are **not validated by our executable test suite**, which runs against an in-memory MongoDB with no Atlas Search backend. Only their plan shape is verified; the stages are never executed. +- `search(spec)` and `searchMeta(spec)` take an Atlas Search operator spec (for example `{ text: { query: 'hello', path: 'title' } }`). +- `vectorSearch(spec)` takes an Atlas Vector Search spec (`{ index, path, queryVector, numCandidates, limit }`). +- Verify these against a real Atlas deployment before relying on them. + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| Pipeline builder | `db.query.from('posts').search({ text: { query: 'hello', path: 'title' } })` | A builder with an Atlas Search stage. Executes only on MongoDB Atlas. | + +## Accumulators + +Accumulators compute per-group values inside a [`group()`](#group) stage. Import them from `@prisma-next/mongo-query-builder`: + +```typescript +import { acc } from '@prisma-next/mongo-query-builder'; +``` + +The builder exposes all nineteen MongoDB accumulators: + +| Accumulator | Signature | Description | +|---|---|---| +| `acc.count()` | `count()` | Number of documents in the group. | +| `acc.sum(expr)` | `sum(expression)` | Sum of the expression across the group. | +| `acc.avg(expr)` | `avg(expression)` | Average of the expression. | +| `acc.min(expr)` | `min(expression)` | Minimum value. | +| `acc.max(expr)` | `max(expression)` | Maximum value. | +| `acc.first(expr)` | `first(expression)` | Value from the first document in the group. | +| `acc.last(expr)` | `last(expression)` | Value from the last document in the group. | +| `acc.push(expr)` | `push(expression)` | Array of the expression's value from every document. | +| `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.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. + +#### Examples + +##### `count()` and `sum()` + +```typescript +import { acc, fn } from '@prisma-next/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 db.execute(plan); +// { total: 2, durationSum: 2 } +``` + +##### `avg()`, `min()`, and `max()` + +```typescript +import { acc, fn } from '@prisma-next/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)), + })) + .build(); +const [stats] = await db.execute(plan); +// { earliest: , latest: , avgYear: 2024 } +``` + +##### `first()` and `last()` + +```typescript +import { acc } from '@prisma-next/mongo-query-builder'; + +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 db.execute(plan); +// { firstTitle: 'Hello world', lastTitle: 'Tutorial one' } +``` + +##### `push()` and `addToSet()` + +```typescript +import { acc } from '@prisma-next/mongo-query-builder'; + +const plan = db.query + .from('posts') + .group((f) => ({ + _id: null, + allTitles: acc.push(f.title), + distinctKinds: acc.addToSet(f.kind), + })) + .build(); +const [{ allTitles, distinctKinds }] = await db.execute(plan); +``` + +##### `firstN()` (an `N`-variant) + +```typescript +import { acc, fn } from '@prisma-next/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 db.execute(plan); +// firstTwo is ['Hello world', 'Tutorial one'] +``` + +## Expression helpers + +Expression helpers (`fn.*`) build the computed values used inside stages like `addFields()`, `project()`, `group()` accumulators, and `match()` (via `expr()`). Import them from `@prisma-next/mongo-query-builder`: + +```typescript +import { fn } from '@prisma-next/mongo-query-builder'; +``` + +The `fn.*` namespace mirrors MongoDB's aggregation expression operators: each helper is the camelCase name of the operator without its `$` prefix (`$toUpper` becomes `fn.toUpper`, `$dateToString` becomes `fn.dateToString`). The helpers below are grouped by category, with the ones exercised by the test suite; the full set follows the same naming pattern. + +| 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`. | + +#### 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. + +#### Examples + +##### Arithmetic in a projection + +```typescript +import { fn } from '@prisma-next/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))), + })) + .build(); +const rows = await db.execute(plan); +// computed is 7 for every document +``` + +##### String composition + +```typescript +import { fn } from '@prisma-next/mongo-query-builder'; + +const plan = db.query + .from('posts') + .project((f) => ({ shout: fn.concat(fn.toUpper(f.title), fn.literal('!')) })) + .build(); +const rows = await db.execute(plan); +``` + +##### Branch with `cond()` + +```typescript +import { fn } from '@prisma-next/mongo-query-builder'; + +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'), + ), + })) + .build(); +const rows = await db.execute(plan); +``` + +##### Array length + +```typescript +import { fn } from '@prisma-next/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 db.execute(plan); +``` + +## Read terminals + +A read terminal turns the chain into an executable plan. Pass the plan to `db.execute(plan)` to run it. + +### `build()` and `aggregate()` + +Compile the pipeline into a plan. + +#### Remarks + +- `build()` produces the plan. `aggregate()` is an alias: it returns the identical plan and executes identically. +- Neither runs the query. Pass the plan to `db.execute(plan)` (or `runtime.execute(plan)`) to fetch documents. + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| Query plan | `db.query.from('posts').build()` | A plan you execute with `db.execute(...)`. | + +#### Examples + +##### `build()` and its `aggregate()` alias + +```typescript +const built = db.query.from('posts').sort({ createdAt: 1 }).build(); +const aggregated = db.query.from('posts').sort({ createdAt: 1 }).aggregate(); +// built and aggregated produce the same plan + +const rows = await db.execute(aggregated); +``` + +## Write methods + +The pipeline builder can write, not just read. Write methods are available at three points: on the root collection, after a `match()` filter, and as pipeline write terminals. + +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. + +### Root-level writes + +`insertOne`, `insertMany`, `updateAll`, `deleteAll`, and `upsertOne` are available directly on `from(root)`, before any stage. + +#### Remarks + +- These return a plan; execute it with `db.execute(plan)`. +- Write results are the driver's own result envelopes (`{ insertedId }`, `{ insertedIds }`, `{ deletedCount }`, `{ modifiedCount }`, `{ upsertedId }`), not decoded documents. + +#### 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 db.execute(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 db.execute(manyPlan); +// { insertedIds: { '0': , '1': } } +``` + +##### `updateAll()` (array-of-ops updater) + +```typescript +const plan = db.query.from('users').updateAll((f) => [f.bio.set('everyone now has bio')]); +await db.execute(plan); +``` + +##### `deleteAll()` + +```typescript +const plan = db.query.from('users').deleteAll(); +const [result] = await db.execute(plan); +// { deletedCount: 2 } +``` + +##### `upsertOne()` on the root (filter, then updater) + +On the root collection, `upsertOne()` takes a filter callback and an updater callback: + +```typescript +const plan = db.query.from('users').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 db.execute(plan); +// inserts when no document matches: result.upsertedId is defined +``` + +### Writes after `match()` + +After a `match()` filter, `updateMany`, `updateOne`, `deleteMany`, `deleteOne`, `upsertOne`, `findOneAndUpdate`, and `findOneAndDelete` write against the matched documents. + +#### Remarks + +- The `match()` filter supplies the write's filter; you do not repeat it. +- After `match()`, `upsertOne()` takes the updater callback only (the filter comes from `match()`). +- `updateOne` / `deleteOne` affect at most one matching document; `updateMany` / `deleteMany` affect all matches. + +#### Examples + +##### `updateMany()` and `updateOne()` + +```typescript +const manyPlan = db.query + .from('users') + .match((f) => f.role.eq('author')) + .updateMany((f) => [f.bio.set('matched-many')]); +await db.execute(manyPlan); + +const onePlan = db.query + .from('users') + .match((f) => f.email.eq('alice@example.com')) + .updateOne((f) => [f.bio.set('single-update')]); +const [result] = await db.execute(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 db.execute(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 db.execute(plan); +// updates 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'`. + +```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 db.execute(afterPlan); +// afterDoc.bio is 'changed' + +const beforePlan = db.query + .from('users') + .match((f) => f.email.eq('alice@example.com')) + .findOneAndUpdate((f) => [f.bio.set('changed')], { returnDocument: 'before' }); +const [beforeDoc] = await db.execute(beforePlan); +// beforeDoc.bio is the pre-update value +``` + +:::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'`. +::: + +##### `findOneAndDelete()` + +```typescript +const plan = db.query + .from('users') + .match((f) => f.email.eq('alice@example.com')) + .findOneAndDelete(); +const [deleted] = await db.execute(plan); +// deleted is the removed document +``` + +### Update operation forms + +Inside an updater callback, each operation targets a field. There are two mutually exclusive forms. + +#### 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. + +#### Examples + +##### Operator form + +```typescript +const plan = db.query + .from('users') + .match((f) => f.role.eq('author')) + .updateMany((f) => [f.bio.set('operator form')]); +await db.execute(plan); +``` + +##### Pipeline form (`f.stage.*`) + +```typescript +const plan = db.query + .from('users') + .match((f) => f.role.eq('author')) + .updateMany((f) => [f.stage.set({ bio: f.name.node })]); +await db.execute(plan); +// each author's bio is set to that author's own name +``` + +### 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 execute with `db.execute(plan)`, and produce no rows of their own. + +#### Examples + +##### Materialize with `out()` + +```typescript +const plan = db.query.from('users').out('users_snapshot'); +await db.execute(plan); +// the users_snapshot collection now holds the pipeline output +``` + +##### Merge with `merge()` + +```typescript +const plan = db.query.from('users').merge({ into: 'users_archive' }); +await db.execute(plan); +``` + +## `rawCommand()` + +Run a raw MongoDB aggregate command through the pipeline builder, bypassing the typed AST. + +#### 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/next/reference/raw-queries). + +#### Examples + +##### Run a raw aggregate pipeline + +```typescript +import { RawAggregateCommand } from '@prisma-next/mongo-query-ast/execution'; + +const plan = db.query.rawCommand(new RawAggregateCommand('posts', [{ $count: 'total' }])); +const rows = await db.execute(plan); +// [{ total: 2 }] +``` + +##### Filter by `_id` (the working escape hatch) + +```typescript +import { RawAggregateCommand } from '@prisma-next/mongo-query-ast/execution'; +import { ObjectId } from 'mongodb'; + +const plan = db.query.rawCommand( + new RawAggregateCommand('posts', [{ $match: { _id: new ObjectId(postId) } }]), +); +const rows = await db.execute(plan); +// a real ObjectId in a raw pipeline document matches correctly +``` diff --git a/apps/docs/content/docs/orm/next/reference/raw-queries.mdx b/apps/docs/content/docs/orm/next/reference/raw-queries.mdx new file mode 100644 index 0000000000..cf59440e20 --- /dev/null +++ b/apps/docs/content/docs/orm/next/reference/raw-queries.mdx @@ -0,0 +1,10 @@ +--- +title: Raw queries reference +description: "Reference for Prisma Next raw query escape hatches: PostgreSQL raw SQL and MongoDB raw commands." +url: /orm/next/reference/raw-queries +metaTitle: Prisma Next raw queries reference +metaDescription: "Reference for Prisma Next raw query escape hatches: PostgreSQL raw SQL and MongoDB raw commands." +badge: early-access +--- + +Content landing in this PR. diff --git a/apps/docs/content/docs/orm/next/reference/transactions-and-runtime.mdx b/apps/docs/content/docs/orm/next/reference/transactions-and-runtime.mdx new file mode 100644 index 0000000000..265d77973b --- /dev/null +++ b/apps/docs/content/docs/orm/next/reference/transactions-and-runtime.mdx @@ -0,0 +1,10 @@ +--- +title: Transactions and runtime reference +description: Reference for the Prisma Next client lifecycle, transactions, prepared statements, and execution options. +url: /orm/next/reference/transactions-and-runtime +metaTitle: Prisma Next transactions and runtime reference +metaDescription: Reference for the Prisma Next client lifecycle, transactions, prepared statements, and execution options. +badge: early-access +--- + +Content landing in this PR. From b05c9e51c5be2f64a1b5cf4fa976cc159d7768b4 Mon Sep 17 00:00:00 2001 From: Nurul Sundarani Date: Tue, 7 Jul 2026 20:58:01 +0530 Subject: [PATCH 10/15] docs(next): add raw queries reference Co-Authored-By: Claude Fable 5 --- .../docs/orm/next/reference/raw-queries.mdx | 280 +++++++++++++++++- 1 file changed, 279 insertions(+), 1 deletion(-) diff --git a/apps/docs/content/docs/orm/next/reference/raw-queries.mdx b/apps/docs/content/docs/orm/next/reference/raw-queries.mdx index cf59440e20..3a4c5d6679 100644 --- a/apps/docs/content/docs/orm/next/reference/raw-queries.mdx +++ b/apps/docs/content/docs/orm/next/reference/raw-queries.mdx @@ -7,4 +7,282 @@ metaDescription: "Reference for Prisma Next raw query escape hatches: PostgreSQL badge: early-access --- -Content landing in this PR. +Raw queries are the escape hatches for the queries the typed surfaces can't express. Reach for a typed surface first: the [ORM client](/orm/next/reference/orm-client) for everyday reads and writes, the [SQL query builder](/orm/next/reference/sql-query-builder) for PostgreSQL joins and aggregates, and the [pipeline builder](/orm/next/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 and the client-level `db.raw` tag) 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[Raw results bypass codec decoding] +Unlike the typed surfaces, raw queries do not carry a result shape, so their results are **not codec-decoded**. On MongoDB this means raw results come back as native BSON: `_id` fields are raw driver `ObjectId` instances (not decoded hex strings), and write methods return the driver's own result envelopes (`{ insertedId }`, `{ matchedCount, modifiedCount }`, `{ deletedCount }`). You are responsible for value handling: compare an `ObjectId` with `String(...)`, and read the envelope keys directly. +::: + +## PostgreSQL raw SQL + +Raw SQL lives inside the [SQL query builder](/orm/next/reference/sql-query-builder), not as a standalone statement. You write a SQL fragment as a tagged template and splice it into a `select()`, `where()`, `orderBy()`, or `update()` call. There is no way to run a bare raw SQL string on its own; every raw fragment is part of a builder query that you [`build()`](/orm/next/reference/sql-query-builder#build) and run with `runtime.execute(...)`. + +The examples in this section run against the `user` / `post` schema from the SQL query builder page. See its [example schema](/orm/next/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`: + +```ts +import postgres from '@prisma-next/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(); +``` + +### `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)`. + +```ts +const plan = db.sql.public.user + .select('id') + .select('upperEmail', (f, fns) => fns.raw`UPPER(${f.email})`.returns('pg/text@1')) + .where((f, fns) => fns.eq(f.id, aliceId)) + .build(); +const rows = await runtime.execute(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/next/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. + +```ts +const plan = db.sql.public.user + .select('id', 'email') + .where((f, fns) => fns.raw`LENGTH(${f.email}) > 15`.returns('pg/bool@1')) + .build(); +const rows = await runtime.execute(plan); +// only users whose email is longer than 15 characters +``` + +### 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. + +```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', + ), + ) + .build(); +const rows = await runtime.execute(plan); +// each row's kindLabel is 'admin' or 'regular user' +``` + +### Binding a bare value with `param()` + +To interpolate a bound value that has no adjacent column to infer a codec from, wrap it in `param(value, { codecId })` and give it an explicit, versioned codec id. Import `param` from `@prisma-next/sql-relational-core/expression`. + +```ts +import { param } from '@prisma-next/sql-relational-core/expression'; + +const targetLength = param(15, { codecId: 'pg/int4@1' }); +const plan = db.sql.public.user + .select('id', 'email') + .where((f, fns) => fns.raw`LENGTH(${f.email}) = ${targetLength}`.returns('pg/bool@1')) + .build(); +const rows = await runtime.execute(plan); +// users whose email is exactly 15 characters long +``` + +:::warning[Always wrap bare values in `param(...)`] +Bare-scalar interpolation (`` fns.raw`${limit}` ``) is currently broken against the real PostgreSQL adapter: its codec inferer emits unversioned codec ids (`pg/int4`) that don't match the versioned registry (`pg/int4@1`), so rendering throws. This is empirically verified. Always wrap values in `param(...)` with an explicit versioned codec id (`param(15, { codecId: 'pg/int4@1' })`). Interpolating a column or a typed `Expression` is also safe, because neither goes through codec inference. +::: + +### Declaring a nullable result + +The object form of `.returns()` declares a nullable result. Use it when a fragment can evaluate to SQL `NULL`, so the result type is `T | null`. + +```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, + }), + ) + .build(); +const rows = await runtime.execute(plan); +// adminName is the displayName for admins, null for everyone else +``` + +### The client-level `db.raw` tag + +`db.raw` on a connected client is the same tagged template as `fns.raw`, just reachable outside a builder callback. It has no field-accessor context (no `f` / `fns`), so use it to build a standalone `Expression` ahead of time and splice it into a query. + +```ts +const serverNow = db.raw`now()`.returns('pg/timestamptz@1'); +const plan = db.sql.public.user + .select((f) => ({ id: f.id, serverNow })) + .where((f, fns) => fns.eq(f.id, aliceId)) + .build(); +const rows = await runtime.execute(plan); +// rows[0].serverNow is a Date +``` + +`db.raw` is not a way to execute a standalone raw SQL statement: there is no plan-building or execution path for a bare `db.raw` result. Every use embeds the `.returns(...)`-typed `Expression` into a `select()`, `where()`, or other builder call. + +### Unsupported interpolation + +Interpolation accepts columns, typed expressions, `param(...)` values, and the bare scalar types (`number`, `bigint`, `string`, `boolean`, `Uint8Array`) — though bare scalars then fail at render time on PostgreSQL (see [the warning above](#binding-a-bare-value-with-param)). Interpolating anything else, such as a `Date`, throws synchronously with code `RUNTIME.RAW_SQL_UNSUPPORTED_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 codec. + +## 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`). + +Each method returns a buildable command: call `.build()` to get a plan, then run it with `db.execute(plan)`. + +```ts +import { ObjectId } from 'mongodb'; +``` + +The examples in this section run against the `users` / `posts` schema from the pipeline builder page. See its [example schema](/orm/next/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 result envelopes rather than decoded documents. Compare an `ObjectId` with `String(...)`, and read envelope 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. + +```ts +const plan = db.raw + .collection('posts') + .aggregate<{ _id: unknown; title: string }>([{ $match: { title: 'Hello world' } }]) + .build(); +const rows = await db.execute(plan); +// rows[0].title === 'Hello world' +// rows[0]._id is a raw ObjectId — String(rows[0]._id) is the hex string +``` + +### `insertOne()` and `insertMany()` + +Insert one or many documents. Both return the driver's insert envelope, with `ObjectId` instances for the generated ids. + +```ts +const onePlan = db.raw + .collection('users') + .insertOne({ name: 'Dave', email: 'dave@example.com', role: 'author' }) + .build(); +const [oneResult] = await db.execute(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 db.execute(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 document form + +```ts +const plan = db.raw + .collection('users') + .updateOne({ _id: new ObjectId(aliceId) }, { $set: { bio: 'Updated bio' } }) + .build(); +const [result] = await db.execute(plan); +// { matchedCount: 1, modifiedCount: 1, ... } +``` + +#### Pipeline form + +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 db.execute(plan); +// { matchedCount: 2, modifiedCount: 2, ... } +``` + +### `deleteOne()` and `deleteMany()` + +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 db.execute(onePlan); +// { deletedCount: 1 } + +const manyPlan = db.raw.collection('users').deleteMany({ role: 'author' }).build(); +const [manyResult] = await db.execute(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. + +#### 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. + +##### 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. + +```ts +const filter = { _id: 'pageViews' }; +const update = { $inc: { count: 1 }, $setOnInsert: { _id: 'pageViews' } }; +const counter = db.raw.collection('users'); + +const first = await db.execute(counter.findOneAndUpdate(filter, update, { upsert: true }).build()); +const second = await db.execute(counter.findOneAndUpdate(filter, update, { upsert: true }).build()); +const third = await db.execute(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 }] +``` + +### `findOneAndDelete()` + +Atomically delete a matching document and return it. The yielded row is the raw matched document itself, not an envelope, so its `_id` is a raw `ObjectId`. + +```ts +const plan = db.raw.collection('users').findOneAndDelete({ _id: new ObjectId(aliceId) }).build(); +const [deleted] = await db.execute(plan); +// deleted is the removed document; deleted._id is a raw ObjectId +``` + +## `db.query.rawCommand()` + +`db.query.rawCommand(command)` runs a raw MongoDB aggregate command through the [pipeline builder](/orm/next/reference/pipeline-builder), bypassing the typed AST. Its rows come back as `unknown`; you type them yourself. + +```ts +import { RawAggregateCommand } from '@prisma-next/mongo-query-ast/execution'; + +const plan = db.query.rawCommand(new RawAggregateCommand('posts', [{ $count: 'total' }])); +const rows = await db.execute(plan); +// [{ 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/next/reference/pipeline-builder#rawcommand) section. From b446ddb4a2f83a8e5337ca5244bdb6ba55486939 Mon Sep 17 00:00:00 2001 From: Nurul Sundarani Date: Tue, 7 Jul 2026 21:12:00 +0530 Subject: [PATCH 11/15] docs(next): add transactions and runtime reference Co-Authored-By: Claude Fable 5 --- .../reference/transactions-and-runtime.mdx | 790 +++++++++++++++++- 1 file changed, 789 insertions(+), 1 deletion(-) diff --git a/apps/docs/content/docs/orm/next/reference/transactions-and-runtime.mdx b/apps/docs/content/docs/orm/next/reference/transactions-and-runtime.mdx index 265d77973b..12be6c412c 100644 --- a/apps/docs/content/docs/orm/next/reference/transactions-and-runtime.mdx +++ b/apps/docs/content/docs/orm/next/reference/transactions-and-runtime.mdx @@ -7,4 +7,792 @@ metaDescription: Reference for the Prisma Next client lifecycle, transactions, p badge: early-access --- -Content landing in this PR. +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. + +The transaction surface is rich on PostgreSQL and not shipped on MongoDB. Where MongoDB has no equivalent, this page says so plainly and links the workaround, rather than showing an API that does not exist. + +Every PostgreSQL example is transcribed from an executable test suite that runs against a live database. MongoDB behavior is verified the same way, including the absence probes that confirm a surface does not exist. + +For a task-oriented walkthrough, see the Fundamentals guide to [Transactions](/orm/next/fundamentals/transactions). For the query surfaces you run inside a transaction, see the [ORM client reference](/orm/next/reference/orm-client), the [SQL query builder reference](/orm/next/reference/sql-query-builder), and the [raw queries reference](/orm/next/reference/raw-queries). + +## Client lifecycle — PostgreSQL + +Create a PostgreSQL client with `postgres(...)`, connect it to get a runtime, run queries, and close it when you are done. + +### `postgres(options)` + +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. + +#### Options + +| 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. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `PostgresClient` | `postgres({ contractJson, url })` | An unconnected client exposing `.orm`, `.sql`, `.enums`, `connect()`, `runtime()`, `transaction()`, `prepare()`, and `close()`. | + +#### Examples + +##### Create a client from a connection string + +```typescript +import postgres from '@prisma-next/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 }); +``` + +##### Bind to an existing `pg` pool + +```typescript +import postgres from '@prisma-next/postgres/runtime'; +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 db = postgres({ contractJson, pg: pool }); +// you own `pool`; close it yourself when the client is done +``` + +### `connect()` + +Open a connection and return a runtime. + +#### Remarks + +- Returns a `Promise`. The runtime is what executes plans directly (`runtime.execute(...)`), opens connections (`runtime.connection()`), and prepares statements (`runtime.prepare(...)`). +- Calling `connect()` after the client has been closed rejects with `Error: Postgres client is closed`. + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `Promise` | `const runtime = await db.connect()` | The connected runtime. | + +#### Examples + +##### Connect and run a query + +```typescript +const runtime = await db.connect(); +const tags = await runtime.execute(db.sql.public.tag.select('id', 'label').build()); +``` + +### `runtime()` + +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)). + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `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 `Error: Postgres client is closed`. +- When you bound the client with `pg` (your own pool or client), close that instance yourself; the client only owns pools it created from a `url`. + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `Promise` | `await db.close()` | Resolves when the client is closed. | + +#### Examples + +##### Close when finished + +```typescript +const db = postgres({ contractJson, url: process.env.DATABASE_URL }); +await db.connect(); +// ... run queries ... +await db.close(); +``` + +### `await using` (automatic disposal) + +The client implements `Symbol.asyncDispose`, so `await using` closes it automatically at the end of its block. + +#### 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 `Error: Postgres client is closed`. +- `await using` requires `@types/node` (which augments `SymbolConstructor` with `asyncDispose`) or a `lib` that declares the symbol. + +#### Examples + +##### Close automatically with `await using` + +```typescript +{ + await using db = postgres({ contractJson, url: process.env.DATABASE_URL }); + await db.connect(); + // ... run queries ... +} // db is closed here +``` + +## Client lifecycle — MongoDB + +Create a MongoDB client with `mongo(...)`. The entry point and several lifecycle details differ from PostgreSQL, most notably that `runtime()` is asynchronous. + +### `mongo(options)` + +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 that includes the database name in its path), `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 Next creates the underlying `MongoClient` and closes it on `close()`. With `mongoClient`, you supplied the client, so Prisma Next 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 Next and driver-level code. +- `mongo(options)` does not open a connection. The runtime is built lazily on first use, or explicitly via [`connect()`](#connect-1). + +#### Options + +| 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 with the database in its path. | +| `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. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `MongoClient` | `mongo({ contractJson, url, dbName })` | An unconnected client exposing `.orm`, `.query`, `.raw`, `.enums`, `execute()`, `connect()`, `runtime()`, and `close()`. | + +#### Examples + +##### Create a client from a connection string + +```typescript +import mongo from '@prisma-next/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' }); +``` + +##### Share an existing `MongoClient` + +```typescript +import mongo from '@prisma-next/mongo/runtime'; +import { MongoClient } from 'mongodb'; +import type { Contract } from './contract.d'; +import contractJson from './contract.json' with { type: 'json' }; + +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()` + +Open a connection and return a runtime. + +#### Remarks + +- Returns a `Promise`. +- Calling `connect()` when the client is already connected rejects with `Error: Mongo client already connected`. The first connection can happen implicitly, the moment any query builds the runtime, so a later explicit `connect()` collides the same way. +- After the client has been closed, `connect()` rejects with `Error: Mongo client is closed`. + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `Promise` | `const runtime = await db.connect()` | The connected runtime (`execute` + `close` only). | + +#### Examples + +##### Connecting twice throws + +```typescript +await db.connect(); +await db.connect(); // throws Error: Mongo client 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 `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 + +| Return type | Example | Description | +|---|---|---| +| `Promise` | `const runtime = await db.runtime()` | The runtime, resolved asynchronously. | + +#### Examples + +##### Await the runtime + +```typescript +const runtime = await db.runtime(); +``` + +### `execute()` + +Run a plan through the client. + +#### Remarks + +- `db.execute(plan)` runs any `MongoQueryPlan`, including a plan built by the [pipeline builder](/orm/next/reference/pipeline-builder) (`db.query`). +- Returns an [`AsyncIterableResult`](#asynciterableresult): `await` it for an array, or `for await` to stream rows. + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `AsyncIterableResult` | `await db.execute(plan)` | The plan's rows. | + +#### Examples + +##### Execute a pipeline-builder plan + +```typescript +const plan = db.query.from('posts').build(); +const posts = await db.execute(plan); +``` + +### `close()` + +Close the client. + +#### Remarks + +- Returns a `Promise`. +- After `close()`, any further use throws `Error: 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)). + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `Promise` | `await db.close()` | Resolves when the client is closed. | + +#### Examples + +##### Use after close throws + +```typescript +await db.close(); +await db.runtime(); // throws Error: Mongo client is closed +``` + +### `await using` (automatic disposal) + +The MongoDB client implements `Symbol.asyncDispose` too, so `await using` closes it at the end of its block. + +#### Remarks + +- Disposal fires at the end of the block, exactly as on PostgreSQL. +- After the block exits, a later `connect()` rejects with `Error: Mongo client is closed`. + +#### Examples + +##### Close automatically with `await using` + +```typescript +{ + 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 Next offers three levels of control, from the high-level `db.transaction(...)` facade down to a manual connection you commit yourself. + +### `db.transaction(callback)` + +Run a callback inside a transaction. The transaction commits when the callback returns and rolls back when it throws. + +#### Remarks + +- Inside the callback, query through the `tx` handle, not `db`: `tx.orm` for models, `tx.sql` and `tx.execute(...)` 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.
`. +- Reads inside the transaction see the transaction's own uncommitted writes. +- The callback's return value passes through as the result of `db.transaction(...)`. +- Do not let an [`AsyncIterableResult`](#asynciterableresult) escape the callback unread. A result you return and then read after the transaction has ended rejects with `RUNTIME.TRANSACTION_CLOSED` (see the Remark below). + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `callback` | `(tx) => Promise` | Yes | The transactional work. `tx` exposes `orm`, `sql`, `execute`, and `enums`. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `Promise` | `await db.transaction(async (tx) => ...)` | Whatever the callback returns. | + +#### Examples + +##### Commit several writes atomically + +```typescript +await db.transaction(async (tx) => { + await tx.orm.public.Tag.create({ label: 'tx-commit-a' }); + await tx.orm.public.Tag.create({ label: 'tx-commit-b' }); +}); +// both tags exist now +``` + +##### Roll back when the callback throws + +```typescript +try { + await db.transaction(async (tx) => { + await tx.orm.public.Tag.create({ label: 'tx-rollback' }); + throw new Error('deliberate rollback'); + }); +} catch { + // the tag was rolled back and does not exist +} +``` + +##### Read your own uncommitted writes + +```typescript +const { createdId, found } = await db.transaction(async (tx) => { + const created = await tx.orm.public.Tag.create({ label: 'tx-ryow' }); + const found = await tx.orm.public.Tag.where({ label: 'tx-ryow' }).first(); + return { createdId: created.id, found }; +}); +// found.id === createdId +``` + +##### Run a SQL builder plan with `tx.sql` and `tx.execute` + +```typescript +await db.transaction(async (tx) => { + await tx.execute( + tx.sql.public.tag.insert([{ id: crypto.randomUUID(), label: 'tx-sql-insert' }]).build(), + ); +}); +``` + +:::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. + +```typescript +const escaped = await db.transaction(async (tx) => { + await tx.orm.public.Tag.create({ label: 'tx-escape' }); + return { rows: tx.execute(tx.sql.public.tag.select('label').build()) }; +}); + +await escaped.rows.toArray(); // rejects with RUNTIME.TRANSACTION_CLOSED +``` +::: + +For Prisma 7 users, an array of queries becomes a callback: + +```diff +- const [user, post] = await prisma.$transaction([ +- prisma.user.create({ data: { email, displayName } }), +- prisma.post.create({ data: { title, userId } }), +- ]); ++ const { user, post } = await db.transaction(async (tx) => { ++ const user = await tx.orm.public.User.create({ email, displayName, kind: 'user' }); ++ const post = await tx.orm.public.Post.create({ title, userId: user.id }); ++ return { user, post }; ++ }); +``` + +The callback form does something the array form never could: one query's result (here `user.id`) can feed the next query in the same transaction. + +### `withTransaction(runtime, callback)` + +A lower-level transaction helper you import directly, without going through the client facade. + +#### Remarks + +- Imported from `@prisma-next/sql-runtime`. +- The callback receives a bare transaction context whose surface is `execute` (and `executePrepared`) 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.execute(...)`. +- Commits on return, rolls back on throw, the same as `db.transaction(...)`. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `runtime` | `Runtime` | Yes | The runtime to open the transaction on. | +| `callback` | `(tx) => Promise` | Yes | The transactional work. `tx` exposes `execute`. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `Promise` | `await withTransaction(runtime, async (tx) => ...)` | Whatever the callback returns. | + +#### Examples + +##### Commit two writes with `withTransaction` + +```typescript +import { withTransaction } from '@prisma-next/sql-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-next/sql-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'); + }); +} catch { + // the tag was rolled back +} +``` + +### 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. + +#### Remarks + +- `runtime.connection()` returns a dedicated connection. `connection.transaction()` opens a transaction on it. Run plans with `transaction.execute(...)`, 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. + +#### 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. | + +#### Examples + +##### Commit 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-commit' }]).build(), +); +await transaction.commit(); +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(); +``` + +##### Evict a connection with `destroy()` + +```typescript +const connection = await runtime.connection(); +await connection.destroy(new Error('connection no longer trusted')); +// the runtime stays healthy and opens a replacement connection on the next query +``` + +## Transactions (MongoDB) + +:::note[MongoDB transactions are not available in Prisma Next 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 Next are not shipped. + +To run a multi-document MongoDB transaction today, use the MongoDB driver directly, sharing one `MongoClient` between Prisma Next and your driver code. See the [Fundamentals transactions guide](/orm/next/fundamentals/transactions#transactions-on-mongodb) for the driver-session pattern. +::: + +A transaction surface for MongoDB is planned (source-read from an internal design document, not shipped): a `db.transaction(fn)` facade with `tx.orm` and `tx.query`, backed by a driver session. As designed, it will require a MongoDB replica set and must not silently degrade to non-transactional execution on a standalone server. Treat all of this as planned design, not current behavior. Nothing in this planned surface exists in the shipped packages today, so this page shows no example code for it. + +## 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. + +### `runtime.prepare(declaration, callback)` + +Prepare a statement off a runtime. + +#### Remarks + +- The declaration maps each parameter name to a codec id (for example `{ label: 'pg/text@1' }`). +- The callback receives the declared `params` and returns a plan. It takes a **single** argument (`(params) => plan`); build the plan with a SQL builder you already hold in scope (such as `db.sql`). +- A declared parameter that the callback never uses is rejected at prepare time with `RUNTIME.PREPARE_UNUSED_PARAM` (with `details.unused` listing the offending names). The rejection happens at `prepare()`, before any execution. +- The resulting `PreparedStatement` runs via `ps.execute(target, params)`; see [`PreparedStatement.execute`](#preparedstatementexecutetarget-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. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `Promise` | `await runtime.prepare({ label: 'pg/text@1' }, (params) => ...)` | A reusable prepared statement. | + +#### Examples + +##### Prepare and run a statement + +```typescript +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(), +); + +const typescript = await ps.execute(runtime, { label: 'typescript' }); +const missing = await ps.execute(runtime, { label: 'does-not-exist' }); +// typescript has one row; missing has none +``` + +##### An unused declared parameter is rejected at prepare time + +```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(), +); +// rejects with RUNTIME.PREPARE_UNUSED_PARAM, details: { unused: ['unused'] } +``` + +### `db.prepare(declaration, callback)` + +Prepare a statement off the client facade. + +#### 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.
`. + +#### 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. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `Promise` | `await db.prepare({ label: 'pg/text@1' }, (sql, params) => ...)` | A reusable prepared statement. | + +#### Examples + +##### Prepare off the client with the injected `sql` builder + +```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(), +); + +const runtime = await db.connect(); +const rows = await ps.execute(runtime, { label: 'typescript' }); +``` + +### `PreparedStatement.execute(target, params)` + +Run a prepared statement against a target, with the parameter values. + +#### Remarks + +- 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. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `target` | `Runtime`, connection, or transaction | Yes | Where to run the statement. | +| `params` | Object of the declared parameter values | Yes | The values for this execution. | + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| `Promise` | `await ps.execute(runtime, { label })` | The statement's rows. | + +#### Examples + +##### Run one prepared statement against a transaction and the runtime + +```typescript +import { withTransaction } from '@prisma-next/sql-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(), +); + +const insertedId = await withTransaction(runtime, async (tx) => { + await tx.execute(db.sql.public.tag.insert([{ id: crypto.randomUUID(), label: 'ps-both-targets' }]).build()); + const inTx = await ps.execute(tx, { label: 'ps-both-targets' }); // runs against the transaction + return inTx[0]!.id; +}); + +const committed = await ps.execute(runtime, { label: 'ps-both-targets' }); // runs against the runtime +// committed[0].id === insertedId +``` + +## Execution options and results + +Every runtime `execute(...)` accepts a `RuntimeExecuteOptions` object as its second argument. + +### `RuntimeExecuteOptions` + +Per-query options for cancellation and scope. + +#### Remarks + +- `signal` is an `AbortSignal` for per-query cancellation. An already-aborted signal short-circuits with `RUNTIME.ABORTED` before any rows stream (verified); per the runtime source, the signal is also threaded through codec calls and checked between rows mid-stream. +- A signal that is **already aborted** when you call `execute(...)` short-circuits before any query runs, rejecting with `RUNTIME.ABORTED` and `details.phase` of `'stream'`. This is the first thing the execute path checks, before any network round-trip. +- 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. + +#### Options + +| Name | Type | Required | Description | +|---|---|---|---| +| `signal` | `AbortSignal` | No | Per-query cancellation signal. | +| `scope` | `'runtime' \| 'connection' \| 'transaction'` | No | The execution scope for the query. | + +#### Examples + +##### A pre-aborted signal short-circuits + +```typescript +const controller = new AbortController(); +controller.abort(new Error('cancelled')); + +await runtime.execute(db.sql.public.tag.select('id').limit(1).build(), { + signal: controller.signal, +}); +// rejects with RUNTIME.ABORTED, details: { phase: 'stream' } +``` + +### `runtime.telemetry()` + +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. + +#### Return type + +| Return type | Example | Description | +|---|---|---| +| Telemetry object or `null` | `runtime.telemetry()` | The most recent query's telemetry, or `null` before any query. | + +#### Examples + +##### Read telemetry before and after a query + +```typescript +const before = runtime.telemetry(); // null + +await runtime.execute(db.sql.public.tag.select('id').limit(1).build()); + +const after = runtime.telemetry(); +// { lane, target: 'postgres', fingerprint, outcome: 'success', durationMs? } +``` + +### `AsyncIterableResult` + +Read terminals (`all()`, `createAll()`, and the client's `execute(...)`) return an `AsyncIterableResult`: `await` it to collect an array, or `for await` to stream rows one at a time. + +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/next/reference/orm-client#asynciterableresult) in the ORM client reference. + +## Prompt your coding agent + +Projects scaffolded with `create-prisma` install Prisma Next skills for your coding agent; the `prisma-next-queries` skill covers transactions. Prompts that map to sections on this page: + +- "Using the prisma-next-queries skill, wrap these two writes in a `db.transaction` so they commit together and roll back on error." +- "Rewrite this manual `runtime.connection()` / `transaction()` block to use `db.transaction(...)` instead." +- "Prepare this repeated lookup query with `runtime.prepare(...)` and run it against both the runtime and a transaction." +- "This project is on MongoDB and needs a multi-document atomic write. Show me the driver-session pattern with a shared MongoClient, since Prisma Next has no `db.transaction` for MongoDB yet." + +## Next + +- [Transactions](/orm/next/fundamentals/transactions): the task-oriented guide, including the MongoDB driver-session workaround. +- [ORM client reference](/orm/next/reference/orm-client): the model methods you call on `tx.orm`. +- [SQL query builder reference](/orm/next/reference/sql-query-builder): the plans you run with `tx.sql` and `tx.execute`. +- [Raw queries reference](/orm/next/reference/raw-queries): raw SQL and MongoDB escape hatches. From ecace7ee0097894c058786751d4dbea8618c0068 Mon Sep 17 00:00:00 2001 From: Nurul Sundarani Date: Tue, 7 Jul 2026 21:31:09 +0530 Subject: [PATCH 12/15] docs(next): address phase-2 final review findings Co-Authored-By: Claude Fable 5 --- .../content/docs/orm/next/reference/index.mdx | 2 +- .../orm/next/reference/pipeline-builder.mdx | 16 +++++++++++-- .../docs/orm/next/reference/raw-queries.mdx | 2 +- .../orm/next/reference/sql-query-builder.mdx | 2 +- .../reference/transactions-and-runtime.mdx | 23 +++---------------- 5 files changed, 20 insertions(+), 25 deletions(-) diff --git a/apps/docs/content/docs/orm/next/reference/index.mdx b/apps/docs/content/docs/orm/next/reference/index.mdx index e3ec8e886a..4c1da1410d 100644 --- a/apps/docs/content/docs/orm/next/reference/index.mdx +++ b/apps/docs/content/docs/orm/next/reference/index.mdx @@ -13,7 +13,7 @@ Use the ORM client for everyday application queries across models and relations. ## How database differences are documented -The reference pages document every method with the classic Remarks / Options / Return type / Examples structure. 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. +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. For a conceptual walkthrough of reading, writing, and querying data (rather than an exhaustive method-by-method reference), see the [Fundamentals](/orm/next/fundamentals/reading-data) section: diff --git a/apps/docs/content/docs/orm/next/reference/pipeline-builder.mdx b/apps/docs/content/docs/orm/next/reference/pipeline-builder.mdx index 9dec335f93..228538589e 100644 --- a/apps/docs/content/docs/orm/next/reference/pipeline-builder.mdx +++ b/apps/docs/content/docs/orm/next/reference/pipeline-builder.mdx @@ -147,8 +147,9 @@ Filter documents by a predicate. #### Remarks -- `match()` takes a callback that receives a field accessor and returns a filter expression (`(f) => f.kind.eq('tutorial'))`. +- `match()` 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). #### Options @@ -188,7 +189,7 @@ const recent = await db.execute(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`. The type system already steers you away (the value type does not include `ObjectId`), so reaching this at all requires a cast. +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. 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/next/reference/orm-client), which handles `_id` encoding for you (`db.orm.posts.where({ _id })`). ::: @@ -622,6 +623,7 @@ Several stages take a single MongoDB stage options object rather than a typed fi #### 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. @@ -853,6 +855,16 @@ const plan = db.query const rows = await db.execute(plan); ``` +### `pipe()` + +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 A read terminal turns the chain into an executable plan. Pass the plan to `db.execute(plan)` to run it. diff --git a/apps/docs/content/docs/orm/next/reference/raw-queries.mdx b/apps/docs/content/docs/orm/next/reference/raw-queries.mdx index 3a4c5d6679..e93f5f73f7 100644 --- a/apps/docs/content/docs/orm/next/reference/raw-queries.mdx +++ b/apps/docs/content/docs/orm/next/reference/raw-queries.mdx @@ -144,7 +144,7 @@ TypeScript already rejects an unsupported value at compile time. If you need to `db.raw.collection(rootName)` returns a raw collection with nine methods for running MongoDB commands directly against a collection. The root name is the contract's lowercase plural root (`'users'`, `'posts'`), the same name the ORM client uses (`db.orm.users`). -Each method returns a buildable command: call `.build()` to get a plan, then run it with `db.execute(plan)`. +Each method returns a buildable command: call `.build()` to get a plan, then run it with `db.execute(plan)`. Because raw filters carry native BSON values, the examples below construct ids with the driver's `ObjectId` class: ```ts import { ObjectId } from 'mongodb'; diff --git a/apps/docs/content/docs/orm/next/reference/sql-query-builder.mdx b/apps/docs/content/docs/orm/next/reference/sql-query-builder.mdx index 6cfe19829b..df4fc934d6 100644 --- a/apps/docs/content/docs/orm/next/reference/sql-query-builder.mdx +++ b/apps/docs/content/docs/orm/next/reference/sql-query-builder.mdx @@ -923,7 +923,7 @@ There is no `fns.coalesce` or `fns.cast`. Express `COALESCE`, `CAST`, and any ot ### `fns.raw` and `.returns()` -Write a raw SQL fragment as a tagged template. Interpolate columns and values with `${...}`; each interpolation is parameterized. Call `.returns(codecId)` to declare the fragment's result type. +Write a raw SQL fragment as a tagged template. Interpolate columns and typed expressions with `${...}`; each interpolation is parameterized. For bare JavaScript values, always wrap them in `param(...)` with an explicit codec id — bare-scalar interpolation is currently broken on PostgreSQL (see the [raw queries reference](/orm/next/reference/raw-queries#binding-a-bare-value-with-param) for the verified details). Call `.returns(codecId)` to declare the fragment's result type. #### Options diff --git a/apps/docs/content/docs/orm/next/reference/transactions-and-runtime.mdx b/apps/docs/content/docs/orm/next/reference/transactions-and-runtime.mdx index 12be6c412c..baa1361204 100644 --- a/apps/docs/content/docs/orm/next/reference/transactions-and-runtime.mdx +++ b/apps/docs/content/docs/orm/next/reference/transactions-and-runtime.mdx @@ -179,7 +179,7 @@ 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 that includes the database name in its path), `uri` plus an explicit `dbName`, or `mongoClient` (an existing `MongoClient` from the `mongodb` package) plus an explicit `dbName`. +- 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 Next creates the underlying `MongoClient` and closes it on `close()`. With `mongoClient`, you supplied the client, so Prisma Next 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 Next 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). @@ -188,7 +188,7 @@ Create a MongoDB client. | 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 with the database in its path. | +| `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. | @@ -720,8 +720,7 @@ Per-query options for cancellation and scope. #### Remarks -- `signal` is an `AbortSignal` for per-query cancellation. An already-aborted signal short-circuits with `RUNTIME.ABORTED` before any rows stream (verified); per the runtime source, the signal is also threaded through codec calls and checked between rows mid-stream. -- A signal that is **already aborted** when you call `execute(...)` short-circuits before any query runs, rejecting with `RUNTIME.ABORTED` and `details.phase` of `'stream'`. This is the first thing the execute path checks, before any network round-trip. +- `signal` is an `AbortSignal` for per-query cancellation. A signal that is **already aborted** when you call `execute(...)` 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. @@ -780,19 +779,3 @@ const after = runtime.telemetry(); Read terminals (`all()`, `createAll()`, and the client's `execute(...)`) return an `AsyncIterableResult`: `await` it to collect an array, or `for await` to stream rows one at a time. 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/next/reference/orm-client#asynciterableresult) in the ORM client reference. - -## Prompt your coding agent - -Projects scaffolded with `create-prisma` install Prisma Next skills for your coding agent; the `prisma-next-queries` skill covers transactions. Prompts that map to sections on this page: - -- "Using the prisma-next-queries skill, wrap these two writes in a `db.transaction` so they commit together and roll back on error." -- "Rewrite this manual `runtime.connection()` / `transaction()` block to use `db.transaction(...)` instead." -- "Prepare this repeated lookup query with `runtime.prepare(...)` and run it against both the runtime and a transaction." -- "This project is on MongoDB and needs a multi-document atomic write. Show me the driver-session pattern with a shared MongoClient, since Prisma Next has no `db.transaction` for MongoDB yet." - -## Next - -- [Transactions](/orm/next/fundamentals/transactions): the task-oriented guide, including the MongoDB driver-session workaround. -- [ORM client reference](/orm/next/reference/orm-client): the model methods you call on `tx.orm`. -- [SQL query builder reference](/orm/next/reference/sql-query-builder): the plans you run with `tx.sql` and `tx.execute`. -- [Raw queries reference](/orm/next/reference/raw-queries): raw SQL and MongoDB escape hatches. From 501a353f7e69a2f60f0495fb6fc31f40454e35e6 Mon Sep 17 00:00:00 2001 From: Nurul Sundarani Date: Tue, 7 Jul 2026 21:45:41 +0530 Subject: [PATCH 13/15] docs(next): drop the coming-to-this-reference note from the index Co-Authored-By: Claude Fable 5 --- apps/docs/content/docs/orm/next/reference/index.mdx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/apps/docs/content/docs/orm/next/reference/index.mdx b/apps/docs/content/docs/orm/next/reference/index.mdx index 4c1da1410d..5f8b86d372 100644 --- a/apps/docs/content/docs/orm/next/reference/index.mdx +++ b/apps/docs/content/docs/orm/next/reference/index.mdx @@ -27,14 +27,6 @@ For a conceptual walkthrough of reading, writing, and querying data (rather than Prisma Next ships first-class support for PostgreSQL and MongoDB today. SQLite is the next SQL target on deck, with MySQL to follow. ::: -## Coming to this reference - -These surfaces are planned but not yet documented here: - -:::note -- Middleware hook APIs (for concepts and the built-in middleware, see [How middleware works](/orm/next/middleware/how-middleware-works)) -::: - }> Every ORM client method, with PostgreSQL and MongoDB behavior documented side by side. From 0ab170ea7aa2ee7e77ea7ab438644aa275d87b24 Mon Sep 17 00:00:00 2001 From: Nurul Sundarani Date: Tue, 7 Jul 2026 21:48:52 +0530 Subject: [PATCH 14/15] docs(next): drop SQLite-on-deck notes from the reference Co-Authored-By: Claude Fable 5 --- apps/docs/content/docs/orm/next/reference/index.mdx | 4 ---- .../content/docs/orm/next/reference/sql-query-builder.mdx | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/apps/docs/content/docs/orm/next/reference/index.mdx b/apps/docs/content/docs/orm/next/reference/index.mdx index 5f8b86d372..e6a9e0ae8b 100644 --- a/apps/docs/content/docs/orm/next/reference/index.mdx +++ b/apps/docs/content/docs/orm/next/reference/index.mdx @@ -23,10 +23,6 @@ For a conceptual walkthrough of reading, writing, and querying data (rather than - [Transactions](/orm/next/fundamentals/transactions) - [Advanced queries](/orm/next/fundamentals/advanced-queries) -:::note[SQLite is next on deck] -Prisma Next ships first-class support for PostgreSQL and MongoDB today. SQLite is the next SQL target on deck, with MySQL to follow. -::: - }> Every ORM client method, with PostgreSQL and MongoDB behavior documented side by side. diff --git a/apps/docs/content/docs/orm/next/reference/sql-query-builder.mdx b/apps/docs/content/docs/orm/next/reference/sql-query-builder.mdx index df4fc934d6..d8de1ab4d3 100644 --- a/apps/docs/content/docs/orm/next/reference/sql-query-builder.mdx +++ b/apps/docs/content/docs/orm/next/reference/sql-query-builder.mdx @@ -12,7 +12,7 @@ The SQL query builder gives you table-level, SQL-shaped methods for building typ 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/next/fundamentals/advanced-queries). :::note[This is the SQL-family builder] -The SQL query builder targets SQL databases. PostgreSQL is supported today, with SQLite next on deck. MongoDB has no SQL builder: use the [ORM client](/orm/next/reference/orm-client) for MongoDB queries today, and the MongoDB pipeline builder (planned) for aggregation pipelines. +The SQL query builder targets SQL databases; PostgreSQL is supported today. MongoDB has no SQL builder: use the [ORM client](/orm/next/reference/orm-client) for MongoDB queries, and the [pipeline builder](/orm/next/reference/pipeline-builder) for aggregation pipelines. ::: ## Example schema From 7004d80c1ff514b407ba50498771d8c8f5213cf0 Mon Sep 17 00:00:00 2001 From: Nurul Sundarani Date: Wed, 8 Jul 2026 17:04:32 +0530 Subject: [PATCH 15/15] chore(cspell): update cspell configuration with additional terms --- apps/docs/cspell.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/docs/cspell.json b/apps/docs/cspell.json index 7f396d0fc3..daa224156d 100644 --- a/apps/docs/cspell.json +++ b/apps/docs/cspell.json @@ -134,6 +134,7 @@ "icfg", "ilike", "ILIKE", + "inferer", "initialise", "Inno", "inshellisense", @@ -187,6 +188,7 @@ "Millis", "moddatetime", "mongosh", + "mongot", "multilinestring", "multipoint", "multipolygon", @@ -296,11 +298,13 @@ "refint", "regclass", "reintrospection", + "relinks", "Replibyte", "Replit", "rootca", "RRFFQ", "RSPCA", + "ryow", "s3cret", "Sabelle", "safeql", @@ -335,8 +339,10 @@ "sslpassword", "sslrootcert", "Stammerjohann", + "streamable", "streamdal", "Streamdal", + "subclassing", "Subtacts", "Sunsetting", "supabase", @@ -392,6 +398,7 @@ "untick", "uploadthing", "UPLOADTHING", + "upserted", "upserting", "Upserting", "upserts",