SQL query builder reference
Reference for the Prisma ORM SQL query builder's select, mutation, and grouped query methods.
The SQL query builder builds typed queries from a table, with one method per SQL clause: select(), innerJoin(), groupBy(), and the rest. It is closer to SQL than the ORM client is, and the same db gives you both. Reach for it when you need a join, aggregate, or SQL feature the ORM client doesn't expose, or when you want direct control over the generated SQL.
Some methods work only on some databases, and some fail only when the query runs. For a task-oriented guide to when and how to reach for the builder, see Advanced queries.
The SQL query builder targets SQL databases; PostgreSQL and SQLite are supported today. MongoDB has no SQL builder: use the ORM client for MongoDB queries, and the pipeline builder for aggregation pipelines.
Example schema
The examples run against the schema below and use the user, post, post_tag, and tag tables. The grouped-query examples use a separate customer and order schema, shown under Grouped queries. PostTag is the join table. The two list fields and the PostTag model together are how Prisma ORM 8 declares a many-to-many, which Relations and joins explains. For the schema syntax itself, see Contract authoring.
Expand for the example schema
types {
Embedding1536 = pgvector.Vector(1536)
}
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
In Prisma ORM 8 your schema.prisma is called contract.prisma. Coming from Prisma ORM 7 gets you from a Prisma ORM 7 project to that file, and has the rest of the setup steps. Install the client with npm install @prisma/orm-postgres. Run npx prisma contract emit to write contract.json and contract.d.ts beside contract.prisma. The examples below assume your own file is in src/prisma/, next to those two files, and they write the type import path as ./contract.d, exactly like that.
You build queries from the client's sql property. Create a Postgres client with postgres(...), then reach tables through db.sql.public. public is the PostgreSQL schema your tables are in. A model goes in public unless your contract puts it in a namespace block. Table accessors are keyed by table name, not model name. A table name is the model name with a lowercase first letter unless the model sets @@map, so the User model mapped to user is reached as db.sql.public.user, and the model for the Post/Tag join table, mapped to post_tag, is reached as db.sql.public.post_tag. Column names are the field names unless a field sets @map.
The db.sql builder
db.runtime() returns the connection that runs a built query. On PostgreSQL it is synchronous, so no await is needed. Hold it in a variable and reuse it. When your app shuts down, close the client with db.close().
A select(), insert(), update(), or delete() call starts a query. build() turns the chain into a query object, which the examples name plan. Nothing touches the database until you pass that object to runtime.query(...). You can run the same built query more than once.
import postgres from '@prisma/orm-postgres/runtime';
import type { Contract } from './contract.d';
import contractJson from './contract.json' with { type: 'json' };
const db = postgres<Contract>({ contractJson, url: process.env.DATABASE_URL });
const runtime = db.runtime();
const plan = db.sql.public.user.select('id', 'email').build();
const users = await runtime.query(plan);SQLite works the same way. Create the client with sqlite(...) from @prisma/orm-sqlite/runtime. SQLite has no schemas, so there is no public in the path and the same table is db.sql.user. Of the SELECT methods on this page, lateralJoin(), outerLateralJoin(), and distinctOn() are not available on SQLite. Every other one is. The rest of this page shows PostgreSQL and writes the root as db.sql.public.
Table access
Every table on db.sql.public exposes the query-building methods.
db.sql.public.user.select('id', 'email'); // start a SELECT
db.sql.public.tag.insert([{ label: 'typescript' }]); // start an INSERTAliasing a query with .as()
Call .as(alias) on a SELECT chain before build() to use that query as a subquery. The aliased query is then something you can pass to innerJoin(), outerLeftJoin(), and the other join methods. Do not call .as() inside a lateralJoin() callback. For the example, see Subquery via .as().
SELECT queries
These methods build and refine a SELECT. They chain, and you resolve the query with build() followed by runtime.query(...). Chain the methods in any order, with two exceptions. A join must come before any call that names the joined table's columns. The columns you pass to distinctOn() must be the first sort keys in orderBy(). SelectQuery in the tables below is the type they return.
The set of column names a query can use is its scope. It starts as the columns of the table you began with, and every join adds the joined table's columns to it.
The where(), select(), orderBy(), update(), and other callback forms receive two arguments. The first is f, which has one property per column in scope. After a join, every column is on f under its table name, such as f.user.email or f.post.id. A column name that only one table has is also on f directly. The second is fns, an object of expression helpers such as fns.eq() and fns.raw. For an enum column you pass the value stored in the database. That is the string after =, so High = "high" is matched by fns.eq(f.priority, 'high'). A member with no =, such as admin in user_type, is matched by its own name, fns.eq(f.kind, 'admin').
select()
Reduce each row 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. A name that is not in the query's scope throws an error whose code is ORM.COLUMN_UNKNOWN. |
| Aliased expression | select(alias, (f, fns) => expr) | Add one computed column under alias. |
| Object of expressions | select((f, fns) => ({ alias: expr, ... })) | Add several computed columns at once. The row type is inferred from the object's shape. |
Every form adds to what the query already returns. Calling select() a second time never replaces the first call's columns, whichever form you use. To return fewer columns, start a new query instead.
A computed expression's result type comes from .returns(...) on fns.raw, or from the fns.* function you called. Every fns.raw fragment needs .returns(). A type id is pg/ plus the PostgreSQL type name plus a version, always @1 today, such as pg/int4@1 or pg/uuid@1. A few use a longer name, such as pg/timestamptz-temporal@1 for DateTime. The common ones are pg/text@1, pg/int4@1, pg/int8@1, pg/float8@1, pg/bool@1, pg/uuid@1, and pg/timestamptz-temporal@1. The full list is what your contract uses. Read any column's id from db.sql.public.<table>.columns.<column>.codecId, and see Binding a bare value with param() for which JavaScript type becomes which id. See fns.raw and .returns().
Return type
| Return type | Example | Description |
|---|---|---|
SelectQuery | db.sql.public.user.select('id', 'email') | A query projected to the named columns or expressions, chainable and buildable. |
Examples
The examples on this page use these ids, which stand for rows that exist:
const aliceId = '00000000-0000-4000-8000-000000000001';
const carolId = '00000000-0000-4000-8000-000000000003';
const helloWorldId = '00000000-0000-4000-8000-000000000010';
const untaggedPostId = '00000000-0000-4000-8000-000000000011';
const newestPostId = '00000000-0000-4000-8000-000000000012';Project a subset of columns
const plan = db.sql.public.user.select('id', 'email').build();
const rows = await runtime.query(plan);
// rows[0] is { id, email }, with no displayNameAdd an aliased computed column
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))
.build();
const rows = await runtime.query(plan);Project multiple computed columns at once
const plan = db.sql.public.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.query(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 f and fns. Combine comparisons with fns.and(...) and fns.or(...), nested freely. |
Return type
| Return type | Example | Description |
|---|---|---|
SelectQuery | db.sql.public.post.where(...) | A query narrowed by the predicate. |
Examples
Combine comparisons with and() and or()
const plan = db.sql.public.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.query(plan);innerJoin()
Combine matching rows from two tables. Every column is then on f under its table name, such as f.user.email or f.post.id. A column name that only one table has is also on f directly.
Options
| Name | Type | Required | Description |
|---|---|---|---|
other | A table (db.sql.public.<table>) or an aliased subquery (.as()) | Yes | The table or subquery to join. |
on | (f, fns) => Expression | Yes | The join condition. |
Return type
| Return type | Example | Description |
|---|---|---|
SelectQuery | db.sql.public.post.innerJoin(db.sql.public.user, ...) | A query over the joined tables, with both tables' columns in scope under their table names. |
Examples
Join posts to their authors
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();
const rows = await runtime.query(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() does. outerLeftJoin() is SQL's LEFT OUTER JOIN and keeps every left-table row. outerRightJoin() is RIGHT OUTER JOIN and keeps every right-table row. outerFullJoin() is FULL OUTER JOIN and keeps unmatched rows from both sides.
Examples
Left join keeps rows with no match
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();
const rows = await runtime.query(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. Use it for a query such as the newest post for each user.
Remarks
- Availability: PostgreSQL only. On SQLite TypeScript rejects the call. If you cast, or call it from plain JavaScript, it throws an error whose
codeisORM.CAPABILITY_MISSING. - The callback receives a
lateralbuilder. Start its subquery withlateral.from(otherTable), then chain the usualSELECTmethods. Inside the callback, the outer table's columns are onfunder its table name, such asf.user.id, so the subquery can filter on the outer row. - Return the query chain directly from the callback. Do not call
.as(...)on it. The first argument oflateralJoin()already names the subquery. - Every column is on
funder its table name, and a column name that only one table has is also onfdirectly. Outside the callback, the subquery's columns are under the alias you gave it, such asf.latestPost.id. Inside the callback, the joined table is stillpost, so the same column isf.post.id. Because both tables haveidandcreatedAt, a bareselect('id')ororderBy((f) => f.createdAt, ...)throws. outerLateralJoin(alias, callback)is theLEFT JOIN LATERALform: same arguments, and rows with no match keep the outer row.
Options
| Name | Type | Required | Description |
|---|---|---|---|
alias | string | Yes | Names the subquery. Address its columns as f.<alias>.<col> 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 | 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
const plan = db.sql.public.user
.lateralJoin('latestPost', (lateral) =>
lateral
.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' })
.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.query(plan);
// rows === [{ userId: aliceId, latestPostId: newestPostId }]orderBy()
Sort the result set by a column or a computed expression, in a direction you choose.
Options
| Name | Type | Required | Description |
|---|---|---|---|
key | Column name (string) or (f, fns) => Expression | Yes | The column or computed value to sort by. |
options.direction | 'asc' | 'desc' | No | Sort direction. |
TypeScript also accepts a nulls option. It does nothing, and no NULLS FIRST or NULLS LAST reaches the SQL. To put nulls last, sort by a computed value first, then by the column:
db.sql.public.user
.orderBy((f, fns) => fns.raw`CASE WHEN ${f.zip} IS NULL THEN 1 ELSE 0 END`.returns('pg/int4@1'))
.orderBy('zip', { direction: 'asc' });Return type
| Return type | Example | Description |
|---|---|---|
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
const plan = db.sql.public.user.select('id', 'email').orderBy('email', { direction: 'asc' }).build();
const rows = await runtime.query(plan);Sort by a computed value
const plan = db.sql.public.user
.select('id', 'email')
.orderBy((f, fns) => fns.raw`LENGTH(${f.email})`.returns('pg/int4@1'), { direction: 'asc' })
.build();
const rows = await runtime.query(plan);distinct()
De-duplicate identical projected rows (SELECT DISTINCT).
Return type
| Return type | Example | Description |
|---|---|---|
SelectQuery | db.sql.public.post.select('priority').distinct() | A query returning only distinct projected rows. |
Examples
De-duplicate projected rows
const plan = db.sql.public.post.select('priority').distinct().build();
const rows = await runtime.query(plan);
// distinct priorities: ['high', 'low', 'urgent']distinctOn()
Keep the first row per distinct key, according to the query's orderBy() (DISTINCT ON).
Remarks
- Availability: PostgreSQL only. On SQLite TypeScript rejects the call. If you cast, or call it from plain JavaScript, it throws an error whose
codeisORM.CAPABILITY_MISSING. - Sort keys that come after the
distinctOn()columns inorderBy()decide which row is kept.
Options
| Name | Type | Required | Description |
|---|---|---|---|
...keys | Column names (string) | Yes | The columns whose distinct combinations are kept. |
Return type
| Return type | Example | Description |
|---|---|---|
SelectQuery | db.sql.public.post.distinctOn('userId') | A query keeping the first row per distinct key. |
Examples
First post per user by date
const plan = db.sql.public.post
.select('id', 'userId', 'createdAt')
.orderBy('userId', { direction: 'asc' })
.orderBy('createdAt', { direction: 'asc' })
.distinctOn('userId')
.build();
const rows = await runtime.query(plan);
// one row per user, each the earliest post by createdAtlimit() and offset()
Cap the number of returned rows and skip rows in the ordered result set.
Options
| Method | Argument | Description |
|---|---|---|
limit(n) | number, or a numeric expression | Return at most n rows. |
offset(n) | number, or a numeric expression | Skip the first n rows. |
Return type
| Return type | Example | Description |
|---|---|---|
SelectQuery | db.sql.public.user.limit(2) | A query with the row cap or offset applied. |
Examples
Page through results
const plan = db.sql.public.user
.select('id')
.orderBy('email', { direction: 'asc' })
.limit(1)
.offset(1)
.build();
const rows = await runtime.query(plan);Subquery via .as()
Call .as(alias) on a SELECT chain before build() to use it as a subquery. Pass the result to any join method as the other argument. SELECT queries and grouped queries have .as(). Queries started with insert(), update(), or delete() do not, so you cannot join against the rows they return.
Options
| Name | Type | Required | Description |
|---|---|---|---|
alias | string | Yes | Names the subquery. Address its columns as f.<alias>.<col> after the join. |
Return type
| Return type | Example | Description |
|---|---|---|
| Subquery | db.sql.public.post.select(...).as('hp') | A subquery you pass as a join's other argument. You cannot call build() on it. Join it into an outer query first. |
Examples
Join against a subquery
const highPriorityPosts = db.sql.public.post
.select('id', 'userId')
.where((f, fns) => fns.eq(f.priority, 'high'))
.as('hp');
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();
const rows = await runtime.query(plan);Grouped queries
Grouping starts with groupBy(), which turns a select query into a GroupedQuery. Write your aggregates in select() first, then call groupBy(). After that you can add having(), orderBy(), limit(), offset(), and distinct().
The examples below use an extra table, order, whose amount column is an integer:
model Order {
id Uuid @id @default(uuid())
customerId Uuid
amount Int
placedAt DateTime @default(now())
@@map("order")
}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 | db.sql.public.order.groupBy('customerId') | A grouped query supporting having(), aggregates, ordering, and limits. |
Examples
Group by a column with a count
The examples below reuse the runtime created here, and name each built query plan, the object build() returns.
const runtime = db.runtime();
const plan = db.sql.public.order
.select('customerId')
.select('orderCount', (f, fns) => fns.count(f.id))
.groupBy('customerId')
.build();
const rows = await runtime.query(plan);
// rows === [{ customerId: '<a customer id>', orderCount: 5 }]- A
count()or integersum()result larger than 2^53 - 1 throws an error whosecodeisRUNTIME.DECODE_FAILED. UsecountBigInt()orsumBigInt(field)when totals can get that large. avg()is computed as a floating-pointnumber, so a long decimal is rounded. UseavgDecimal(field)when you need the exact figure.- Every error this page names is an
Errorwith acodeproperty, so check for one as below.
try { await runtime.query(plan); } catch (error) {
if (error instanceof Error && 'code' in error && error.code === 'RUNTIME.DECODE_FAILED') { /* the count is too big for a number */ }
}Group by a computed value
const plan = db.sql.public.order
.select('yearPlaced', (f, fns) => fns.raw`EXTRACT(YEAR FROM ${f.placedAt})`.returns('pg/numeric@1'))
.select('orderCount', (f, fns) => fns.count(f.id))
.groupBy('yearPlaced')
.build();
const rows = await runtime.query(plan);
// rows === [{ yearPlaced: '2024', orderCount: 10 }]. PostgreSQL computes EXTRACT as numeric, which arrives as the string '2024'. Write Number(row.yearPlaced) for 2024.Annotate with the type PostgreSQL computes, not the type you want. Here pg/numeric@1 types yearPlaced as string, which is what you receive, while .returns('pg/int4@1') would type it number and still hand you a string. .groupBy('yearPlaced') groups by the alias given to the selected value. To group by the expression itself, repeat the fragment inside groupBy(), .returns() and all:
.groupBy((f, fns) => fns.raw`EXTRACT(YEAR FROM ${f.placedAt})`.returns('pg/numeric@1'))A name that is neither a column of the table you started from nor an alias you selected throws an error whose code is ORM.COLUMN_UNKNOWN, at the moment you call groupBy(). If you select a column you did not group or aggregate, the query fails when it runs, not when you build it. That failure comes back as PostgreSQL's own error, not a Prisma ORM code.
having()
Filter groups by an aggregate comparison. Build the predicate from the aggregate functions (fns.count, fns.sum, fns.avg, fns.min, fns.max) and the comparison functions, comparing against a JavaScript literal.
Remarks
- Compare against a plain JavaScript number, as in
fns.gt(fns.count(), 1). Passing abigintliteral such as1nthrows an error whosecodeisRUNTIME.ENCODE_FAILEDwhen the query runs.fns.countBigInt()andfns.sumBigInt()are the other way round: compare those against abigintliteral, as infns.gt(fns.countBigInt(), 1n). - Do not use a selected alias in
having(). TypeScript will let you, but PostgreSQL rejects it and the query fails when it runs. Write the aggregate again instead.
Options
| 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 | db.sql.public.order.groupBy('customerId').having(...) | A grouped query filtered to the groups matching the predicate. |
Examples
Filter groups by a sum threshold
const plan = db.sql.public.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.query(plan); // only the customer whose orders sum over 1000, with totalAmount 1500Compare with count(), avg(), min(), max()
const plan = db.sql.public.order
.select('customerId')
.select('avgAmount', (f, fns) => fns.avg(f.amount))
.select('exactAvg', (f, fns) => fns.avgDecimal(f.amount))
.select('minAmount', (f, fns) => fns.min(f.amount))
.select('maxAmount', (f, fns) => fns.max(f.amount))
.groupBy('customerId')
.having((f, fns) => fns.and(fns.gt(fns.count(), 1), fns.gt(fns.avg(f.amount), 100)))
.build();
const rows = await runtime.query(plan); // avgAmount is 300, exactAvg is the exact string '300.0000000000000000', minAmount is 100, maxAmount is 500Ordering and limiting a grouped query
A GroupedQuery supports the same orderBy(), limit(), offset(), distinct(), and distinctOn() methods as a SelectQuery. Sort by an aggregate alias to order groups. A name that is neither a column of the table you started from nor an alias you selected throws an error whose code is ORM.COLUMN_UNKNOWN, at the moment you call orderBy().
Examples
Order groups by total, keep the top one
const plan = db.sql.public.order
.select('customerId')
.select('totalAmount', (f, fns) => fns.sum(f.amount))
.groupBy('customerId')
.orderBy('totalAmount', { direction: 'desc' })
.limit(1)
.build();
const rows = await runtime.query(plan); // the single highest-spending customerMutations
where() hits every rowwhere() is optional on update() and delete(). Leave it out and every row in the table is changed, with no warning.
Mutations start from a table with insert(), update(), or delete(), and end with build(). Add returning() to get the affected rows back. Run a mutation with no returning() using runtime.execute(plan), which resolves to { affectedRows }. With returning(), run it with runtime.query(plan). The examples below go back to this page's example models, so they use db.sql.public.tag and db.sql.public.user again.
insert()
Insert one or more rows in a single statement.
Remarks
insert()always takes an array. A single-row insert is a one-element array. There is no separate single-row overload. An empty array throws an error whosecodeisORM.MUTATION_DATA_MISSING.- Leave a column out and the
@default(...)incontract.prismais applied. Prisma ORM generates@default(uuid())and@default(now())values when you callbuild(), not when the query runs. The built query then holds fixed values, so running it twice inserts the same id twice, which fails on a unique column. Call.build()again for each insert. - There is no
ON CONFLICTand no upsert here. For "insert or update", use the ORM client'supsert()or write the statement withdb.raw.sql.
Options
| Name | Type | Required | Description |
|---|---|---|---|
rows | Array of row objects | Yes | The rows to insert. Every column is optional in TypeScript. |
PostgreSQL still rejects a missing required column that has no default. Never call .returns() or param() on an insert value.
Return type
| Return type | Example | Description |
|---|---|---|
InsertQuery | db.sql.public.tag.insert([...]) | An insert query. Buildable directly, or chain returning(). |
Examples
Insert a single row
const plan = db.sql.public.tag.insert([{ label: 'single-row-tag' }]).build();
await runtime.execute(plan); // execute runs a write that returns no rows. id is filled in from @default(uuid())Insert multiple rows in one statement
const plan = db.sql.public.tag.insert([{ label: 'multi-row-a' }, { label: 'multi-row-b' }]).build();
await runtime.execute(plan);returning()
Return columns from the rows affected by an insert(), update(), or delete().
Remarks
- Availability: PostgreSQL and SQLite.
returning()takes column names only. There is noreturning('*'), and you cannot return a computed expression.
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 | db.sql.public.tag.insert([...]).returning('id', 'label') | The mutation query, which now gives you back those columns from each affected row. Run it with query, not execute. |
Examples
Return the inserted row
const plan = db.sql.public.tag.insert([{ label: 'returned-tag' }]).returning('id', 'label').build();
const rows = await runtime.query(plan); // rows === [{ id: '<the generated uuid>', label: 'returned-tag' }]update()
Update matched rows. Set columns with a values object, or derive new values from existing columns with an expression callback. Choose the rows with where(), and use returning() to get the updated rows.
Options
update() accepts a values object or an expression callback. You cannot mix the two forms in one call.
| Form | Signature | Description |
|---|---|---|
| Values object | update({ col: value, ... }) | Set columns to fixed values. |
| Expression callback | update((f, fns) => ({ col: expr })) | Set columns to expressions computed from existing columns. |
Every value the callback returns must be an expression. To set a column to a fixed value in the callback form, wrap that value in a fns.raw fragment, as the second example below does.
Return type
| Return type | Example | Description |
|---|---|---|
UpdateQuery | db.sql.public.user.update({ ... }) | An update query. Chain where() and optionally returning(). |
Examples
Update with a values object
const plan = db.sql.public.user
.update({ displayName: 'Bobby' })
.where((f, fns) => fns.eq(f.email, 'bob@example.com'))
.returning('id', 'displayName')
.build();
const rows = await runtime.query(plan); // one row, with displayName now 'Bobby'Derive a value from an existing column
const plan = db.sql.public.user
.update((f, fns) => ({
displayName: fns.raw`UPPER(${f.displayName})`.returns('pg/text@1'),
email: fns.raw`${'carol@archived.example.com'}`.returns('pg/text@1'),
}))
.where((f, fns) => fns.eq(f.email, 'carol@example.com'))
.returning('id', 'displayName')
.build();
const rows = await runtime.query(plan); // one row, with displayName now 'CAROL' and a fixed new emaildelete()
Delete matched rows. delete() takes no arguments. Choose the rows with where(), and use returning() to get the deleted rows back.
Return type
| Return type | Example | Description |
|---|---|---|
DeleteQuery | db.sql.public.tag.delete() | A delete query. Chain where() and optionally returning(). |
Examples
Delete and return the removed row
const plan = db.sql.public.tag
.delete()
.where((f, fns) => fns.eq(f.label, 'to-delete'))
.returning('id', 'label')
.build();
const rows = await runtime.query(plan); // one row, the tag that was deletedparam()
Give a value its type by hand, for a value with no column to take the type from, such as a literal inside fns.raw. param() works in any query, not only in mutations.
Remarks
- Import
paramfrom@prisma/orm-postgres/relational-core/expression. - Values passed to
insert(),update(), and comparison functions such asfns.eq(f.col, value)each take their type from the column they are used with, so you rarely needparam(). Reach for it when the type the builder would pick is not the one the column needs. Insidefns.rawno column lends its type, so a bare string becomespg/text@1and a value compared there with auuidcolumn needsparam(id, { codecId: 'pg/uuid@1' }). ATemporal.Instantcannot be interpolated bare at all. - The option is called
codecId, but it takes the same type id used everywhere else on this page. Binding a bare value withparam()lists the type id each kind of JavaScript value gets, which is where to look for a date, a uuid, a boolean, or an array.
Options
| Name | Type | Required | Description |
|---|---|---|---|
value | T | Yes | The value to bind. |
opts.codecId | string | Yes | The type id to send the value as, 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
import { param } from '@prisma/orm-postgres/relational-core/expression';
const targetId = param('11111111-1111-1111-1111-111111111111', { codecId: 'pg/uuid@1' });
const plan = db.sql.public.user
.select('id', 'email')
.where((f, fns) => fns.raw`${f.id} = ${targetId}`.returns('pg/bool@1'))
.build();
const rows = await runtime.query(plan);Expressions and functions
Callback forms receive fns alongside the f argument. fns is an object of helper functions: comparisons, boolean combinations, aggregates, and raw SQL.
Built-in functions
These exist on every database:
| Category | Functions |
|---|---|
| Comparison | eq(a, b), ne(a, b), gt(a, b), gte(a, b), lt(a, b), lte(a, b) |
| Membership | in(expr, values), notIn(expr, values) |
| Boolean | and(...predicates), or(...predicates) |
| Existence | exists(subquery), notExists(subquery) |
| Raw SQL | raw`...`.returns(typeId) |
For in() and notIn(), values is an array or a subquery. All four of in(), notIn(), exists(), and notExists() take a select query that you do not call build() on.
const named = db.sql.public.user
.select('id', 'email')
.where((f, fns) => fns.in(f.email, ['bob@example.com', 'carol@example.com']))
.build();
const authorsById = db.sql.public.user
.select('id', 'email')
.where((f, fns) => fns.in(f.id, db.sql.public.post.select('userId')))
.build();
const authors = db.sql.public.user
.select('id', 'email')
.where((f, fns) => fns.exists(db.sql.public.post.select('id').where((inner, innerFns) => innerFns.eq(inner.userId, f.id))))
.build();Which aggregate functions exist depends on your database. PostgreSQL gives you these eight. The contract type is the one you write in contract.prisma, and the PostgreSQL type is in brackets:
| Function | Returns |
|---|---|
count() / count(field) | number. With no argument it counts rows. With a field it counts that field's non-null values |
countBigInt() / countBigInt(field) | bigint. Use it when the count can pass 2^53 - 1 |
sum(field) | number over an Int (int4) or BigInt (int8) column. A string over Decimal or Numeric (numeric), a number over Float (float8) |
sumBigInt(field) | bigint. The sum over an integer column that can pass 2^53 - 1 |
avg(field) | number over an Int (int4) or Float (float8) column |
avgDecimal(field) | An exact decimal string. The version of avg that does not round |
min(field) | The input column's own type. A VarChar column gives a String (text) |
max(field) | The input column's own type. A VarChar column gives a String (text) |
On PostgreSQL you also get ilike(expr, pattern) for text columns, as in fns.ilike(f.displayName, '%alice%'). cosineDistance(a, b) compares two vector values and returns a number. It comes with pgvector, the PostgreSQL extension for vector columns, so it is there only if your contract composes pgvector. See Extension types.
fns.raw and .returns()
Write a raw SQL fragment as a tagged template. Interpolate columns, typed expressions, and bare JavaScript values with ${...}. For a bare value, the builder picks the type id from its JavaScript type. Binding a bare value with param() lists which JavaScript type becomes which type id, and shows how to choose a different one with param(...). Call .returns(typeId) to declare the fragment's result type.
Options
| Name | Type | Required | Description |
|---|---|---|---|
| SQL fragment | Tagged template | Yes | The raw SQL, with ${...} interpolations for columns and values. |
.returns(typeId) | string, or { codecId, nullable } | Yes, always | Declares the result type, for example 'pg/int4@1', 'pg/text@1'. For a fragment used as a where() predicate, write .returns('pg/bool@1'). |
Remarks
- Always call
.returns(). Nothing in the builder accepts a fragment without it. .returns()only tells TypeScript what type to expect. It does not convert the value. Convert the value in JavaScript, or run the statement withdb.raw.sqland.returnsRow(), which does decode.- Use
fns.rawfor any SQL feature without a dedicated helper:COALESCE,CAST,LENGTH,UPPER,EXTRACT, and so on. There is nofns.coalesceand nofns.cast.
Examples
Compute a column with a SQL function
const plan = db.sql.public.user
.select('emailLength', (f, fns) => fns.raw`LENGTH(${f.email})`.returns('pg/int4@1'))
.build();Compiling and executing
build()
Compile a query so you can run it.
Remarks
build()takes zero arguments on every query type: select, insert, update, delete, and grouped. You supply parameter values where you write them, insideinsert([...]), awhere()callback, or aparam()call.
Return type
| Return type | Example | Description |
|---|---|---|
| Built query | db.sql.public.user.select('id').build() | A query you run with runtime.query(...). You can get the TypeScript type of one row with ResultType. |
Running a built query
Run a built query with runtime, which is db.runtime().
| Call | What you get |
|---|---|
runtime.query(plan) | Rows. await it for an array of rows (Row[]), or for await over it to take one row at a time. |
runtime.execute(plan) | { affectedRows }. |
| Which one | query for anything that returns rows, which means a SELECT or a write with returning(). execute for everything else. |
ResultType
Recover a built query's row type at the type level.
Remarks
- Import
ResultTypefrom@prisma/orm-postgres/components/runtime. It works with the ORM client as well as the SQL builder. ResultType<typeof plan>is one row, not an array.await runtime.query(plan)resolves toRow[], butResultType<typeof plan>isRow.- It also works on ORM queries:
ResultType<typeof db.orm.public.User.include('posts')>.db.ormis keyed by model name, so the model isUserthere and the table isuserunderdb.sql. See Model and result types.
Examples
Recover the row type from a built query
import type { ResultType } from '@prisma/orm-postgres/components/runtime';
const plan = db.sql.public.user.select('id', 'email').build();
type Row = ResultType<typeof plan>; // { id: string; email: string }
const rows = await runtime.query(plan); // Row[]Streaming vs. collecting
await runtime.query(plan) collects every row into an array. This is the common case and what every example on this page uses. You can also loop the same result with for await to handle one row at a time. On PostgreSQL every row is loaded before the loop starts, so for await saves nothing there. Use await.
Keep the result in a variable, as in const result = runtime.query(plan), and you can await result as many times as you like. You cannot mix await and for await on the same result, and you cannot for await it twice. Either one throws an error whose code is RUNTIME.ITERATOR_CONSUMED. See AsyncIterableResult.
