Writing

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 RETURNING

create 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 updated

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 deleted

upsert

"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.

  • target is required
  • update can be 'all', a column list, an object, or a function
  • batchSize chunks large inserts without changing the API
  • where is available for SQL-only conflict-update predicates
  • relation include is 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, and deleteMany return a { count, data? } summary, not individual rows.
  • upsertMany also returns { count, data? }.
  • update and delete are nullable by default because the target row may not exist — handle null, or call .throw().
  • Returning affected rows (data) depends on the driver supporting RETURNING.
  • upsertMany is 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.

On this page