SQL query builder reference
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 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")
}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. To put a model's table in another PostgreSQL schema, such as auth, wrap the model in a namespace auth { ... } block in your contract, and you then reach its table through db.sql.auth instead. See Namespaces.
Table accessors are keyed by table name, not model name. A table name is the model's @@map value, or the model name exactly as written when the model has no @@map. In the example schema, the User model is mapped to user, so you write db.sql.public.user. The PostTag model for the join table is mapped to post_tag, so you write db.sql.public.post_tag. Column names are the field names unless a field sets @map, and for a field that does, you write the @map name everywhere in the builder. A field declared userId Uuid @map("user_id") is select('user_id') and f.user_id, and each row you get back has a user_id key.
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);The example schema's Embedding1536 column is a pgvector type, so a client for that schema also needs pgvector in extensions, as Built-in functions shows.
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, but every other one is. The rest of this page shows PostgreSQL and writes the root as db.sql.public.
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 INSERTCall .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().
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, and 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, which 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').
Reduce each row to a chosen set of columns or computed expressions.
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 | Example | Description |
|---|---|---|
SelectQuery |
db.sql.public.user.select('id', 'email') |
A query projected to the named columns or expressions, chainable and buildable. |
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';const plan = db.sql.public.user.select('id', 'email').build();
const rows = await runtime.query(plan);
// rows[0] is { id, email }, with no displayNameconst 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);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 }]Restrict a query to rows matching an expression.
| 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 | Example | Description |
|---|---|---|
SelectQuery |
db.sql.public.post.select('id').where(...) |
A query narrowed by the predicate. |
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);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.
| 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 | 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. |
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()
Section titled “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.
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 }]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.
- 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.
| 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 | Example | Description |
|---|---|---|
SelectQuery |
db.sql.public.user.lateralJoin('latestPost', ...) |
A query with the lateral subquery's columns in scope under alias. |
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 }]Sort the result set by a column or a computed expression, in a direction you choose.
| Name | Type | Required | Description |
|---|---|---|---|
key |
Column name (string) or (f, fns) => Expression |
Yes | The column or computed value to sort by. |
options.direction |
'asc' | 'desc' |
No | Sort direction. |
TypeScript also accepts a nulls option, but 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.task
.select('id', 'description')
.orderBy((f, fns) => fns.raw`CASE WHEN ${f.description} IS NULL THEN 1 ELSE 0 END`.returns('pg/int4@1'))
.orderBy('description', { direction: 'asc' });| Return type | Example | Description |
|---|---|---|
SelectQuery |
db.sql.public.user.select('email').orderBy('email', ...) |
A query with the sort key applied. Call orderBy() again to add secondary keys. |
const plan = db.sql.public.user.select('id', 'email').orderBy('email', { direction: 'asc' }).build();
const rows = await runtime.query(plan);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);De-duplicate identical projected rows (SELECT DISTINCT).
| Return type | Example | Description |
|---|---|---|
SelectQuery |
db.sql.public.post.select('priority').distinct() |
A query returning only distinct projected rows. |
const plan = db.sql.public.post.select('priority').distinct().build();
const rows = await runtime.query(plan);
// distinct priorities: ['high', 'low', 'urgent']Keep the first row per distinct key, according to the query's orderBy() (DISTINCT ON).
- 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.
| Name | Type | Required | Description |
|---|---|---|---|
...keys |
Column names (string) |
Yes | The columns whose distinct combinations are kept. |
| Return type | Example | Description |
|---|---|---|
SelectQuery |
db.sql.public.post.select('userId').distinctOn('userId') |
A query keeping the first row per distinct key. |
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 createdAtCap the number of returned rows and skip rows in the ordered result set.
| Method | Argument | Description |
|---|---|---|
limit(n) |
number, or a numeric expression |
Return at most n rows. |
offset(n) |
number, or a numeric expression |
Skip the first n rows. |
| Return type | Example | Description |
|---|---|---|
SelectQuery |
db.sql.public.user.select('id').limit(2) |
A query with the row cap or offset applied. |
const plan = db.sql.public.user
.select('id')
.orderBy('email', { direction: 'asc' })
.limit(1)
.offset(1)
.build();
const rows = await runtime.query(plan);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(), but queries started with insert(), update(), or delete() do not, so you cannot join against the rows they return.
| Name | Type | Required | Description |
|---|---|---|---|
alias |
string |
Yes | Names the subquery. Address its columns as f.<alias>.<col> after the join. |
| 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. |
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);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")
}Group rows by one or more columns or by a computed expression, producing one row per distinct group.
| 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 | Example | Description |
|---|---|---|
GroupedQuery |
db.sql.public.order.select('customerId').groupBy('customerId') |
A grouped query supporting having(), aggregates, ordering, and limits. |
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 }]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 */ }
}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, and that failure comes back as PostgreSQL's own error, not a Prisma ORM code.
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.
- 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.
| 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 | Example | Description |
|---|---|---|
GroupedQuery |
db.sql.public.order.select('customerId').groupBy('customerId').having(...) |
A grouped query filtered to the groups matching the predicate. |
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 1500const 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 500A 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().
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 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 one or more rows in a single statement.
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.
| 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 | Example | Description |
|---|---|---|
InsertQuery |
db.sql.public.tag.insert([...]) |
An insert query. Buildable directly, or chain returning(). |
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())const plan = db.sql.public.tag.insert([{ label: 'multi-row-a' }, { label: 'multi-row-b' }]).build();
await runtime.execute(plan);Return columns from the rows affected by an insert(), update(), or delete().
- Availability: PostgreSQL and SQLite.
returning()takes column names only. There is noreturning('*'), and you cannot return a computed expression.
| Name | Type | Required | Description |
|---|---|---|---|
...columns |
Column names (string) |
Yes | The columns to return from each affected row. |
| 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. |
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 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.
update() accepts a values object or an expression callback, but 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 | Example | Description |
|---|---|---|
UpdateQuery |
db.sql.public.user.update({ ... }) |
An update query. Chain where() and optionally returning(). |
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'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 matched rows. delete() takes no arguments. Choose the rows with where(), and use returning() to get the deleted rows back.
| Return type | Example | Description |
|---|---|---|
DeleteQuery |
db.sql.public.tag.delete() |
A delete query. Chain where() and optionally returning(). |
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 deletedGive 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.
- 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.
| 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 | Example | Description |
|---|---|---|
ParamRef |
param('%@example.com', { codecId: 'pg/text@1' }) |
A bound-parameter reference usable inside fns.raw. |
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);Callback forms receive fns alongside the f argument. fns is an object of helper functions: comparisons, boolean combinations, aggregates, and raw SQL.
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.post.userId, f.user.id))))
.build();Which aggregate functions exist depends on your database, and 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, fns also has ilike(expr, pattern), a case-insensitive SQL LIKE match for text columns, as in fns.ilike(f.displayName, '%alice%'). There is no fns.like, so write a case-sensitive LIKE in a fns.raw fragment that returns a boolean:
const plan = db.sql.public.user
.select('id', 'displayName')
.where((f, fns) => fns.raw`${f.displayName} LIKE ${'%Alice%'}`.returns('pg/bool@1'))
.build();ilike does not work on a native_enum column, typed pg.enum(...), because PostgreSQL cannot pattern-match an enum type. Passing one is a type error. See Enums.
Full-text search on PostgreSQL uses fns.fullTextMatches(column, query) to filter, fns.fullTextRank(column, query) to score, and fns.fullTextHeadline(column, query) to return the matching text with each match marked. Build query from the search text with fns.websearchToTsquery(text), fns.plaintoTsquery(text), fns.phrasetoTsquery(text), or fns.toTsquery(text), or with the tsquery template tag from @prisma/orm-postgres/target/full-text. Never pass user input to fns.toTsquery(), because text that breaks PostgreSQL's search syntax makes the query fail when it runs; use fns.websearchToTsquery() or the tsquery tag for user input. Full-text search on the ORM client reference explains what each of these does, and the index that lets PostgreSQL search without reading every row.
Each of the three full-text functions takes an options object as its last argument, and each accepts language. Pass the same language to the function that builds query, as in fns.fullTextMatches(f.title, fns.websearchToTsquery(q, { language: 'german' }), { language: 'german' }). fullTextRank also takes normalization and coverDensity, which the ORM client reference describes. fullTextHeadline also takes these options:
startSelandstopSel: the text put before and after each match. PostgreSQL uses<b>and</b>when you leave them out.maxWordsandminWords: the longest and shortest headline, in words.minWordsmust be lower thanmaxWords.highlightAll: true: return the whole text with every match marked, instead of a short excerpt around the matches.
This query returns each matching post with its rank and a headline that marks the matches with <mark>:
const q = 'prisma orm';
const plan = db.sql.public.post
.select('id', 'title')
.select('rank', (f, fns) => fns.fullTextRank(f.title, fns.websearchToTsquery(q)))
.select('headline', (f, fns) =>
fns.fullTextHeadline(f.title, fns.websearchToTsquery(q), { startSel: '<mark>', stopSel: '</mark>' }),
)
.where((f, fns) => fns.fullTextMatches(f.title, fns.websearchToTsquery(q)))
.orderBy((f, fns) => fns.fullTextRank(f.title, fns.websearchToTsquery(q)), { direction: 'desc' })
.limit(10)
.build();
const results = await runtime.query(plan);
// { id: string; title: string; rank: number; headline: string }[]rank is a number, and a higher rank is a better match. headline is a string: the title with each match between <mark> and </mark>.
pgvector is the PostgreSQL extension for vector columns. If your contract uses a pgvector type, such as Embedding1536 in the example schema, pass pgvector in extensions when you create the client, because without it postgres(...) throws an error whose code is RUNTIME.MISSING_EXTENSION_PACK:
import pgvector from '@prisma/orm-extension-pgvector/runtime';
const db = postgres<Contract>({
contractJson,
url: process.env.DATABASE_URL,
extensions: [pgvector],
});fns then also has fns.cosineDistance(a, b), which returns the cosine distance between two vectors as a number. Each argument is a vector column or an array of numbers, as in fns.cosineDistance(f.embedding, queryEmbedding). See Extension types.
Write a raw SQL fragment as a tagged template, and 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.
| 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'). |
- 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.
const plan = db.sql.public.user
.select('emailLength', (f, fns) => fns.raw`LENGTH(${f.email})`.returns('pg/int4@1'))
.build();Compile a query so you can run it.
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 | 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. |
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. |
Recover a built query's row type at the type level.
- 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.
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[]await runtime.query(plan) collects every row into an array, which 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.