Model delegate
Every method on a table delegate - reads, writes, pagination, and the plugin-related helpers.
A model delegate is what you get from client.users, client.posts, or client.repository(name). Every table has the same surface, typed against its own columns and relations.
Reads
| Method | Signature | Returns |
|---|---|---|
findMany | (args?: QueryArgs) | Row[] |
findFirst | (args?: QueryArgs) | ThrowingResult<Row> |
findOne | (args?: QueryArgs) | ThrowingResult<Row> (alias of findFirst) |
findUnique | (args: QueryArgs) | ThrowingResult<Row> |
count | (args?: CountArgs) | number |
exists | (args?: ExistsArgs) | boolean |
paginate | (args: PaginationArgs) | OffsetPaginationResult<Row> |
cursor | (args: CursorArgs) | CursorPaginationResult<Row> |
Every read returns an explainable result: a normal promise with an extra .explain(options?) method that returns the query plan. See explain.
ThrowingResult<T> is an explainable Promise<T | null> with a .throw() method - see throwing results. All read args are documented in query options, and pagination results in pagination.
Writes
| Method | Signature | Returns |
|---|---|---|
create | (args: { data, skipDuplicates?, select?, include?, meta? }) | Row, or Row | null when skipDuplicates is set |
createMany | (args: { data: Row[], skipDuplicates?, select?, include?, meta? }) | BatchResult<Row> |
update | (args: { where, data, select?, include?, meta? }) | ThrowingResult<Row> |
updateMany | (args: { where?, data, meta? }) | BatchResult<never> |
updateEach | (args: { by, data, update, where?, select?, onEmpty?, meta? }) | BatchResult<Row> |
upsert | (args: { where, create, update, select?, include?, meta? }) | Row |
upsertMany | (args: { data, target, update, select?, batchSize?, where?, meta? }) | BatchResult<Row> |
delete | (args: { where, select?, include?, meta? }) | ThrowingResult<Row> |
deleteMany | (args: { where?, meta? }) | BatchResult<never> |
BatchResult<T> is { count: number; data?: T[] } - data is populated when the driver supports RETURNING.
Less obvious write options
skipDuplicates
create and createMany accept skipDuplicates.
trueignores duplicate conflicts using the dialect's supported default conflict handling['email']or another explicit column list targets specific unique columns when the dialect supports it
const maybeUser = await client.users.create({
data: {
email: 'alice@example.com',
name: 'Alice',
},
skipDuplicates: true,
});
if (!maybeUser) {
// insert was skipped
}When a single-row create is skipped, it returns null. For createMany, count reflects only rows that were actually inserted.
updateEach
updateEach updates many rows with a different value per row in a single UPDATE ... SET column = CASE ... END statement. by is the column (the Drizzle column object) used to match each input row, and update maps columns to a function that returns the new value for a row.
const { count, data } = await client.users.updateEach({
by: users.id,
data: [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
],
update: {
name: (row) => row.name,
},
select: { id: true, name: true },
});- every input row must include the
byvalue, andbyvalues must be unique - each resolver must return a value (or a Drizzle
SQL);undefinedthrows whereis combined with the generatedby in (...)filterselectis scalar-only- an empty
dataarray returns{ count: 0 }, or throws withonEmpty: 'throw'
upsertMany
upsertMany is the native batch upsert path for high-volume writes. It requires an explicit conflict target and supports multiple update strategies:
'all'updates every mutable column on conflict['name', 'active']updates only the listed columns{ name: 'Alice', updatedAt: sql\now()` }` provides an explicit update object(ctx) => ({ name: ctx.excluded.name })builds the update object fromexcluded,sql, and table columns
import { sql } from 'drizzle-orm';
const result = await client.users.upsertMany({
data: [
{ email: 'a@example.com', name: 'A', active: true },
{ email: 'b@example.com', name: 'B', active: false },
],
target: ['email'],
update: (ctx) => ({
name: ctx.excluded.name,
active: ctx.excluded.active,
updatedAt: sql`now()`,
}),
batchSize: 500,
select: {
id: true,
email: true,
name: true,
},
});Constraints worth knowing:
upsertManyis native-only and intentionally fails fast on unsupported dialects/featureswhereis SQL-only and applies to the update side of the conflict pathselectis supported, but relation selects are notincludeis intentionally not supported
Plugin helpers
These exist on every delegate and are mostly used by plugins:
| Member | Description |
|---|---|
$model | { name, dbName, hasColumn(column) } metadata for the table |
$state | the current ephemeral plugin state |
$withState(state) | clone the delegate with merged plugin state |
$withoutPlugins() | clone the delegate with all plugin transforms/hooks bypassed |
Plugins can also add methods here - for example the soft-delete plugin adds restore() and restoreById() to compatible models.
$withState() and $withoutPlugins() are not just for plugin authors. They are also useful in application code when you need a one-off state flag or an intentional bypass for repair/admin flows.
Args are extensible
Plugins that declare operationArgs add typed fields to these method
signatures. For example, with the soft-delete plugin, findMany accepts a
deleted: 'with' | 'without' | 'only' argument, and delete accepts
mode: 'soft' | 'hard'.