Create, update & delete
The write surface — create, createMany, update, updateMany, updateEach, delete, deleteMany, upsert, and upsertMany.
The write methods mirror the read API and return predictable shapes. Single-row mutations return the affected row; batch mutations return a summary.
create
Inserts one row and returns the created record:
const user = await client.users.create({
data: {
email: 'alice@example.com',
name: 'Alice',
active: true,
},
});createMany
Inserts many rows in one statement and returns a batch summary:
const batch = await client.users.createMany({
data: [
{ email: 'a@example.com', name: 'A', active: true },
{ email: 'b@example.com', name: 'B', active: false },
],
});
batch.count; // number inserted
batch.data; // inserted rows, when the driver supports RETURNINGcreate and createMany both support skipDuplicates:
const maybeCreated = await client.users.create({
data: {
email: 'alice@example.com',
name: 'Alice',
active: true,
},
skipDuplicates: ['email'],
});With single-row create, a skipped insert returns null. With createMany, count only includes rows that were actually inserted.
update
Updates a single matching row. The result is nullable (the target may not exist) and also exposes .throw():
const updated = await client.users.update({
where: { id: 1 },
data: { name: 'Alice Updated' },
});
if (!updated) {
throw new Error('Update target not found');
}updateMany
Updates every matching row and returns the affected count:
const batch = await client.users.updateMany({
where: { active: true },
data: { active: false },
});
batch.count; // rows updatedupdateEach
Updates many rows with different values in a single statement. Instead of applying the same value to all matching rows (like updateMany), updateEach generates a CASE expression per column so each row can have its own value:
const batch = await client.users.updateEach({
by: users.id,
data: [
{ id: 1, name: 'Alice Updated', city: 'New York' },
{ id: 2, name: 'Bob Updated', city: 'Chicago' },
],
update: {
name: (row) => row.name,
city: (row) => row.city,
},
});This generates a single SQL statement:
UPDATE users
SET
name = CASE WHEN id = 1 THEN 'Alice Updated' WHEN id = 2 THEN 'Bob Updated' ELSE name END,
city = CASE WHEN id = 1 THEN 'New York' WHEN id = 2 THEN 'Chicago' ELSE city END
WHERE id IN (1, 2)Options
| Option | Type | Required | Description |
|---|---|---|---|
by | TableColumn | Yes | Drizzle column instance used to match rows (e.g., users.id) |
data | Row[] | Yes | Source rows containing the by column value plus any extra fields |
update | object | Yes | Per-column callbacks (row) => value that resolve the new value for each row |
where | WhereArg | No | Extra filter combined with the generated by IN (...) predicate |
select | SelectInput | No | Scalar column projection for returned rows (relation include not supported) |
onEmpty | 'return' | 'throw' | No | Behavior when data is empty. Defaults to 'return' |
meta | Meta | No | Custom metadata forwarded to hooks |
Return type
updateEach returns Promise<BatchResult<T>>:
{
count: number; // number of rows affected
data?: T[]; // returned rows when `select` is provided and driver supports RETURNING
}Using where with updateEach
Combine a where filter with the by column to scope which rows are eligible for update:
const batch = await client.users.updateEach({
by: users.id,
data: [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
],
update: { name: (row) => row.name },
where: { active: true }, // only update active users
});Projecting results
Use select to return specific scalar columns after the update:
const batch = 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, email: true },
});
console.log(batch.data); // [{ id: 1, name: 'Alice', ... }, ...]Empty input handling
By default, passing an empty data array returns { count: 0 }. Set onEmpty: 'throw' to raise an error instead:
// Returns { count: 0 }
await client.users.updateEach({
by: users.id,
data: [],
update: { name: (row) => row.name },
});
// Throws BetterDrizzleError
await client.users.updateEach({
by: users.id,
data: [],
update: { name: (row) => row.name },
onEmpty: 'throw',
});Duplicate detection
updateEach rejects duplicate by values in the data array. Each row must have a unique match column value:
// Throws — duplicate id values
await client.users.updateEach({
by: users.id,
data: [
{ id: 1, name: 'Alice' },
{ id: 1, name: 'Bob' }, // duplicate!
],
update: { name: (row) => row.name },
});When to use updateEach vs updateMany
Use updateMany when | Use updateEach when |
|---|---|
| All matching rows should have the same value | Each row needs a different value |
A simple WHERE filter is sufficient | You need per-row logic via callbacks |
| You don't need to return updated rows | You want to select and return specific columns |
delete
Deletes a single matching row and returns it (nullable, with .throw()):
const deleted = await client.users.delete({
where: { id: 1 },
});deleteMany
Deletes every matching row and returns the affected count:
const batch = await client.users.deleteMany({
where: { active: false },
});
batch.count; // rows deletedupsert
"Create if missing, update if present" — without branching in your own code:
const user = await client.users.upsert({
where: { email: 'alice@example.com' },
create: {
email: 'alice@example.com',
name: 'Alice',
active: true,
},
update: {
name: 'Alice Renamed',
},
});upsert is usually cleaner than "find, then branch, then write" when the behavior is genuinely upsert-shaped.
upsertMany
For bulk create-or-update flows, use upsertMany instead of looping over upsert():
import { sql } from 'drizzle-orm';
const result = await client.users.upsertMany({
data: [
{ email: 'alice@example.com', name: 'Alice', active: true },
{ email: 'bob@example.com', name: 'Bob', 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,
},
});upsertMany is designed around native conflict handling, not userland loops.
targetis requiredupdatecan be'all', a column list, an object, or a functionbatchSizechunks large inserts without changing the APIwhereis available for SQL-only conflict-update predicates- relation
includeis intentionally not supported
Projecting write results
create, update, delete, upsert, createMany, updateEach, and upsertMany can project results. updateEach and upsertMany support select; the others support select and include:
const user = await client.users.create({
data: { email: 'writer@example.com', name: 'Writer', active: true },
select: { id: true, email: true },
});
// ^? { id: number; email: string }Notes
createMany,updateMany,updateEach, anddeleteManyreturn a{ count, data? }summary, not individual rows.upsertManyalso returns{ count, data? }.updateanddeleteare nullable by default because the target row may not exist — handlenull, or call.throw().- Returning affected rows (
data) depends on the driver supportingRETURNING. updateEachis native-first — it generates a singleUPDATE ... CASEstatement instead of per-row loops.updateEachrejects duplicatebyvalues in the input data.upsertManyis intentionally native-first and currently fails fast on unsupported dialects instead of degrading to slow per-row loops.- Need lifecycle behavior on writes — timestamps, soft delete, audit trails? That belongs in plugins or hooks, not duplicated per call.