# Model delegate

> Every method on a table delegate - reads, writes, pagination, and the plugin-related helpers.

URL: https://better-drizzle.com/docs/reference/model-api

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

| Method       | Signature                                                                | Returns                                      |
| ------------ | ------------------------------------------------------------------------ | -------------------------------------------- |
| `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 key | `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 lazy thenable that runs the read when awaited, with an extra `.explain(options?)` method that returns the query plan. See [explain](/docs/querying/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](/docs/querying/prepared-statements).

`ThrowingResult<T>` is an explainable `Promise<T | null>` with a `.throw()` method - see [throwing results](/docs/writing/throwing-results). All read args are documented in [query options](/docs/reference/query-options), and pagination results in [pagination](/docs/querying/pagination).

## Writes

| Method       | Signature                                                                        | Returns                                              |
| ------------ | -------------------------------------------------------------------------------- | ---------------------------------------------------- |
| `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

```ts
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.

```ts
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

```ts
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](/docs/plugins/writing-plugins):

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