Create, update & delete
The write surface — create, createMany, update, updateMany, 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 updateddelete
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, and upsertMany can project results. upsertMany supports 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, 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. 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.