better-drizzle
Reference

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

MethodSignatureReturns
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

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

  • true ignores 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 by value, and by values must be unique
  • each resolver must return a value (or a Drizzle SQL); undefined throws
  • where is combined with the generated by in (...) filter
  • select is scalar-only
  • an empty data array returns { count: 0 }, or throws with onEmpty: '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 from excluded, 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:

  • upsertMany is native-only and intentionally fails fast on unsupported dialects/features
  • where is SQL-only and applies to the update side of the conflict path
  • select is supported, but relation selects are not
  • include is intentionally not supported

Plugin helpers

These exist on every delegate and are mostly used by plugins:

MemberDescription
$model{ name, dbName, hasColumn(column) } metadata for the table
$statethe 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'.

On this page