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), where must pin one row by a primary or unique keyThrowingResult<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 lazy thenable that runs the read when awaited, with an extra .explain(options?) method that returns the query plan. See explain. Reads also expose .prepare(name?), which compiles the read once, with param() markers in place of values, and returns a statement with execute(values). See prepared statements.

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?, batchSize?, meta? })BatchResult<Row>
update(args: { where, data, select?, include?, meta? })ThrowingWriteResult<Row>
updateMany(args: { where?, data, select?, meta? })BatchResult<Payload>
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? })ThrowingWriteResult<Row>
deleteMany(args: { where?, select?, meta? })BatchResult<Payload>

ThrowingWriteResult<T> is a plain Promise<T | null> with a .throw() method. Writes run as soon as they are called and expose neither .explain() nor .prepare().

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 accepts a structured filter or Drizzle SQL and applies to the existing conflicting row on 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
$where(where)compile a typed where into a Drizzle SQL condition

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