# better-drizzle changelog

> Every better-drizzle release, newest first. Source: https://github.com/almeidazs/better-drizzle/releases

## 0.4.0 (unreleased)

`0.4.0` adds tenant isolation: the experimental [`better-drizzle/tenant`](https://better-drizzle.com/docs/plugins/tenant) plugin, built on a new core primitive for plugin authors, [model scopes](https://better-drizzle.com/docs/plugins/writing-plugins#model-scopes). The soft-delete plugin moves onto model scopes, which changes what nested reads return.

### Behavior changes

- **Soft-deleted rows are hidden in nested reads.** `better-drizzle/soft-delete` used to filter only the root query, so deleted rows appeared in `include`, relation `select`, relation filters (`some`, `every`, `none`, `is`, `isNot`), `include._count`, and relation sorts. They are hidden there now. The `deleted` argument still controls the root rows only. `connect`, `disconnect`, and `set` no longer reach deleted rows. `$withoutPlugins()` still reads deleted rows at every level. See [soft delete](https://better-drizzle.com/docs/plugins/soft-delete#what-is-filtered).
- **Transactions rethrow callback errors unwrapped.** An error thrown inside the `transaction(...)` callback, including a nested one, now reaches the caller as the same object instead of a `BetterDrizzleError` with code `OPERATION_ERROR` and operation `"transaction"`. `onTransactionError` hooks still receive the normalized error. See [transactions](https://better-drizzle.com/docs/advanced/transactions). ([#81](https://github.com/almeidazs/better-drizzle/pull/81))
- **Soft deletes and restores keep other scopes.** The inner update of a soft `delete()` / `deleteMany()` and of `restore()` / `restoreById()` runs through `$withoutPlugins({ keepScopes: true })` instead of `$withoutPlugins()`, so on a tenant-scoped client it only touches that tenant's rows.

### Features

- **Tenant plugin (experimental).** `better-drizzle/tenant` scopes every model that has a tenant column: root reads, relation loads and filters, `_count`, relation sorts, writes, upserts, and relation-write lookups. `db.$tenant({ ... })` returns a scoped client, `db.$withoutTenant({ reason })` an audited bypass. It stamps tenant columns on create (optional in create types and zod/ata schemas), rejects changes to them, fails closed without a tenant, denies raw SQL on scoped clients by default, and can set PostgreSQL RLS settings per transaction. Options: `columns`, `global`, `models`, `missing`, `raw`, `rls`, and `strict`. See [tenant](https://better-drizzle.com/docs/plugins/tenant).
- **Model scopes for plugin authors.** A plugin can declare `scope: { key, model, root, missing, mismatch, bypass, session, rawSession }`. The core ANDs each model's scope into every statement that references the model, stamps and protects scope columns on writes, and handles upsert conflicts per dialect. `extendClient` receives `scope` and `withScope(state, meta?)`; hooks, transforms, intercepts, and raw hooks receive `scopes`. Clients without scope plugins keep their previous code paths. See [model scopes](https://better-drizzle.com/docs/plugins/writing-plugins#model-scopes).
- **`$withoutPlugins({ keepScopes: true })`.** Skips plugin hooks and transforms but keeps model scopes. `$withoutPlugins()` without options drops every scope; on a tenant-scoped client its operations pass `meta.tenantBypass = { reason: '$withoutPlugins' }`.
- **New error codes.** `TENANT_REQUIRED` (403), `TENANT_MISMATCH` (403), `TENANT_RAW_DENIED` (403), and `TENANT_ALREADY_SCOPED` (400). See [error codes](https://better-drizzle.com/docs/reference/errors#error-codes).
- **`ClientExtensionTypeResolver`.** A type-level resolver for client extensions typed from the schema, passed as the seventh `definePlugin` generic. Its optional `hidden` removes table keys from the root client type.
- **`better()` returns `BetterRootClient`.** It equals `BetterDrizzleClient` unless a plugin hides models (the tenant plugin's `strict` option), in which case the hidden delegates are removed from the root client and its `$withContext()`, `extends()`, and transaction clients.

### Fixes

- **Empty updates are a no-op.** When `data` has no column values after plugin transforms, `update` returns the current row, `updateMany` returns `{ count: 0 }`, and `upsert` returns the existing row, instead of failing with Drizzle's `No values to set`. ([#82](https://github.com/almeidazs/better-drizzle/pull/82))

### Contributors

- [@almeidazs](https://github.com/almeidazs)
- [@joaotonaco](https://github.com/joaotonaco) in [#81](https://github.com/almeidazs/better-drizzle/pull/81) and [#82](https://github.com/almeidazs/better-drizzle/pull/82)

## 0.3.0 - 2026-10-12

better-drizzle now runs on **Drizzle ORM 1.0 RC** (relational queries v2). This release also adds prepared statements, relation ordering, a UUID plugin, two experimental plugins (`cache` and `ata`), atomic updates, PostgreSQL array and JSONB path support, `$where()` for typed conditions in raw Drizzle SQL, and stricter types and runtime checks.

### Highlights

- **Drizzle ORM 1.x.** Tables and relations come from your `drizzle({ relations })` instance, so `better(db)` needs no `schema` option. ([#57](https://github.com/almeidazs/better-drizzle/pull/57))
- **Prepared reads.** Mark values with `param()` and call `.prepare()` on any read. ([#64](https://github.com/almeidazs/better-drizzle/pull/64))
- **Read cache (experimental).** Opt-in Redis caching with automatic, relation-aware invalidation after writes and commits. ([#60](https://github.com/almeidazs/better-drizzle/pull/60))
- **Atomic updates.** `increment`, `decrement`, `multiply`, `divide`, and `toggle` without reading first. ([#53](https://github.com/almeidazs/better-drizzle/pull/53))
- **PostgreSQL arrays and JSONB.** Typed array filters and mutations, and typed JSONB path updates through `jsonb_set`. ([#50](https://github.com/almeidazs/better-drizzle/pull/50), [#54](https://github.com/almeidazs/better-drizzle/pull/54))
- **`$where()`.** Compile typed better-drizzle conditions into raw Drizzle SQL. ([#63](https://github.com/almeidazs/better-drizzle/pull/63))
- **Relation ordering.** Sort by fields of `one` relations, at any depth, or by `_count` of to-many relations, with correlated subqueries. ([#67](https://github.com/almeidazs/better-drizzle/pull/67))
- **UUIDs.** `better-drizzle/uuid` fills UUIDv7 ids on create, makes them optional in create types, and `buuid()` stores them as 16 bytes on MySQL and SQLite. ([#71](https://github.com/almeidazs/better-drizzle/pull/71))
- **ATA plugin (experimental).** JSON Schema validation built on ata-validator, a faster alternative to the Zod plugin. ([#51](https://github.com/almeidazs/better-drizzle/pull/51))
- **Also:** `createMany({ batchSize })`, insensitive `equals` / `in` / `notIn`, a timestamps `now` clock with per-model overrides, and `INVALID_ARGS` (400) for invalid arguments.

### Install

```bash
npm install better-drizzle@0.3.0 drizzle-orm@1.0.0-rc.4
```

The `drizzle-orm` peer range is `>=1.0.0-rc.4 <1.0.0-rc.5`. Install it with the explicit version: until Drizzle 1.0 is tagged `latest` on npm, a plain `drizzle-orm` install resolves to 0.x. Projects on Drizzle 0.x should stay on `better-drizzle@~0.2.0`.

> [!IMPORTANT]
> This release has breaking changes. Follow the [upgrading guide](https://better-drizzle.com/docs/guides/upgrading#moving-to-drizzle-orm-1x) before you bump. The same notes live in the [changelog](https://better-drizzle.com/docs/changelog).

Each entry links to the commits that made it. Merged pull requests are linked as `#n`. **Breaking changes** are removed or renamed APIs, new compile errors, and changes the commits mark as breaking. **Behavior changes** compile unchanged but can change what your code sees at runtime.

### Breaking changes

#### Drizzle ORM 1.x

- **Drizzle ORM 1.x only.** The `drizzle-orm` peer range is `>=1.0.0-rc.4 <1.0.0-rc.5` (was `^0.30.0`). It is capped below `1.0.0-rc.5` because the typecheck fails against that snapshot. Projects on `drizzle-orm` 0.x must stay on `better-drizzle@~0.2.0`. See [upgrading](https://better-drizzle.com/docs/guides/upgrading#1-upgrade-drizzle-orm). ([`8b09683`](https://github.com/almeidazs/better-drizzle/commit/8b09683), [`21cfd3e`](https://github.com/almeidazs/better-drizzle/commit/21cfd3e), [#57](https://github.com/almeidazs/better-drizzle/pull/57))
- **`better()` no longer takes `schema`.** Tables and relations come from the Drizzle instance (`db._.relations`, built by `drizzle({ client, relations })`). The options argument is optional, so `better(db)` works. A Drizzle instance without `relations` throws `No tables found on the Drizzle instance. ...`. Only tables in the relations config get a delegate. See [upgrading](https://better-drizzle.com/docs/guides/upgrading#3-pass-relations-to-drizzle-and-drop-schema-from-better). ([`8b09683`](https://github.com/almeidazs/better-drizzle/commit/8b09683))
- **Type parameters are `typeof relations`.** `BetterDrizzleClient`, `BetterTableKey`, `WhereArg`, explicit `better<...>()` calls, and every other exported helper type take the relations config instead of the schema module. See [upgrading](https://better-drizzle.com/docs/guides/upgrading#4-switch-type-generics-to-typeof-relations). ([`8b09683`](https://github.com/almeidazs/better-drizzle/commit/8b09683), [`64af0c4`](https://github.com/almeidazs/better-drizzle/commit/64af0c4))
- **Many-to-many comes only from `.through()`.** The `relations: { inferManyToMany, manyToMany }` option on `better()` is removed, and junction tables are no longer inferred. Declare many-to-many relations with Drizzle's `.through()`. See [relations](https://better-drizzle.com/docs/querying/relations#many-to-many-relations). ([`8b09683`](https://github.com/almeidazs/better-drizzle/commit/8b09683))
- **Some relation shapes are unsupported.** A relation with a relation-level `where`, or a `one` relation declared through a junction, throws `Relation "x" on "t" cannot be loaded: ...` (or `... cannot be filtered: ...`) when used. Drizzle's own `db.query` still handles them. See [limitations](https://better-drizzle.com/docs/guides/limitations). ([`8b09683`](https://github.com/almeidazs/better-drizzle/commit/8b09683), [`9614cdc`](https://github.com/almeidazs/better-drizzle/commit/9614cdc))
- **Plugin `ctx.schema` is the relations config.** In `setup`, hooks, transforms, and extensions it is keyed by table key, and each entry is `{ table, name, relations }`. Plugin code that iterated the schema module with `isTable(...)` must read `ctx.schema[key].table`. ([`8b09683`](https://github.com/almeidazs/better-drizzle/commit/8b09683))
- **Column metadata follows Drizzle 1.x.** `dataType` is now `"<type> <constraint>"` (for example `number int32`), so compare its first part. PostgreSQL arrays are the element column with `dimensions > 0`, and there is no `PgArray` class. See [upgrading](https://better-drizzle.com/docs/guides/upgrading#6-check-error-handling-and-plugin-column-requirements). ([`8b09683`](https://github.com/almeidazs/better-drizzle/commit/8b09683), [`9614cdc`](https://github.com/almeidazs/better-drizzle/commit/9614cdc))

#### API and types

- **Stricter delegate argument types.** Unknown keys are compile errors at the top level and inside `where`, `data`, `create`, `update`, `select`, `include`, and `orderBy`. So is `select` combined with `include` at the same level. `updateEach` options get the same check. See [upgrading](https://better-drizzle.com/docs/guides/upgrading#7-typecheck). ([`64af0c4`](https://github.com/almeidazs/better-drizzle/commit/64af0c4), [`48df501`](https://github.com/almeidazs/better-drizzle/commit/48df501))
- **`OrderType` was removed** from `better-drizzle` and `better-drizzle/zod`. Write `'asc'` / `'desc'`. ([`ce1a297`](https://github.com/almeidazs/better-drizzle/commit/ce1a297))
- **`update()` and `delete()` return `ThrowingWriteResult`.** The type drops `.explain()`, which writes never supported at runtime. `.throw()` is unchanged. See [throwing results](https://better-drizzle.com/docs/writing/throwing-results). ([`ce1a297`](https://github.com/almeidazs/better-drizzle/commit/ce1a297))
- **`cursor()` cursors are typed.** `after` / `before` accept only a cursor object or `null`, not strings. `CursorPaginationResult` types `nextCursor` and `previousCursor` as that cursor object (or `null`), so they can be passed back without a cast. See [cursor pagination](https://better-drizzle.com/docs/querying/pagination#cursor-pagination). ([`fd84f34`](https://github.com/almeidazs/better-drizzle/commit/fd84f34), [`ce1a297`](https://github.com/almeidazs/better-drizzle/commit/ce1a297))
- **`create({ skipDuplicates })` is typed nullable.** When `skipDuplicates` is set and not `false`, the result type includes `null`, matching the runtime result for a skipped insert. See [`create`](https://better-drizzle.com/docs/writing/crud#create). ([`35d4137`](https://github.com/almeidazs/better-drizzle/commit/35d4137))
- **`lock` is rejected inside nested relation args.** Nested `include` and relation `select` args no longer accept `lock` in their types, and the Zod nested query schema drops it. The runtime always rejected it with `LOCK_NOT_SUPPORTED`, and that check stays for untyped callers. See [locks](https://better-drizzle.com/docs/advanced/locks#which-methods-accept-lock). ([`c0013ae`](https://github.com/almeidazs/better-drizzle/commit/c0013ae))
- **`findUnique` requires a unique `where`.** The `where` must pin one row: an equality on every primary key column, on a unique column, or on every column of a composite unique key. `{ equals }` and `param()` count as equalities. Any other `where`, including `{ id: undefined }`, throws `UNIQUE_WHERE_REQUIRED` before SQL runs. This covers regular reads, `.explain()`, and `.prepare()`. Use `findFirst` for non-unique lookups. See [`findUnique`](https://better-drizzle.com/docs/querying/reads#findunique). ([`74c6cf7`](https://github.com/almeidazs/better-drizzle/commit/74c6cf7), [`ba1d106`](https://github.com/almeidazs/better-drizzle/commit/ba1d106))
- **Invalid arguments throw `INVALID_ARGS` (status 400).** Input validation failures used `OPERATION_ERROR` (status 500). These include unknown columns or relations, `select` with `include`, `page` with `skip`, `before` with `after`, an invalid `batchSize`, duplicate `updateEach` `by` values, malformed atomic, array, or JSONB mutations, relation command errors, and Zod parse failures. Dialect limits, schema limits, and wrapped failures keep `OPERATION_ERROR`. See [error codes](https://better-drizzle.com/docs/reference/errors#error-codes). ([`9b9b62b`](https://github.com/almeidazs/better-drizzle/commit/9b9b62b))

#### Plugins

- **`better-drizzle/rules`: `mergeRules` was removed.** Use `merge({ extends, rules })`. `extends` takes one config or an array applied in order, `rules` holds overrides applied last, and `undefined` / `null` / `false` entries are skipped. The merge is shallow per rule. The `MergeRulesOptions` type is new. See [rules](https://better-drizzle.com/docs/plugins/rules#overriding-a-preset). ([`a92a7ce`](https://github.com/almeidazs/better-drizzle/commit/a92a7ce))
- **`config.requires.columns` enforces `optional` and `type`.** An `optional` column may be missing. A `type` that matches neither the column's `columnType`, its full `dataType`, nor one part of it fails bootstrap with `PLUGIN_REQUIRED_COLUMN_TYPE`. Before, `type` was informational and `optional` columns still failed. See [setup and fail-fast requirements](https://better-drizzle.com/docs/plugins/writing-plugins#setup-and-fail-fast-requirements). ([`5fcee33`](https://github.com/almeidazs/better-drizzle/commit/5fcee33))

### Behavior changes

#### Reads

- **Reads are lazy.** `findMany`, `findFirst`, `findOne`, `findUnique`, `count`, `exists`, `paginate`, and `cursor` return lazy thenables, like Drizzle's query builders. The query and its hooks run on the first `then` / `catch` / `finally` (or `await`, or `Promise.all`), and at most once. A read that is never awaited does not run. Wrap reads with `Promise.resolve(...)` for Bun's `expect(...).resolves` / `.rejects`. See [testing](https://better-drizzle.com/docs/guides/testing). ([`9614cdc`](https://github.com/almeidazs/better-drizzle/commit/9614cdc))
- **`.explain()` output.** Cursor probes that need a row from the data query are reported under `deferredProbes` instead of running. A statement that only runs under a condition has a `condition` string. `.explain()` alone never runs the read. See [explain](https://better-drizzle.com/docs/querying/explain). ([`9614cdc`](https://github.com/almeidazs/better-drizzle/commit/9614cdc), [`64af0c4`](https://github.com/almeidazs/better-drizzle/commit/64af0c4))
- **`cursor()` without `orderBy` pages by primary key ascending.** The default direction used to depend on the call, so the first page and later pages could walk different orders. See [cursor pagination](https://better-drizzle.com/docs/querying/pagination#cursor-pagination). ([`337cf06`](https://github.com/almeidazs/better-drizzle/commit/337cf06))
- **`mode: 'insensitive'` works on SQLite and MySQL.** It compiled to `ILIKE` everywhere, which is a syntax error outside PostgreSQL. SQLite and MySQL now compile to `lower(column) like lower(pattern)`, including prepared `param()` patterns. PostgreSQL keeps `ILIKE`. See [string filters](https://better-drizzle.com/docs/querying/filters#string-filters). ([`25be62b`](https://github.com/almeidazs/better-drizzle/commit/25be62b))
- **`mode: 'insensitive'` applies to `equals`, `in`, `notIn`, and scalar `not`.** These string operators used to ignore `mode`, even on PostgreSQL. They now lower both sides in SQL on every dialect: `lower(column) = lower(value)`, `lower(column) in (lower(...), ...)`, and `not (...)` for `notIn` and `not`. `null` and empty lists keep their previous semantics, and a nested `not: { ... }` still reads its own `mode`. Prepared `param()` values work, including PostgreSQL list params (`lower(column) = any(select lower(v) from unnest($1::text[]) v)`). PostgreSQL JSONB path filters and array element predicates (`some` / `every` / `none`) follow the same rule; insensitive array predicates skip the `@>` / `&&` fast paths. See [string filters](https://better-drizzle.com/docs/querying/filters#string-filters). ([`a0808d3`](https://github.com/almeidazs/better-drizzle/commit/a0808d3))
- **`contains` and `startsWith` escape wildcards.** `%`, `_`, and the SQL escape character are matched as literal text. This also applies to case-insensitive filters, nested `not`, PostgreSQL JSONB paths and array elements, and prepared params. Use a Drizzle `SQL` fragment for wildcard patterns. See [string filters](https://better-drizzle.com/docs/querying/filters#string-filters). ([`784ac15`](https://github.com/almeidazs/better-drizzle/commit/784ac15))
- **Unknown `select` keys throw before SQL runs.** This includes keys set to `false` or `undefined` and nested projections. Batch write projections (`updateMany`, `deleteMany`, `updateEach`, `upsertMany`) accept scalar columns only. See [selecting fields](https://better-drizzle.com/docs/querying/selecting-fields#select-narrows-the-payload). ([`fef4015`](https://github.com/almeidazs/better-drizzle/commit/fef4015))
- **JSON path filters require `jsonb`.** Drizzle 1.x types do not tell `json` from `jsonb`, so a path filter on a `json` column type-checks but throws `JSON path filters require a jsonb column; json columns only support whole-document filters.` See [JSONB](https://better-drizzle.com/docs/querying/jsonb). ([`9614cdc`](https://github.com/almeidazs/better-drizzle/commit/9614cdc))

#### Writes

- **`update()` and `delete()` change at most one row.** In `0.2.x` they ran their `where` against every matching row and returned the first. A `where` that pins the primary key, a unique column, or every column of a composite unique key runs as a plain statement. Any other `where` is limited to one row: a keyed `LIMIT 1` subquery on PostgreSQL and SQLite (`ctid` / `rowid` without a primary key), and `SELECT ... FOR UPDATE` plus a write by primary key on MySQL, in the active or an implicit transaction. Use `updateMany` / `deleteMany` for several rows. See [update](https://better-drizzle.com/docs/writing/crud#update) and [delete](https://better-drizzle.com/docs/writing/crud#delete). ([`191dbb3`](https://github.com/almeidazs/better-drizzle/commit/191dbb3), [`7e81d12`](https://github.com/almeidazs/better-drizzle/commit/7e81d12), [`a306502`](https://github.com/almeidazs/better-drizzle/commit/a306502), [`ba1d106`](https://github.com/almeidazs/better-drizzle/commit/ba1d106))
- **Native `upsert` on unique keys.** A `where` with only the columns of one unique key, and the same values in `create`, runs one `INSERT ... ON CONFLICT (columns) DO UPDATE` on PostgreSQL and SQLite. On MySQL it runs `ON DUPLICATE KEY UPDATE` when that key is the only one that can match. A `where` without the primary key no longer takes the primary key path when `create` omits it too. That path inserted without checking `where`; it now reads first. The read-then-write path updates the row it found. See [`upsert`](https://better-drizzle.com/docs/writing/crud#upsert). ([`10179cd`](https://github.com/almeidazs/better-drizzle/commit/10179cd), [`191dbb3`](https://github.com/almeidazs/better-drizzle/commit/191dbb3))
- **MySQL `upsert` checks other unique keys.** The native `ON DUPLICATE KEY UPDATE` path runs only when the table declares no unique key besides the target. MySQL fires that clause on any unique key, so other tables use the read-then-write path instead of possibly updating a different row. ([`9614cdc`](https://github.com/almeidazs/better-drizzle/commit/9614cdc), [`10179cd`](https://github.com/almeidazs/better-drizzle/commit/10179cd))
- **Text-backed timestamps are ISO strings.** `timestamps()` and `softDelete()` write ISO 8601 strings to columns whose `dataType` is a string (such as SQLite `text`) and `Date` objects to native date columns. See [timestamps](https://better-drizzle.com/docs/plugins/timestamps). ([`c0ceef0`](https://github.com/almeidazs/better-drizzle/commit/c0ceef0), [`a0b6e6e`](https://github.com/almeidazs/better-drizzle/commit/a0b6e6e))

#### Transactions, errors, and hooks

- **No rollback handling after `COMMIT`.** When an `afterCommit` callback or an `afterTransactionCommit` hook throws, there is no `ROLLBACK` on SQLite and no `afterRollback` or `onTransactionError` hook. The error is rethrown as-is and the data stays committed. See [transactions](https://better-drizzle.com/docs/advanced/transactions#lifecycle-callbacks). ([`9614cdc`](https://github.com/almeidazs/better-drizzle/commit/9614cdc))
- **All success callbacks run.** Every `afterCommit` callback, `afterTransactionCommit` hook, and plugin `afterRaw` hook runs even when an earlier one throws. The first error is still rethrown. ([`3271b63`](https://github.com/almeidazs/better-drizzle/commit/3271b63))
- **Driver errors are unwrapped.** Drizzle 1.x wraps driver errors in `DrizzleQueryError` with the driver error as `cause`. `BetterDrizzleError.from(...)`, `getDatabaseErrorInfo`, `isUniqueViolation` / `isForeignKeyViolation` / `isNotNullViolation` / `isCheckViolation`, and transaction retries read the innermost driver error, so codes and constraint names still resolve. Code that reads raw driver errors around raw Drizzle calls should read `error.cause`. See [errors](https://better-drizzle.com/docs/reference/errors#getdatabaseerrorinfo). ([`2fddee2`](https://github.com/almeidazs/better-drizzle/commit/2fddee2))
- **Library error codes are kept.** Operation, raw, and transaction wrappers keep the `code` of a `BetterDrizzleError` thrown inside them, for example from a hook or a nested call, instead of replacing it with `OPERATION_ERROR`. ([`2fddee2`](https://github.com/almeidazs/better-drizzle/commit/2fddee2))

#### Plugins

- **`softDelete()` filters every read and every write with a `where`.** In `0.2.x` only `findMany`, `findFirst`, `count`, and `exists` hid deleted rows. `findUnique`, `findOne`, `paginate`, `cursor`, `update`, `updateMany`, `updateEach`, `delete`, and `deleteMany` now do too, and each accepts `deleted`. `deleteMany()` is a soft delete and returns the count it marked. Writes with an empty `where` stay no-ops, `mode: 'hard'` still matches deleted rows, and `upsert` / `upsertMany` and relation loads stay unfiltered. See [soft delete](https://better-drizzle.com/docs/plugins/soft-delete#what-is-filtered). ([`1dad322`](https://github.com/almeidazs/better-drizzle/commit/1dad322))
- **Hard-delete rules check `deleteMany`.** `requireTenantOnDelete`, `noHardDeleteOnSoftDeleteModel`, and `requireHardDeleteReason` now also check `deleteMany`. See [rules](https://better-drizzle.com/docs/plugins/rules#soft-delete-and-system-mode). ([`1dad322`](https://github.com/almeidazs/better-drizzle/commit/1dad322))
- **Raw SQL rules check `$executeRaw`.** `requireRawComment`, `requireRawTimeout`, `noRawMutation`, and `noRawWithoutTransaction` used to check only `$raw`. See [rules](https://better-drizzle.com/docs/plugins/rules#raw-sql). ([`86667cb`](https://github.com/almeidazs/better-drizzle/commit/86667cb))
- **Zod validates numeric "number mode" as `number`.** `numeric(..., { mode: 'number' })` and `decimal(..., { mode: 'number' })` columns validate as numbers in `$zod` (and `$ata`). The default string mode still validates as strings. See [zod](https://better-drizzle.com/docs/plugins/zod). ([`01eeb27`](https://github.com/almeidazs/better-drizzle/commit/01eeb27))
- **Zod cursor schemas reject string cursors.** `$zod` (and `$ata`) cursor args schemas accept only the cursor object or `null`, matching the types. ([`5d1843c`](https://github.com/almeidazs/better-drizzle/commit/5d1843c))

### New features

#### Querying

- **Relation ordering.** `orderBy` accepts fields of `one` relations, nested to any depth, and `{ _count: 'asc' | 'desc' }` for `many` and `.through()` relations. Correlated subqueries keep the root row count and query count unchanged, including nested relation reads and `paginate()`. A missing `one` relation sorts as `NULL`; an empty to-many count is `0`. The cache plugin tracks relation and junction dependencies and preserves nested sort priority; Zod and ATA validate relation sorts. See [ordering by relations](https://better-drizzle.com/docs/querying/reads#ordering-by-relations). ([#67](https://github.com/almeidazs/better-drizzle/pull/67))
- **Relation-sort limits.** `cursor()` and the `cursor` read argument reject relation sorts with `INVALID_ARGS`, including on the first page. Only `_count` is supported for to-many sorts, without a filter; sort subqueries do not apply soft-delete filters. Index the relation keys and check [`.explain()`](https://better-drizzle.com/docs/querying/explain) for the cost of correlated sorts. ([#67](https://github.com/almeidazs/better-drizzle/pull/67))
- **`$where()` on every delegate.** It compiles a typed `where`, including logical operators and relation filters, into a Drizzle `SQL` condition for raw `db.select()` queries, joins, and subqueries. See [reusing a filter in raw Drizzle](https://better-drizzle.com/docs/querying/filters#reusing-a-filter-in-raw-drizzle). ([`309278a`](https://github.com/almeidazs/better-drizzle/commit/309278a), [#63](https://github.com/almeidazs/better-drizzle/pull/63))
- **`NULL` ordering.** `orderBy` accepts `{ direction, nulls: 'first' | 'last' }`. PostgreSQL and SQLite use native `NULLS FIRST` / `NULLS LAST`, MySQL emulates it with `IS NULL`, and unsupported dialects fail when the client is created. See [ordering](https://better-drizzle.com/docs/querying/reads#ordering-limit-and-offset). ([`b129c9f`](https://github.com/almeidazs/better-drizzle/commit/b129c9f))
- **PostgreSQL array filters.** Native array columns get a typed `ArrayFilter` with `has`, `hasEvery`, `hasSome`, `hasNone`, `containedBy`, `isEmpty`, `length` (total cardinality), `equals`, and `not`. They compile to `@>`, `&&`, `<@`, and `cardinality()` with bound params, and throw `ARRAY_QUERY_UNSUPPORTED` outside PostgreSQL. See [arrays](https://better-drizzle.com/docs/querying/arrays). ([`17b91a7`](https://github.com/almeidazs/better-drizzle/commit/17b91a7), [`f5b829c`](https://github.com/almeidazs/better-drizzle/commit/f5b829c), [`0e5ab13`](https://github.com/almeidazs/better-drizzle/commit/0e5ab13), [#50](https://github.com/almeidazs/better-drizzle/pull/50))
- **Array element predicates.** `some`, `every`, and `none` take the element's typed scalar filter. Simple predicates use GIN-compatible containment or overlap, lone comparisons use `ANY` / `ALL`, and the rest use `unnest()`. See [arrays](https://better-drizzle.com/docs/querying/arrays). ([`598d33f`](https://github.com/almeidazs/better-drizzle/commit/598d33f), [`77bfb7b`](https://github.com/almeidazs/better-drizzle/commit/77bfb7b), [#50](https://github.com/almeidazs/better-drizzle/pull/50))
- **JSONB dotted path filters.** `{ metadata: { 'profile.age': { gte: 18 } } }` works without the `{ json: ... }` wrapper. The wrapper is still supported and no longer marked deprecated. See [JSONB](https://better-drizzle.com/docs/querying/jsonb). ([`0000afe`](https://github.com/almeidazs/better-drizzle/commit/0000afe), [`ce1a297`](https://github.com/almeidazs/better-drizzle/commit/ce1a297))

#### Writing

- **Atomic updates.** Number columns accept `set`, `increment`, `decrement`, `multiply`, and `divide`, and boolean columns accept `toggle: true`, in `update`, `updateMany`, `updateEach`, `upsert`, and `upsertMany`. Invalid envelopes, non-finite operands, and division by zero fail before SQL runs. Post-write hooks receive the compiled expressions as `compiled`. See [atomic updates](https://better-drizzle.com/docs/writing/atomic-updates). ([`3194d6d`](https://github.com/almeidazs/better-drizzle/commit/3194d6d), [`6c87e8a`](https://github.com/almeidazs/better-drizzle/commit/6c87e8a), [`6b3c4c1`](https://github.com/almeidazs/better-drizzle/commit/6b3c4c1), [#53](https://github.com/almeidazs/better-drizzle/pull/53))
- **PostgreSQL array mutations.** Array columns accept one of `append`, `prepend`, `remove`, `replace`, or `addUnique` in the same write methods. `addUnique` is one statement that keeps input order and skips existing values. They throw `ARRAY_MUTATION_UNSUPPORTED` outside PostgreSQL. See [arrays](https://better-drizzle.com/docs/querying/arrays). ([`0e5ab13`](https://github.com/almeidazs/better-drizzle/commit/0e5ab13), [#50](https://github.com/almeidazs/better-drizzle/pull/50))
- **JSONB path mutations.** Dotted paths and the `{ json: ... }` wrapper update nested keys through chained `jsonb_set(..., true)` calls in `update`, `updateMany`, `updateEach`, `upsert`, and `upsertMany`. Paths and value types are checked for `$type<T>()` columns. Missing ancestors are created, and unrelated keys are kept. Conflicting paths throw, and dotted paths throw `JSONB_MUTATION_UNSUPPORTED` outside PostgreSQL. See [JSONB path mutations](https://better-drizzle.com/docs/querying/jsonb#jsonb-path-mutations). ([`134d156`](https://github.com/almeidazs/better-drizzle/commit/134d156), [`27678db`](https://github.com/almeidazs/better-drizzle/commit/27678db), [`15a81a7`](https://github.com/almeidazs/better-drizzle/commit/15a81a7), [#54](https://github.com/almeidazs/better-drizzle/pull/54))
- **`updateMany` and `deleteMany` return rows.** Both return `BatchResult<Payload>` (`{ count, data? }`) and accept a scalar `select`. PostgreSQL and SQLite return the affected rows through `RETURNING`. Empty results omit `data`, and MySQL returns only `{ count }`. See [`updateMany`](https://better-drizzle.com/docs/writing/crud#updatemany) and [`deleteMany`](https://better-drizzle.com/docs/writing/crud#deletemany). ([`e4c9b8e`](https://github.com/almeidazs/better-drizzle/commit/e4c9b8e), [`187864f`](https://github.com/almeidazs/better-drizzle/commit/187864f))
- **`createMany` `batchSize`.** Splits large inserts into one statement per chunk, like `upsertMany`. `count` sums the rows actually inserted (including with `skipDuplicates`), returned rows keep input order, and hooks fire once per call. See [`createMany`](https://better-drizzle.com/docs/writing/crud#createmany). ([`32688f4`](https://github.com/almeidazs/better-drizzle/commit/32688f4))
- **`upsertMany` on MySQL.** Compiles to `ON DUPLICATE KEY UPDATE`. `target` must be the primary key or one unique key, rows that set (or defaults that fill) another unique key are rejected, `where` is rejected, and `count` is the number of rows sent. See [`upsertMany` details](https://better-drizzle.com/docs/reference/query-options#upsertmany-details). ([`ba58421`](https://github.com/almeidazs/better-drizzle/commit/ba58421), [`5864e7f`](https://github.com/almeidazs/better-drizzle/commit/5864e7f))
- **`upsertMany` structured `where`.** `where` accepts a structured filter as well as a Drizzle `SQL` fragment. It decides whether the conflicting row is updated and does not filter incoming inserts. PostgreSQL and SQLite only. See [`upsertMany`](https://better-drizzle.com/docs/writing/crud#upsertmany). ([`ba0dfba`](https://github.com/almeidazs/better-drizzle/commit/ba0dfba))

#### Prepared statements

- **Prepared reads.** Mark values with `param(name)` (exported from `better-drizzle`), call `.prepare(name?)` on any read, and run it with `execute(values)`. Plugins and `beforeQuery` run once at prepare time. Intercepts, `afterQuery`, plugin after hooks, and `onError` run on each execution. Statements support per-execution `meta`, `.throw()`, and `explain(values)`. Writes cannot be prepared. See [prepared statements](https://better-drizzle.com/docs/querying/prepared-statements). ([`a2100f6`](https://github.com/almeidazs/better-drizzle/commit/a2100f6), [`f212662`](https://github.com/almeidazs/better-drizzle/commit/f212662), [#64](https://github.com/almeidazs/better-drizzle/pull/64))
- **Params in filters and pagination.** `param()` works as any filter operand and as `take`, `skip`, `page`, `perPage`, `limit`, `cursor`, `after`, and `before`. Its value type is inferred from where it is used. `in` / `notIn` params bind one array and are PostgreSQL-only. Params inside relation `include` / `select` args are not supported. See [where params can go](https://better-drizzle.com/docs/querying/prepared-statements#where-params-can-go). ([`d63fed4`](https://github.com/almeidazs/better-drizzle/commit/d63fed4), [`3fc71cb`](https://github.com/almeidazs/better-drizzle/commit/3fc71cb), [`58aa710`](https://github.com/almeidazs/better-drizzle/commit/58aa710))
- **Prepared types.** `PreparedParam`, `PreparedParams<typeof statement>`, `PreparedResult<typeof statement>`, and `Bindable<T>` are exported. ([`d63fed4`](https://github.com/almeidazs/better-drizzle/commit/d63fed4), [`a2100f6`](https://github.com/almeidazs/better-drizzle/commit/a2100f6))
- **Prepared error codes.** `PREPARED_PARAM_MISSING`, `PREPARED_PARAM_UNKNOWN`, and `PREPARED_UNSUPPORTED`, all with status 400. See [errors](https://better-drizzle.com/docs/querying/prepared-statements#errors). ([`6393092`](https://github.com/almeidazs/better-drizzle/commit/6393092))

#### Pagination

- **`paginate()` takes `page` and `perPage`.** They are shorthand for `skip` + `limit`. `page` cannot be combined with `skip`. `$zod` (and `$ata`) pagination schemas accept them too. See [offset pagination](https://better-drizzle.com/docs/querying/pagination#offset-pagination). ([`cd10154`](https://github.com/almeidazs/better-drizzle/commit/cd10154))
- **Cursor pagination over nullable keys.** Cursor tokens include every `orderBy` field, so a nullable sort key works when you repeat the same order and add a unique, non-null tie-breaker. See [cursor pagination](https://better-drizzle.com/docs/querying/pagination#cursor-pagination). ([`63a924c`](https://github.com/almeidazs/better-drizzle/commit/63a924c))

#### Errors

New `BetterDrizzleErrorCode` values (see [error codes](https://better-drizzle.com/docs/reference/errors#error-codes)):

| Code | Status | Thrown when |
| --- | --- | --- |
| `INVALID_ARGS` | 400 | operation arguments are invalid ([`9b9b62b`](https://github.com/almeidazs/better-drizzle/commit/9b9b62b)) |
| `UNIQUE_WHERE_REQUIRED` | 400 | a `findUnique` `where` does not pin one row ([`74c6cf7`](https://github.com/almeidazs/better-drizzle/commit/74c6cf7)) |
| `PREPARED_PARAM_MISSING` | 400 | `execute()` misses a value for a param ([`6393092`](https://github.com/almeidazs/better-drizzle/commit/6393092)) |
| `PREPARED_PARAM_UNKNOWN` | 400 | `execute()` gets a value for an unknown param ([`6393092`](https://github.com/almeidazs/better-drizzle/commit/6393092)) |
| `PREPARED_UNSUPPORTED` | 400 | the query shape or dialect cannot be prepared ([`6393092`](https://github.com/almeidazs/better-drizzle/commit/6393092)) |
| `ARRAY_QUERY_UNSUPPORTED` | 400 | an array filter runs outside PostgreSQL ([`f5b829c`](https://github.com/almeidazs/better-drizzle/commit/f5b829c)) |
| `ARRAY_MUTATION_UNSUPPORTED` | 400 | an array mutation runs outside PostgreSQL ([`0e5ab13`](https://github.com/almeidazs/better-drizzle/commit/0e5ab13)) |
| `JSONB_MUTATION_UNSUPPORTED` | 400 | a JSONB path mutation runs outside PostgreSQL ([`134d156`](https://github.com/almeidazs/better-drizzle/commit/134d156)) |
| `PLUGIN_REQUIRED_COLUMN_TYPE` | 500 | a plugin's required column has the wrong type ([`5fcee33`](https://github.com/almeidazs/better-drizzle/commit/5fcee33)) |
| `CACHE_INVALID_OPTIONS` | 500 | the cache plugin gets invalid options ([`03e2810`](https://github.com/almeidazs/better-drizzle/commit/03e2810)) |
| `CACHE_SERIALIZATION_ERROR` | 500 | a cached value cannot be serialized or deserialized ([`03e2810`](https://github.com/almeidazs/better-drizzle/commit/03e2810)) |
| `CACHE_STORE_ERROR` | 500 | the cache store fails to read, write, or delete ([`03e2810`](https://github.com/almeidazs/better-drizzle/commit/03e2810)) |
| `CACHE_VALUE_TOO_LARGE` | 500 | a serialized value exceeds `maxSize` ([`03e2810`](https://github.com/almeidazs/better-drizzle/commit/03e2810)) |

`OPERATION_ERROR` now means an operation failed for a reason other than invalid arguments.

#### Transactions and raw SQL

- **Raw option extensions.** Plugins can add typed raw SQL options by augmenting the `RawOptionsExtensions` interface. Raw hooks receive unknown options unchanged in `rawOptions`. The cache plugin's `cache: { invalidate }` raw option uses this. See [writing plugins](https://better-drizzle.com/docs/plugins/writing-plugins). ([`03e2810`](https://github.com/almeidazs/better-drizzle/commit/03e2810))

#### Types

- **Per-table model extensions as interfaces.** Declare them with an interface extending `ModelExtensionTypeResolver` (HKT style). Generic function resolvers still work but can hit TS2589 with Drizzle 1.x types. `better-drizzle/zod` adds `ZodModelExtensionResolver` in this form. See [writing plugins](https://better-drizzle.com/docs/plugins/writing-plugins#defineplugin-generics). ([`64af0c4`](https://github.com/almeidazs/better-drizzle/commit/64af0c4), [`606b068`](https://github.com/almeidazs/better-drizzle/commit/606b068))
- **`PluginModelInfo` exposes keys and relations.** `primaryKey` lists the primary key column keys, and `relations` maps each relation to `{ model, kind, foreignKey, through? }`. ([`03e2810`](https://github.com/almeidazs/better-drizzle/commit/03e2810))
- **Typed batch results.** `updateMany` and `deleteMany` return `BatchResult<PayloadForArgs<...>>` instead of `BatchResult<never>`. See [typing results](https://better-drizzle.com/docs/guides/typing-results). ([`e4c9b8e`](https://github.com/almeidazs/better-drizzle/commit/e4c9b8e))
- **JSDoc matches runtime behavior.** Comments that contradicted the runtime were corrected, and examples no longer show the removed `schema` option. ([`ce1a297`](https://github.com/almeidazs/better-drizzle/commit/ce1a297), [`f78e498`](https://github.com/almeidazs/better-drizzle/commit/f78e498))

#### Plugins

##### UUIDs

- **New `better-drizzle/uuid` plugin.** `uuids()` fills UUIDv7 (default) or UUIDv4 ids on `create`, `createMany`, `upsert`, and `upsertMany` when they are `undefined`, and the id becomes optional in create input types. Ids are insert-only on `upsertMany` conflicts. `uuidv7()` and `uuidv4()` are exported. See [UUIDs](https://better-drizzle.com/docs/plugins/uuid). ([#71](https://github.com/almeidazs/better-drizzle/pull/71))
- **`buuid()` columns.** `better-drizzle/uuid/mysql` (`binary(16)`) and `better-drizzle/uuid/sqlite` (`blob`) store UUIDs as 16 bytes, typed as `string`. Zod and ATA validate them as UUID strings. ([#71](https://github.com/almeidazs/better-drizzle/pull/71))
- **Generated columns for plugin authors.** Plugins declare `generated: { column, models?, accepts? }`; the column becomes optional in create inputs, is listed in `model.generated`, and is optional in Zod and ATA create schemas in any plugin order. See [generated columns](https://better-drizzle.com/docs/plugins/writing-plugins#generated-columns). ([#71](https://github.com/almeidazs/better-drizzle/pull/71))

##### Cache (experimental)

- **New `better-drizzle/cache` plugin and `better-drizzle/cache/redis` store.** Opt-in read caching with versioned keys. Every read accepts `cache` (`true`, a custom key, or `{ ttl, negativeTtl, tags, key, vary, refresh, afterHooks }`), and models can opt in through `models`. Observed writes invalidate their dependencies (rows, entities, model epochs, and declared foreign-key relations). Inside a transaction, invalidation waits for the commit and is dropped on rollback. See [cache](https://better-drizzle.com/docs/plugins/cache). ([`d39942f`](https://github.com/almeidazs/better-drizzle/commit/d39942f), [`dba31b1`](https://github.com/almeidazs/better-drizzle/commit/dba31b1), [#60](https://github.com/almeidazs/better-drizzle/pull/60))
- **Manual invalidation.** `client.$cache.invalidate({ models, tags, keys })` and `client.$cache.clear()`, plus `cache: { invalidate }` hints on writes and raw calls. See [invalidating by hand](https://better-drizzle.com/docs/plugins/cache#invalidating-by-hand). ([`d39942f`](https://github.com/almeidazs/better-drizzle/commit/d39942f), [`03e2810`](https://github.com/almeidazs/better-drizzle/commit/03e2810))
- **Custom stores.** `store` takes any `CacheStore`. The Redis store is one implementation. See [custom stores](https://better-drizzle.com/docs/plugins/cache#custom-stores). ([`d39942f`](https://github.com/almeidazs/better-drizzle/commit/d39942f), [`eaa1cbd`](https://github.com/almeidazs/better-drizzle/commit/eaa1cbd))
- **Prepared reads are cached by their values.** Placeholders hash as named tokens, and the execution values join the key. ([`a7cf39f`](https://github.com/almeidazs/better-drizzle/commit/a7cf39f))
- **Experimental.** Options, the `$cache` API, the store interface, and the entry format can change in a patch release during `0.3.x`. See [stability](https://better-drizzle.com/docs/reference/stability#experimental-in-03x). ([`adc5bb8`](https://github.com/almeidazs/better-drizzle/commit/adc5bb8))

##### ATA (experimental)

- **New `better-drizzle/ata` plugin.** JSON Schema validation of operation inputs and results with [ata-validator](https://ata-validator.com), intended as a faster alternative to `better-drizzle/zod`. `ata-validator` `^1.30.1` is an optional peer. Schemas are on `db.<table>.$ata` as plain objects. They compile on first use, or up front with `precompile: true`. See [ata](https://better-drizzle.com/docs/plugins/ata). ([`9041ec1`](https://github.com/almeidazs/better-drizzle/commit/9041ec1), [`e236826`](https://github.com/almeidazs/better-drizzle/commit/e236826), [`077dc0d`](https://github.com/almeidazs/better-drizzle/commit/077dc0d), [`ecdb8b4`](https://github.com/almeidazs/better-drizzle/commit/ecdb8b4), [`d9482b0`](https://github.com/almeidazs/better-drizzle/commit/d9482b0), [#51](https://github.com/almeidazs/better-drizzle/pull/51))
- **Columns JSON Schema cannot describe.** `Date`, `BigInt`, and `Buffer` columns are checked by residue predicates after schema validation. Array columns and their filters are supported. See [ata](https://better-drizzle.com/docs/plugins/ata#the-three-types-json-schema-cannot-describe). ([`9041ec1`](https://github.com/almeidazs/better-drizzle/commit/9041ec1), [`3cb9a0a`](https://github.com/almeidazs/better-drizzle/commit/3cb9a0a), [`ed05f2b`](https://github.com/almeidazs/better-drizzle/commit/ed05f2b))
- **Relation-aware results.** Result envelopes are validated through `afterCreate`, `afterQuery`, and `afterUpdate`. ([`322fe81`](https://github.com/almeidazs/better-drizzle/commit/322fe81))
- **Small public surface.** The entrypoint exports `ata` (default and named), its types, and `version`. The registry, column, row, `where`, and query-schema builders are internal. ([`0620970`](https://github.com/almeidazs/better-drizzle/commit/0620970))
- **Experimental.** Options, the `$ata` API, and the generated schemas can change in a patch release. See [stability](https://better-drizzle.com/docs/reference/stability#experimental-in-03x). ([`1f5e73e`](https://github.com/almeidazs/better-drizzle/commit/1f5e73e))

##### Zod

- **Drizzle 1.x column schemas.** `$zod` schemas are built from Drizzle 1.x column metadata. The plugin needs a Zod version with Zod 4-compatible schema types (peer `^3.25.0 || ^4.0.0`). See [zod](https://better-drizzle.com/docs/plugins/zod). ([`606b068`](https://github.com/almeidazs/better-drizzle/commit/606b068))
- **New inputs validated.** PostgreSQL array filters and element predicates, atomic update envelopes, `page` / `perPage`, and `orderBy` `nulls`. ([`c6b2b24`](https://github.com/almeidazs/better-drizzle/commit/c6b2b24), [`0e5e9ab`](https://github.com/almeidazs/better-drizzle/commit/0e5e9ab), [`d1916f2`](https://github.com/almeidazs/better-drizzle/commit/d1916f2), [`cd10154`](https://github.com/almeidazs/better-drizzle/commit/cd10154), [`b129c9f`](https://github.com/almeidazs/better-drizzle/commit/b129c9f))

##### Soft delete

- **`deleted` on every filtered operation.** Each filtered read and write accepts `deleted`. `deleteMany()` soft deletes. See the [behavior change](#plugins-1) above and [soft delete](https://better-drizzle.com/docs/plugins/soft-delete). ([`1dad322`](https://github.com/almeidazs/better-drizzle/commit/1dad322))

##### Timestamps

- **ISO strings for text columns.** See the [behavior change](#writes) above. ([`a0b6e6e`](https://github.com/almeidazs/better-drizzle/commit/a0b6e6e))
- **Clock and per-model options.** `now?: () => Date` sets the clock for every timestamp the plugin writes; it is called once per write, so `createdAt` and `updatedAt` match across all rows of the call. `models?: Record<string, false | { createdAt?: string; updatedAt?: string }>` overrides column names for one model or turns the plugin off for it (`false`); unknown model keys are ignored. A column named in `models` that the model lacks makes `better()` throw `PLUGIN_REQUIRED_COLUMN_MISSING`. `TimestampModelOptions` is exported from `better-drizzle/timestamps`. See [timestamps](https://better-drizzle.com/docs/plugins/timestamps). ([`18b8e31`](https://github.com/almeidazs/better-drizzle/commit/18b8e31))

##### Rules

- **`merge({ extends, rules })`** replaces `mergeRules`. See [breaking changes](#plugins). ([`a92a7ce`](https://github.com/almeidazs/better-drizzle/commit/a92a7ce))
- **Raw and `deleteMany` coverage.** Raw rules check `$executeRaw`, and hard-delete rules check `deleteMany`. See [behavior changes](#plugins-1). ([`86667cb`](https://github.com/almeidazs/better-drizzle/commit/86667cb), [`1dad322`](https://github.com/almeidazs/better-drizzle/commit/1dad322))
- **Docs for every rule.** Each rule, preset level, and option is documented. See [rules](https://better-drizzle.com/docs/plugins/rules#rule-reference). ([`a420e77`](https://github.com/almeidazs/better-drizzle/commit/a420e77))

##### ESLint

- No rule changes. The documentation now lists every rule id. See [eslint](https://better-drizzle.com/docs/plugins/eslint). ([`d25e22f`](https://github.com/almeidazs/better-drizzle/commit/d25e22f))

##### Plugin API

- **Intercepts.** `intercept(ctx)` wraps execution after before hooks, transforms, and the client `beforeQuery` hook, so it sees the final args. Plugins compose outermost-first. `next()` runs the operation, `annotate()` passes values to after hooks as `annotations`, and `skipAfterHooks()` skips them. On prepared executions, `ctx.params` holds the values. `.explain()` never runs intercepts. Intercepts are stable API. See [intercepts](https://better-drizzle.com/docs/plugins/writing-plugins#intercepts). ([`03e2810`](https://github.com/almeidazs/better-drizzle/commit/03e2810), [`ac02e78`](https://github.com/almeidazs/better-drizzle/commit/ac02e78), [`eaa1cbd`](https://github.com/almeidazs/better-drizzle/commit/eaa1cbd))
- **`operationArgs` in intercepts.** The intercept context carries the same typed plugin `operationArgs` as before hooks. See [typed operation args](https://better-drizzle.com/docs/plugins/writing-plugins#typed-operation-args). ([`03e2810`](https://github.com/almeidazs/better-drizzle/commit/03e2810))
- **Raw option extensions.** See [transactions and raw SQL](#transactions-and-raw-sql). ([`03e2810`](https://github.com/almeidazs/better-drizzle/commit/03e2810))
- **`requires.columns` checks.** See [breaking changes](#plugins). ([`5fcee33`](https://github.com/almeidazs/better-drizzle/commit/5fcee33))

### Performance

Published overhead figures are on the [benchmarks](https://better-drizzle.com/docs/performance/benchmarks) page.

- **One query per cursor page.** Populated single-primary-key `cursor()` pages derive `hasPrevious` from an inline `EXISTS` check in the data query instead of a second probe. Empty pages and complex shapes keep the exact fallback probe. See [benchmarks](https://better-drizzle.com/docs/performance/benchmarks#reads). ([`a7b38a5`](https://github.com/almeidazs/better-drizzle/commit/a7b38a5))
- **Native upsert on unique keys.** See [writes](#writes). ([`10179cd`](https://github.com/almeidazs/better-drizzle/commit/10179cd))
- **Plain single-row writes on composite unique keys.** `update()` and `delete()` skip the `LIMIT 1` subquery when `where` pins every column of a composite unique key. Unique constraints and non-partial unique indexes are precomputed per table. ([`a306502`](https://github.com/almeidazs/better-drizzle/commit/a306502))
- **Less work per transaction.** Read specs are built lazily, once per delegate and kind, and the `delete` spec is cached on first use. Transactions create delegates for every table and no longer pay for operations they never run. ([`f212662`](https://github.com/almeidazs/better-drizzle/commit/f212662), [`3809019`](https://github.com/almeidazs/better-drizzle/commit/3809019))

### Fixes

- **Binary filters.** Bare `Buffer` values now work in `where` clauses, including lookups by unique binary keys. Both `Buffer` and `Uint8Array` values keep the simple read and cursor query paths. ([#68](https://github.com/almeidazs/better-drizzle/pull/68))
- **Relation keys set to `undefined` in writes.** `create`, `update`, and `upsert` threw `INVALID_ARGS` when a relation key was present but `undefined`, such as optional form data spread into `data`. They are now skipped like `undefined` columns; `null` still throws. ([#69](https://github.com/almeidazs/better-drizzle/pull/69))
- **`version` was stale.** Every entrypoint reported `0.1.1`. All entrypoints now report the package version. ([`058dd28`](https://github.com/almeidazs/better-drizzle/commit/058dd28))
- **JSONB `in`, `notIn`, and `mode`.** JSONB path filters accepted them in the types, but the compiler ignored them, so `in` matched every row. Lists now compile to a type-guarded `IN` per JSON type, and `mode: 'insensitive'` uses `ILIKE`. See [JSONB](https://better-drizzle.com/docs/querying/jsonb#lists-and-case-insensitive-matching). ([`eb0a633`](https://github.com/almeidazs/better-drizzle/commit/eb0a633))
- **MySQL single-row writes.** `update()` and `delete()` with a non-unique `where` read and wrote in separate statements, so a concurrent write could make the returned row differ from the changed one. They now lock the row first. ([`7e81d12`](https://github.com/almeidazs/better-drizzle/commit/7e81d12))
- **MySQL `upsertMany` builder.** `onDuplicateKeyUpdate` is called on its insert builder, which it needs as `this`. ([`5864e7f`](https://github.com/almeidazs/better-drizzle/commit/5864e7f))
- **Soft delete on text columns.** Deletion timestamps are ISO strings for text-backed columns. ([`c0ceef0`](https://github.com/almeidazs/better-drizzle/commit/c0ceef0))
- **Rules limit detection.** Removed fallback keys that no current operation passes. ([`86667cb`](https://github.com/almeidazs/better-drizzle/commit/86667cb))
- **Query parity on Drizzle 1.x.** Restored compiler, explain, and operation parity across PostgreSQL, SQLite, and MySQL after the migration, including quoted PostgreSQL enum array casts. ([`9614cdc`](https://github.com/almeidazs/better-drizzle/commit/9614cdc), [`64af0c4`](https://github.com/almeidazs/better-drizzle/commit/64af0c4))
- **Security.** Resolved CodeQL findings in error message parsing, the ata registry, and the rules plugin. ([`57f2b19`](https://github.com/almeidazs/better-drizzle/commit/57f2b19))

### Documentation and tooling

- **Learning-path docs.** The sidebar is ordered from basic to advanced. "Why better-drizzle?" replaces the comparison page. Examples destructure results, and API examples pair better-drizzle with raw Drizzle code tabs. ([`f9286b3`](https://github.com/almeidazs/better-drizzle/commit/f9286b3), [`ccb2d70`](https://github.com/almeidazs/better-drizzle/commit/ccb2d70), [`ec99046`](https://github.com/almeidazs/better-drizzle/commit/ec99046), [`33178d3`](https://github.com/almeidazs/better-drizzle/commit/33178d3), [#48](https://github.com/almeidazs/better-drizzle/pull/48))
- **New pages.** [null and undefined](https://better-drizzle.com/docs/querying/null-and-undefined), [arrays](https://better-drizzle.com/docs/querying/arrays), [prepared statements](https://better-drizzle.com/docs/querying/prepared-statements), [atomic updates](https://better-drizzle.com/docs/writing/atomic-updates), [typing results](https://better-drizzle.com/docs/guides/typing-results), [testing](https://better-drizzle.com/docs/guides/testing), [observability](https://better-drizzle.com/docs/guides/observability), [troubleshooting](https://better-drizzle.com/docs/guides/troubleshooting), [recipes](https://better-drizzle.com/docs/guides/recipes), [cache](https://better-drizzle.com/docs/plugins/cache), [ata](https://better-drizzle.com/docs/plugins/ata), and this changelog. ([`fa4b450`](https://github.com/almeidazs/better-drizzle/commit/fa4b450), [`2e6c2c8`](https://github.com/almeidazs/better-drizzle/commit/2e6c2c8), [`aa43117`](https://github.com/almeidazs/better-drizzle/commit/aa43117), [`2123019`](https://github.com/almeidazs/better-drizzle/commit/2123019), [`587b769`](https://github.com/almeidazs/better-drizzle/commit/587b769), [`2447d2c`](https://github.com/almeidazs/better-drizzle/commit/2447d2c), [`ca09a00`](https://github.com/almeidazs/better-drizzle/commit/ca09a00), [`077dc0d`](https://github.com/almeidazs/better-drizzle/commit/077dc0d))
- **Expanded pages.** Reference pages (errors, model API, query options, client), plugin hooks and options, frameworks, extensions, dynamic repositories, parity, limitations, JSONB, rules, the Drizzle 1.x upgrade guide, and the [multi-tenancy](https://better-drizzle.com/docs/guides/multi-tenancy) warning that transforms do not isolate tenants. ([`9e752e6`](https://github.com/almeidazs/better-drizzle/commit/9e752e6), [`d25e22f`](https://github.com/almeidazs/better-drizzle/commit/d25e22f), [`736b898`](https://github.com/almeidazs/better-drizzle/commit/736b898), [`8edd825`](https://github.com/almeidazs/better-drizzle/commit/8edd825), [`a420e77`](https://github.com/almeidazs/better-drizzle/commit/a420e77), [`d1562c4`](https://github.com/almeidazs/better-drizzle/commit/d1562c4), [`95bfede`](https://github.com/almeidazs/better-drizzle/commit/95bfede), [`eaa1cbd`](https://github.com/almeidazs/better-drizzle/commit/eaa1cbd), [`d635403`](https://github.com/almeidazs/better-drizzle/commit/d635403))
- **Docs site.** Twoslash type hovers, click-to-copy code blocks, a docs header that hides on scroll, SEO metadata with Open Graph images, `robots.txt`, and a sitemap. ([`5a214f8`](https://github.com/almeidazs/better-drizzle/commit/5a214f8), [`9e7b373`](https://github.com/almeidazs/better-drizzle/commit/9e7b373), [`4ee204b`](https://github.com/almeidazs/better-drizzle/commit/4ee204b), [`a623279`](https://github.com/almeidazs/better-drizzle/commit/a623279), [`1ef96c1`](https://github.com/almeidazs/better-drizzle/commit/1ef96c1))
- **Benchmarks.** `bench:report` samples both sides in one window and publishes ratios. The JSONB suite runs again and checks plan parity. The raw cursor scenario computes `hasPrevious` for real. New suites cover arrays, JSONB mutations, atomic updates, cache workloads (`bench:cache`), validation plugins, and prepared reads. See [parity](https://better-drizzle.com/docs/performance/parity). ([`ed71127`](https://github.com/almeidazs/better-drizzle/commit/ed71127), [`a2e468a`](https://github.com/almeidazs/better-drizzle/commit/a2e468a), [`d58c894`](https://github.com/almeidazs/better-drizzle/commit/d58c894), [`c133b4d`](https://github.com/almeidazs/better-drizzle/commit/c133b4d), [`84583f7`](https://github.com/almeidazs/better-drizzle/commit/84583f7), [`27678db`](https://github.com/almeidazs/better-drizzle/commit/27678db), [`f29295a`](https://github.com/almeidazs/better-drizzle/commit/f29295a), [`cb411d8`](https://github.com/almeidazs/better-drizzle/commit/cb411d8), [`9599064`](https://github.com/almeidazs/better-drizzle/commit/9599064), [`1715289`](https://github.com/almeidazs/better-drizzle/commit/1715289), [`3e5d92c`](https://github.com/almeidazs/better-drizzle/commit/3e5d92c))
- **CI.** A database job runs the PostgreSQL, MySQL, and Redis suites (`test:databases`) against service containers. ([`700d5d2`](https://github.com/almeidazs/better-drizzle/commit/700d5d2))
- **Release workflow.** Publishing waits for the full CI workflow, and the already-published check reads the package name from `package.json`. ([`2232352`](https://github.com/almeidazs/better-drizzle/commit/2232352))
- **Engines.** `engines.node` is `>=18`, matching the build target. ([`9865aa5`](https://github.com/almeidazs/better-drizzle/commit/9865aa5))
- **Package metadata.** New `./ata`, `./cache`, and `./cache/redis` exports, and npm keywords. ([`dba31b1`](https://github.com/almeidazs/better-drizzle/commit/dba31b1), [`a623279`](https://github.com/almeidazs/better-drizzle/commit/a623279))
- **Skill pack.** `skills/better-drizzle` was rewritten for app developers, with querying, writing, plugins, troubleshooting, security, and contributing references, and now covers `$where()`, prepared reads, and `ata`. See [AI](https://better-drizzle.com/docs/ai). ([`8974e21`](https://github.com/almeidazs/better-drizzle/commit/8974e21), [`1f5e73e`](https://github.com/almeidazs/better-drizzle/commit/1f5e73e))
- **Local development.** Docker Compose adds a Redis service, and `.env.example` documents each key. ([`c9b57aa`](https://github.com/almeidazs/better-drizzle/commit/c9b57aa), [`9effb9c`](https://github.com/almeidazs/better-drizzle/commit/9effb9c))

### Pull requests

- docs: rework the docs into a learning path with full API coverage by @almeidazs in https://github.com/almeidazs/better-drizzle/pull/48
- feat: add typed PostgreSQL array filters and updates by @almeidazs in https://github.com/almeidazs/better-drizzle/pull/50
- feat(ata): add new plugin by @mertcanaltin in https://github.com/almeidazs/better-drizzle/pull/51
- feat: add native atomic updates by @almeidazs in https://github.com/almeidazs/better-drizzle/pull/53
- feat(jsonb): add typed JSONB path mutations via jsonb_set by @flaxodotdev in https://github.com/almeidazs/better-drizzle/pull/54
- feat!: support Drizzle ORM 1.x (0.3.0) by @almeidazs in https://github.com/almeidazs/better-drizzle/pull/57
- feat: add opt-in query caching with Redis storage by @almeidazs in https://github.com/almeidazs/better-drizzle/pull/60
- feat: add `$where()` method for compiling typed conditions in raw Drizzle SQL by @joaotonaco in https://github.com/almeidazs/better-drizzle/pull/63
- feat: add prepared statements for reads by @almeidazs in https://github.com/almeidazs/better-drizzle/pull/64
- feat: order reads by relation fields and counts by @joaotonaco in https://github.com/almeidazs/better-drizzle/pull/67
- fix(query): treat binary values as scalars in where filters by @willzys in https://github.com/almeidazs/better-drizzle/pull/68
- fix(relations): ignore relation keys set to undefined in writes by @joaotonaco in https://github.com/almeidazs/better-drizzle/pull/69
- feat(uuid): add uuids plugin with generated columns and buuid by @almeidazs in https://github.com/almeidazs/better-drizzle/pull/71

### New contributors

- @mertcanaltin made their first contribution in https://github.com/almeidazs/better-drizzle/pull/51
- @flaxodotdev made their first contribution in https://github.com/almeidazs/better-drizzle/pull/54
- @joaotonaco made their first contribution in https://github.com/almeidazs/better-drizzle/pull/63
- @willzys made their first contribution in https://github.com/almeidazs/better-drizzle/pull/68

Thanks to all four for their work on this release.

<details>
<summary>All 174 commits since v0.2.0</summary>

- [`ad48e9e`](https://github.com/almeidazs/better-drizzle/commit/ad48e9e) docs: release 0.3.0
- [`967043f`](https://github.com/almeidazs/better-drizzle/commit/967043f) style(web): remove the duplicate search from the docs sidebar
- [`02bbce5`](https://github.com/almeidazs/better-drizzle/commit/02bbce5) feat(web): redesign the landing page and logo
- [`d5c1b7a`](https://github.com/almeidazs/better-drizzle/commit/d5c1b7a) docs: add the 0.3.0 changelog
- [`18b8e31`](https://github.com/almeidazs/better-drizzle/commit/18b8e31) feat(timestamps): add a clock option and per-model overrides
- [`a0808d3`](https://github.com/almeidazs/better-drizzle/commit/a0808d3) feat(query): apply insensitive mode to equality and list filters
- [`32688f4`](https://github.com/almeidazs/better-drizzle/commit/32688f4) feat(createMany): support batchSize
- [`9b9b62b`](https://github.com/almeidazs/better-drizzle/commit/9b9b62b) feat(errors)!: report invalid arguments as INVALID_ARGS
- [`c0013ae`](https://github.com/almeidazs/better-drizzle/commit/c0013ae) refactor(types)!: reject lock inside nested relation args
- [`9865aa5`](https://github.com/almeidazs/better-drizzle/commit/9865aa5) build: declare the supported Node.js range
- [`1f5e73e`](https://github.com/almeidazs/better-drizzle/commit/1f5e73e) docs: mark ata experimental and mention $where in the skill pack
- [`48df501`](https://github.com/almeidazs/better-drizzle/commit/48df501) fix(types): reject unknown updateEach options
- [`10179cd`](https://github.com/almeidazs/better-drizzle/commit/10179cd) perf(upsert): use native conflict handling for unique keys
- [`ba1d106`](https://github.com/almeidazs/better-drizzle/commit/ba1d106) feat(delegate): treat { equals } filters as pinned keys
- [`187864f`](https://github.com/almeidazs/better-drizzle/commit/187864f) docs: document batch mutation results and align benchmark parity
- [`e4c9b8e`](https://github.com/almeidazs/better-drizzle/commit/e4c9b8e) feat: return affected rows from updateMany and deleteMany
- [`ba0dfba`](https://github.com/almeidazs/better-drizzle/commit/ba0dfba) feat: support structured conflict filters in upsertMany
- [`fef4015`](https://github.com/almeidazs/better-drizzle/commit/fef4015) fix: reject unknown select keys before executing queries
- [`784ac15`](https://github.com/almeidazs/better-drizzle/commit/784ac15) fix: escape literal wildcards in contains and startsWith filters
- [`eaa1cbd`](https://github.com/almeidazs/better-drizzle/commit/eaa1cbd) docs: complete 0.3.0 upgrade notes and document cache stores
- [`0620970`](https://github.com/almeidazs/better-drizzle/commit/0620970) refactor(ata)!: stop exporting internal helpers
- [`3809019`](https://github.com/almeidazs/better-drizzle/commit/3809019) perf(delegate): build the delete operation spec on first use
- [`74c6cf7`](https://github.com/almeidazs/better-drizzle/commit/74c6cf7) feat(delegate)!: require a unique where in findUnique
- [`7e81d12`](https://github.com/almeidazs/better-drizzle/commit/7e81d12) fix(delegate): lock MySQL single-row writes in a transaction
- [`a306502`](https://github.com/almeidazs/better-drizzle/commit/a306502) perf(delegate): detect composite unique keys for single-row writes
- [`5d1843c`](https://github.com/almeidazs/better-drizzle/commit/5d1843c) fix(zod,ata): reject string cursors in cursor args schemas
- [`9345101`](https://github.com/almeidazs/better-drizzle/commit/9345101) docs(agents): record single-row writes and insensitive filter dialects
- [`ce1a297`](https://github.com/almeidazs/better-drizzle/commit/ce1a297) refactor(types)!: tighten write results, cursor tokens, and JSDoc
- [`191dbb3`](https://github.com/almeidazs/better-drizzle/commit/191dbb3) fix(delegate): limit update and delete to a single row
- [`25be62b`](https://github.com/almeidazs/better-drizzle/commit/25be62b) fix(query): support insensitive string filters on SQLite and MySQL
- [`2232352`](https://github.com/almeidazs/better-drizzle/commit/2232352) ci(release): gate publishing on CI and fix the published check
- [`109e459`](https://github.com/almeidazs/better-drizzle/commit/109e459) docs(agents): record prepared statement internals
- [`aa43117`](https://github.com/almeidazs/better-drizzle/commit/aa43117) docs(prepared): document prepared statements
- [`3e5d92c`](https://github.com/almeidazs/better-drizzle/commit/3e5d92c) perf(bench): compare prepared reads with Drizzle prepared statements
- [`d07d9df`](https://github.com/almeidazs/better-drizzle/commit/d07d9df) test(prepared): cover prepared reads on SQLite, PostgreSQL, and MySQL
- [`a7cf39f`](https://github.com/almeidazs/better-drizzle/commit/a7cf39f) feat(cache): key prepared reads by their values
- [`f212662`](https://github.com/almeidazs/better-drizzle/commit/f212662) feat(delegate): add .prepare() to read results
- [`ac02e78`](https://github.com/almeidazs/better-drizzle/commit/ac02e78) feat(plugins): pass prepared values to intercepts
- [`a2100f6`](https://github.com/almeidazs/better-drizzle/commit/a2100f6) feat(prepared): compile reads into Drizzle prepared statements
- [`58aa710`](https://github.com/almeidazs/better-drizzle/commit/58aa710) refactor(operations): split cursor pages into build and finish steps
- [`3fc71cb`](https://github.com/almeidazs/better-drizzle/commit/3fc71cb) feat(query): bind prepared params in the where compiler
- [`d63fed4`](https://github.com/almeidazs/better-drizzle/commit/d63fed4) feat(types): accept param() in filters and pagination options
- [`6393092`](https://github.com/almeidazs/better-drizzle/commit/6393092) feat(errors): add prepared statement error codes
- [`309278a`](https://github.com/almeidazs/better-drizzle/commit/309278a) feat: add $where() method for compiling typed conditions in raw Drizzle queries
- [`700d5d2`](https://github.com/almeidazs/better-drizzle/commit/700d5d2) ci: run the PostgreSQL, MySQL, and Redis suites
- [`adc5bb8`](https://github.com/almeidazs/better-drizzle/commit/adc5bb8) docs(cache): mark the cache plugin as experimental
- [`d635403`](https://github.com/almeidazs/better-drizzle/commit/d635403) docs(multi-tenancy): warn that transforms do not isolate tenants
- [`95bfede`](https://github.com/almeidazs/better-drizzle/commit/95bfede) docs(upgrading): cover 0.3.0 breaking and behavior changes
- [`21cfd3e`](https://github.com/almeidazs/better-drizzle/commit/21cfd3e) fix(deps): cap the drizzle-orm peer below 1.0.0-rc.5
- [`dc85b94`](https://github.com/almeidazs/better-drizzle/commit/dc85b94) docs(cache): clarify invalidation guarantees and performance costs
- [`cb411d8`](https://github.com/almeidazs/better-drizzle/commit/cb411d8) perf(bench): add cache workloads with result parity checks
- [`f3f180c`](https://github.com/almeidazs/better-drizzle/commit/f3f180c) test(cache): cover invalidation races and query identity regressions
- [`d39942f`](https://github.com/almeidazs/better-drizzle/commit/d39942f) feat(cache): add versioned read caching and Redis storage
- [`3271b63`](https://github.com/almeidazs/better-drizzle/commit/3271b63) fix(client): finish success callbacks despite observer failures
- [`03e2810`](https://github.com/almeidazs/better-drizzle/commit/03e2810) feat(plugins): add operation intercepts and raw option extensions
- [`ca09a00`](https://github.com/almeidazs/better-drizzle/commit/ca09a00) docs(cache): document the cache plugin and plugin intercepts
- [`143d1b0`](https://github.com/almeidazs/better-drizzle/commit/143d1b0) test(cache): cover cache plugin, Redis store, and plugin intercepts
- [`dba31b1`](https://github.com/almeidazs/better-drizzle/commit/dba31b1) build(cache): register cache and cache/redis subpaths
- [`c9b57aa`](https://github.com/almeidazs/better-drizzle/commit/c9b57aa) chore(dev): add Redis service to docker compose
- [`d26beb8`](https://github.com/almeidazs/better-drizzle/commit/d26beb8) style: format merged and numeric-mode tests
- [`5864e7f`](https://github.com/almeidazs/better-drizzle/commit/5864e7f) fix(upsertMany): guard the MySQL builder method type
- [`a8cabae`](https://github.com/almeidazs/better-drizzle/commit/a8cabae) docs(skill): drop reference to removed type test files
- [`37c6215`](https://github.com/almeidazs/better-drizzle/commit/37c6215) docs: document soft delete coverage, page inputs, and MySQL upsertMany
- [`ba58421`](https://github.com/almeidazs/better-drizzle/commit/ba58421) feat(upsertMany): support MySQL with ON DUPLICATE KEY UPDATE
- [`cd10154`](https://github.com/almeidazs/better-drizzle/commit/cd10154) feat(paginate): accept page and perPage
- [`1dad322`](https://github.com/almeidazs/better-drizzle/commit/1dad322) feat(soft-delete): filter every read and write with a where
- [`c8a8790`](https://github.com/almeidazs/better-drizzle/commit/c8a8790) docs(skill): drop resolved known issues
- [`01eeb27`](https://github.com/almeidazs/better-drizzle/commit/01eeb27) fix(zod,ata): type numeric number mode as a number
- [`2fddee2`](https://github.com/almeidazs/better-drizzle/commit/2fddee2) fix: unwrap Drizzle 1.x errors and keep library error codes
- [`fc2aa00`](https://github.com/almeidazs/better-drizzle/commit/fc2aa00) chore(types): remove inline type test files
- [`861551f`](https://github.com/almeidazs/better-drizzle/commit/861551f) docs: align AI page and query options with the skill rewrite
- [`8974e21`](https://github.com/almeidazs/better-drizzle/commit/8974e21) docs(skill): rewrite skill pack for app developers
- [`1d8e642`](https://github.com/almeidazs/better-drizzle/commit/1d8e642) chore(docker): remove unused Postgres init schema
- [`9effb9c`](https://github.com/almeidazs/better-drizzle/commit/9effb9c) chore(env): document each .env.example key
- [`1d90dc8`](https://github.com/almeidazs/better-drizzle/commit/1d90dc8) docs: correct JSONB shorthand and junction caveats
- [`58f6da2`](https://github.com/almeidazs/better-drizzle/commit/58f6da2) test: cover Drizzle 1.x migration failure modes
- [`f78e498`](https://github.com/almeidazs/better-drizzle/commit/f78e498) docs(types): drop removed schema option from JSDoc examples
- [`d1562c4`](https://github.com/almeidazs/better-drizzle/commit/d1562c4) docs: expand Drizzle 1.x upgrade guide and fix stale pages
- [`f56a6d8`](https://github.com/almeidazs/better-drizzle/commit/f56a6d8) docs(agent): update migration guidance and audit notes
- [`2b4736e`](https://github.com/almeidazs/better-drizzle/commit/2b4736e) docs: explain the Drizzle 1.x API and upgrade path
- [`89a98da`](https://github.com/almeidazs/better-drizzle/commit/89a98da) perf(bench): migrate parity suites and stabilize array samples
- [`a0b6e6e`](https://github.com/almeidazs/better-drizzle/commit/a0b6e6e) fix(plugins): align timestamps and guardrails with Drizzle 1.x
- [`ed05f2b`](https://github.com/almeidazs/better-drizzle/commit/ed05f2b) fix(ata): validate Drizzle 1.x residues and overrides
- [`606b068`](https://github.com/almeidazs/better-drizzle/commit/606b068) fix(zod): support Drizzle 1.x column schemas
- [`64af0c4`](https://github.com/almeidazs/better-drizzle/commit/64af0c4) fix(types): preserve delegate inference with Drizzle 1.x
- [`9614cdc`](https://github.com/almeidazs/better-drizzle/commit/9614cdc) fix(core): restore query parity across Drizzle 1.x dialects
- [`8b09683`](https://github.com/almeidazs/better-drizzle/commit/8b09683) feat(core): bind Drizzle 1.x relation metadata
- [`15a81a7`](https://github.com/almeidazs/better-drizzle/commit/15a81a7) fix(jsonb): typecheck upsertMany paths and document mutations
- [`27678db`](https://github.com/almeidazs/better-drizzle/commit/27678db) fix(jsonb): harden path mutations and add parity coverage
- [`63a924c`](https://github.com/almeidazs/better-drizzle/commit/63a924c) fix(orderBy): handle nullable cursor ordering
- [`134d156`](https://github.com/almeidazs/better-drizzle/commit/134d156) feat(jsonb): add typed JSONB path mutations via jsonb_set
- [`b129c9f`](https://github.com/almeidazs/better-drizzle/commit/b129c9f) feat(orderBy): add null directions
- [`0000afe`](https://github.com/almeidazs/better-drizzle/commit/0000afe) feat(jsonb): add dotted path filter shorthand
- [`57f2b19`](https://github.com/almeidazs/better-drizzle/commit/57f2b19) fix(security): resolve codeql findings
- [`2123019`](https://github.com/almeidazs/better-drizzle/commit/2123019) docs: document atomic updates
- [`f29295a`](https://github.com/almeidazs/better-drizzle/commit/f29295a) bench(writes): measure atomic update parity
- [`cd69e4f`](https://github.com/almeidazs/better-drizzle/commit/cd69e4f) test(core): cover atomic write operations
- [`d1916f2`](https://github.com/almeidazs/better-drizzle/commit/d1916f2) feat(validation): support atomic update envelopes
- [`6b3c4c1`](https://github.com/almeidazs/better-drizzle/commit/6b3c4c1) feat(hooks): expose compiled atomic expressions
- [`6c87e8a`](https://github.com/almeidazs/better-drizzle/commit/6c87e8a) feat(core): compile atomic scalar mutations
- [`3194d6d`](https://github.com/almeidazs/better-drizzle/commit/3194d6d) feat(types): add atomic scalar update inputs
- [`03bab62`](https://github.com/almeidazs/better-drizzle/commit/03bab62) docs(ata): align navigation icon with zod
- [`322fe81`](https://github.com/almeidazs/better-drizzle/commit/322fe81) fix(ata): validate relation-aware operations
- [`1715289`](https://github.com/almeidazs/better-drizzle/commit/1715289) test(ata): benchmark validation as a share of a real operation
- [`3cb9a0a`](https://github.com/almeidazs/better-drizzle/commit/3cb9a0a) feat(ata): array columns and their filters
- [`9599064`](https://github.com/almeidazs/better-drizzle/commit/9599064) test(ata): benchmark the two validation plugins on one table
- [`d9482b0`](https://github.com/almeidazs/better-drizzle/commit/d9482b0) chore(ata): require the ata-validator release the where clause needs
- [`077dc0d`](https://github.com/almeidazs/better-drizzle/commit/077dc0d) feat(ata): the plugin, its registry and the operation schemas
- [`e236826`](https://github.com/almeidazs/better-drizzle/commit/e236826) feat(ata): where clause schema
- [`ecdb8b4`](https://github.com/almeidazs/better-drizzle/commit/ecdb8b4) chore(ata): ata-validator as an optional peer, like zod
- [`9041ec1`](https://github.com/almeidazs/better-drizzle/commit/9041ec1) feat(ata): column mapping and row validation
- [`e90c0f8`](https://github.com/almeidazs/better-drizzle/commit/e90c0f8) docs(agents): document array predicate compilation
- [`2447d2c`](https://github.com/almeidazs/better-drizzle/commit/2447d2c) docs(guides): add common query recipes
- [`2769b85`](https://github.com/almeidazs/better-drizzle/commit/2769b85) docs(arrays): explain element predicate semantics
- [`e716815`](https://github.com/almeidazs/better-drizzle/commit/e716815) perf(arrays): benchmark element predicate parity
- [`254561c`](https://github.com/almeidazs/better-drizzle/commit/254561c) test(arrays): cover element predicate edge cases
- [`0e5e9ab`](https://github.com/almeidazs/better-drizzle/commit/0e5e9ab) feat(zod): validate array element predicates
- [`77bfb7b`](https://github.com/almeidazs/better-drizzle/commit/77bfb7b) feat(query): compile array element predicates
- [`598d33f`](https://github.com/almeidazs/better-drizzle/commit/598d33f) feat(types): expose array element predicates
- [`587b769`](https://github.com/almeidazs/better-drizzle/commit/587b769) docs(guides): add typing, testing, observability, and troubleshooting guides
- [`fa4b450`](https://github.com/almeidazs/better-drizzle/commit/fa4b450) docs(querying): add a null and undefined page
- [`bcf7a2d`](https://github.com/almeidazs/better-drizzle/commit/bcf7a2d) docs(arrays): document GIN indexing and helper usage
- [`a2e5b56`](https://github.com/almeidazs/better-drizzle/commit/a2e5b56) chore: note array filter constraints in AGENTS.md
- [`0e5ab13`](https://github.com/almeidazs/better-drizzle/commit/0e5ab13) feat(arrays): refine PostgreSQL array filters, zod schemas, and types
- [`7d9de7c`](https://github.com/almeidazs/better-drizzle/commit/7d9de7c) docs(arrays): improve raw SQL examples
- [`f196cc3`](https://github.com/almeidazs/better-drizzle/commit/f196cc3) docs(agents): record array filter constraints
- [`2e6c2c8`](https://github.com/almeidazs/better-drizzle/commit/2e6c2c8) docs(arrays): document PostgreSQL array filters
- [`84583f7`](https://github.com/almeidazs/better-drizzle/commit/84583f7) perf(arrays): add PostgreSQL parity benchmark
- [`dbbfb77`](https://github.com/almeidazs/better-drizzle/commit/dbbfb77) test(arrays): cover typed PostgreSQL array filters
- [`c6b2b24`](https://github.com/almeidazs/better-drizzle/commit/c6b2b24) feat(zod): validate PostgreSQL array filters
- [`f5b829c`](https://github.com/almeidazs/better-drizzle/commit/f5b829c) feat(arrays): compile PostgreSQL array predicates
- [`17b91a7`](https://github.com/almeidazs/better-drizzle/commit/17b91a7) feat(arrays): add typed array filters
- [`b20db3f`](https://github.com/almeidazs/better-drizzle/commit/b20db3f) perf(web): use cnfast
- [`9705f39`](https://github.com/almeidazs/better-drizzle/commit/9705f39) perf(web): skip twoslash hovers on object keys and Drizzle tabs
- [`53f0406`](https://github.com/almeidazs/better-drizzle/commit/53f0406) style(web): restore oxfmt formatting
- [`5001150`](https://github.com/almeidazs/better-drizzle/commit/5001150) chore(deps): sync lockfile with twoslash dependencies
- [`5a214f8`](https://github.com/almeidazs/better-drizzle/commit/5a214f8) feat(doc): add twoslash support
- [`736b898`](https://github.com/almeidazs/better-drizzle/commit/736b898) docs(guides): expand frameworks, extensions, repositories, parity, and limitations
- [`d25e22f`](https://github.com/almeidazs/better-drizzle/commit/d25e22f) docs(plugins): document hooks, eslint rule ids, and plugin options
- [`9e752e6`](https://github.com/almeidazs/better-drizzle/commit/9e752e6) docs(reference): complete errors, model API, query options, and client pages
- [`5fcee33`](https://github.com/almeidazs/better-drizzle/commit/5fcee33) feat(plugins): honor optional and type in requires.columns
- [`86667cb`](https://github.com/almeidazs/better-drizzle/commit/86667cb) fix(rules): check $executeRaw in raw rules and drop unused limit keys
- [`fd84f34`](https://github.com/almeidazs/better-drizzle/commit/fd84f34) fix(types): accept only cursor objects in cursor() after/before
- [`337cf06`](https://github.com/almeidazs/better-drizzle/commit/337cf06) fix(cursor): page by primary key ascending when orderBy is omitted
- [`35d4137`](https://github.com/almeidazs/better-drizzle/commit/35d4137) fix(types): type create() as nullable when skipDuplicates is set
- [`834a2b7`](https://github.com/almeidazs/better-drizzle/commit/834a2b7) fix(web): theme-aware sponsors, light logo, and mobile grid overflow
- [`90dfa9e`](https://github.com/almeidazs/better-drizzle/commit/90dfa9e) style: replace em dashes with plain hyphens
- [`dcd5c96`](https://github.com/almeidazs/better-drizzle/commit/dcd5c96) chore: note sidebar and example conventions in AGENTS.md
- [`ec99046`](https://github.com/almeidazs/better-drizzle/commit/ec99046) docs: destructure results in pagination, write, and explain examples
- [`ccb2d70`](https://github.com/almeidazs/better-drizzle/commit/ccb2d70) docs: replace the comparison page with Why better-drizzle?
- [`f9286b3`](https://github.com/almeidazs/better-drizzle/commit/f9286b3) docs: reorder the sidebar into a basic-to-advanced learning path
- [`4ee204b`](https://github.com/almeidazs/better-drizzle/commit/4ee204b) feat(web): slim docs header that hides on scroll down
- [`97f4be7`](https://github.com/almeidazs/better-drizzle/commit/97f4be7) fix(web): highlight docs code with the oniguruma engine
- [`a420e77`](https://github.com/almeidazs/better-drizzle/commit/a420e77) docs(rules): document every rule, preset level, and option
- [`a92a7ce`](https://github.com/almeidazs/better-drizzle/commit/a92a7ce) feat(rules)!: replace mergeRules with merge({ extends, rules })
- [`0bd18b5`](https://github.com/almeidazs/better-drizzle/commit/0bd18b5) feat(web): highlight text selection with the brand color
- [`0f8e9a6`](https://github.com/almeidazs/better-drizzle/commit/0f8e9a6) docs(README): fix broken links and make it better
- [`ee941b3`](https://github.com/almeidazs/better-drizzle/commit/ee941b3) chore(deps): sync lockfile optional peer metadata
- [`a2e468a`](https://github.com/almeidazs/better-drizzle/commit/a2e468a) docs: republish benchmark numbers from a single measured run
- [`ed71127`](https://github.com/almeidazs/better-drizzle/commit/ed71127) feat(benchmark): add an interleaved overhead report
- [`d58c894`](https://github.com/almeidazs/better-drizzle/commit/d58c894) fix(benchmark): repair the JSONB suite and assert plan parity
- [`c133b4d`](https://github.com/almeidazs/better-drizzle/commit/c133b4d) fix(benchmark): compute hasPrevious on the raw cursor side
- [`a7b38a5`](https://github.com/almeidazs/better-drizzle/commit/a7b38a5) perf(cursor): resolve navigation metadata in one indexed query
- [`ce4326a`](https://github.com/almeidazs/better-drizzle/commit/ce4326a) chore(oxlint): fix error ""core.plugins is possibly undefined""
- [`a623279`](https://github.com/almeidazs/better-drizzle/commit/a623279) feat(web): improve SEO metadata across the docs site
- [`1ef96c1`](https://github.com/almeidazs/better-drizzle/commit/1ef96c1) feat(web): add robots.txt and sitemap
- [`c0ceef0`](https://github.com/almeidazs/better-drizzle/commit/c0ceef0) fix(soft-delete): serialize text deletion timestamps
- [`b4f3652`](https://github.com/almeidazs/better-drizzle/commit/b4f3652) docs(agents): document docs code block conventions
- [`33178d3`](https://github.com/almeidazs/better-drizzle/commit/33178d3) docs(web): add raw Drizzle comparison tabs to API examples
- [`8edd825`](https://github.com/almeidazs/better-drizzle/commit/8edd825) docs(web): expand JSONB filters guide with examples and semantics
- [`9e7b373`](https://github.com/almeidazs/better-drizzle/commit/9e7b373) feat(web): copy docs code blocks on click with a shine effect
- [`eb0a633`](https://github.com/almeidazs/better-drizzle/commit/eb0a633) feat(query): support in, notIn and insensitive mode on JSONB paths
- [`23527ae`](https://github.com/almeidazs/better-drizzle/commit/23527ae) docs(web): update documentation site for v0.2.0
- [`058dd28`](https://github.com/almeidazs/better-drizzle/commit/058dd28) fix(version): align version constants with 0.2.0

</details>

**Full changelog**: https://github.com/almeidazs/better-drizzle/compare/v0.2.0...v0.3.0

Release: https://github.com/almeidazs/better-drizzle/releases/tag/v0.3.0

## 0.2.0 - 2026-09-20

`0.2.0` ships as **one package**: the official plugins move from scoped `@better-drizzle/*` packages to subpath exports of `better-drizzle`. It also adds relation reads and writes, cursor pagination, row locks, `.explain()`, and the `rules`, `eslint`, and `zod` plugins.

> [!IMPORTANT]
> Breaking change for plugin imports. Follow [Upgrading to 0.2](https://better-drizzle.com/docs/guides/upgrading#upgrading-to-02) before you bump.

### Breaking changes

- **One package, plugin subpaths.** Import plugins from `better-drizzle/rules`, `better-drizzle/zod`, `better-drizzle/soft-delete`, `better-drizzle/timestamps`, and `better-drizzle/eslint`. The scoped `@better-drizzle/*` packages are discontinued. `better-drizzle/plugins` is the new plugin authoring entry point. Every entry point ships ESM, CommonJS, and declarations. ([`0858c46`](https://github.com/almeidazs/better-drizzle/commit/0858c46), [`a066b56`](https://github.com/almeidazs/better-drizzle/commit/a066b56))

### Features

- **Relation reads.** Nested `include` and `select` with `where`, `orderBy`, `take`, `skip`, and `cursor` at any depth, loaded by a batched loader (one query per relation node). ([`42184d3`](https://github.com/almeidazs/better-drizzle/commit/42184d3))
- **Relation writes.** `connect` on create, and `connect`, `disconnect`, and `set` on update, in an implicit transaction. ([`42184d3`](https://github.com/almeidazs/better-drizzle/commit/42184d3), [`64964a5`](https://github.com/almeidazs/better-drizzle/commit/64964a5))
- **Many-to-many.** Simple junction tables are inferred. ([`5942400`](https://github.com/almeidazs/better-drizzle/commit/5942400))
- **`include._count`.** Relation totals as correlated subqueries, without extra round trips. ([`997f471`](https://github.com/almeidazs/better-drizzle/commit/997f471))
- **`cursor()` pagination.** `before` / `after` cursors with `hasNext` and `hasPrevious`; `paginate()` stays offset-based. ([`37b6c8d`](https://github.com/almeidazs/better-drizzle/commit/37b6c8d))
- **Row locks.** `lock` on reads for PostgreSQL and MySQL, with `skipLocked`, `noWait`, and `locks.transactionsOnly`. ([`b64c100`](https://github.com/almeidazs/better-drizzle/commit/b64c100))
- **`.explain()`.** Every read returns a lazy thenable with a cross-dialect `.explain()`; relation stages are reported as deferred. ([`5c451f9`](https://github.com/almeidazs/better-drizzle/commit/5c451f9), [`ce0e3e4`](https://github.com/almeidazs/better-drizzle/commit/ce0e3e4))
- **`extends()`.** Client-level helpers that survive `$withContext()` and transaction clients. ([`290e1cb`](https://github.com/almeidazs/better-drizzle/commit/290e1cb))
- **PostgreSQL JSONB filters.** Typed path filters on JSONB columns. ([`7dd1cf9`](https://github.com/almeidazs/better-drizzle/commit/7dd1cf9))
- **`better-drizzle/rules`.** Runtime guardrails (for example, writes without `where`). ([`c7ae257`](https://github.com/almeidazs/better-drizzle/commit/c7ae257))
- **`better-drizzle/eslint`.** The statically checkable rules as an ESLint plugin. ([`2c0fe7b`](https://github.com/almeidazs/better-drizzle/commit/2c0fe7b))
- **`better-drizzle/zod`.** Generated Zod schemas on `db.<table>.$zod` and opt-out runtime validation. ([`28822b0`](https://github.com/almeidazs/better-drizzle/commit/28822b0))
- **Agent skill.** A first-party skill pack for AI coding agents. ([`cca8f2e`](https://github.com/almeidazs/better-drizzle/commit/cca8f2e))

### Fixes

- To-many relation filters correlate on the parent row. ([#27](https://github.com/almeidazs/better-drizzle/pull/27))
- Relation filters under `include` reference the aliased parent table. ([`421d320`](https://github.com/almeidazs/better-drizzle/commit/421d320))
- The zod plugin keeps relation commands during validation. ([`07b0e34`](https://github.com/almeidazs/better-drizzle/commit/07b0e34))

### Tooling

- Builds move to tsdown; linting and formatting move to Ultracite (oxlint and oxfmt). ([`0c6216f`](https://github.com/almeidazs/better-drizzle/commit/0c6216f), [`369d7b7`](https://github.com/almeidazs/better-drizzle/commit/369d7b7))
- A real-database integration suite and a full parity benchmark suite. ([`0a433ac`](https://github.com/almeidazs/better-drizzle/commit/0a433ac), [`c705d68`](https://github.com/almeidazs/better-drizzle/commit/c705d68))

### Contributors

- [@almeidazs](https://github.com/almeidazs)
- [@pablofdezr](https://github.com/pablofdezr) made their first contribution in [#27](https://github.com/almeidazs/better-drizzle/pull/27)

**Full changelog**: https://github.com/almeidazs/better-drizzle/compare/better-drizzle@0.1.1...v0.2.0

Release: https://github.com/almeidazs/better-drizzle/releases/tag/v0.2.0

## 0.1.1 - 2026-06-27

Adds batch updates with `updateEach` and scoped metadata with `$withContext()`. `@better-drizzle/timestamps@0.1.1` and `@better-drizzle/soft-delete@0.1.1` were released with it.

### Features

- **`updateEach`.** Updates many rows with different values in one `UPDATE ... SET column = CASE ... END` statement. Takes `by`, `data`, `update`, an optional `where`, optional scalar `select`, and `onEmpty`, and rejects duplicate `by` values. ([#12](https://github.com/almeidazs/better-drizzle/pull/12))
- **`$withContext(meta)`.** Returns a client that merges default `meta` into every operation, raw SQL call, and transaction lifecycle payload. ([`68f3f9a`](https://github.com/almeidazs/better-drizzle/commit/68f3f9a))

### Packaging

- `engines.node` is `>=18`. ([#13](https://github.com/almeidazs/better-drizzle/pull/13))
- The package is built with `tsc`, and the `drizzle-orm` peer range starts at `0.30.0`. ([`bb76ca6`](https://github.com/almeidazs/better-drizzle/commit/bb76ca6))

### Documentation

- `llms.txt` on the docs site, and `updateEach` docs. ([`d5dd2bb`](https://github.com/almeidazs/better-drizzle/commit/d5dd2bb), [`0888e84`](https://github.com/almeidazs/better-drizzle/commit/0888e84))

**Full changelog**: https://github.com/almeidazs/better-drizzle/commits/better-drizzle@0.1.1

Release: https://github.com/almeidazs/better-drizzle/releases/tag/better-drizzle%400.1.1

## 0.1.0 - 2026-06-26

First public release of better-drizzle: a repository-style API on top of an existing Drizzle client, for PostgreSQL, MySQL, and SQLite.

### Highlights

- **Table delegates.** `findMany`, `findFirst`, `findUnique`, `create`, `createMany`, `update`, `updateMany`, `delete`, `deleteMany`, `upsert`, `count`, `exists`, `paginate`, and `repository(name)` lookup.
- **Duplicate handling.** `create` and `createMany` accept `skipDuplicates` (`true` or a list of columns). ([#10](https://github.com/almeidazs/better-drizzle/pull/10))
- **`upsertMany`.** Native-first batch upsert with conflict targets, update strategies, chunking, and optional `select`. It fails fast on unsupported dialects instead of falling back to per-row loops. ([#11](https://github.com/almeidazs/better-drizzle/pull/11))
- **Transactions.** Nested savepoints, retries, explicit rollback, `afterCommit`, and `afterRollback`.
- **Raw SQL.** Safe `$raw` and `$executeRaw` with tagged templates, plus `$rawUnsafe` behind an explicit option.
- **Plugins.** Typed `operationArgs`, transforms, hooks, client extensions, and model extensions. ([#1](https://github.com/almeidazs/better-drizzle/pull/1))
- **Errors.** `BetterDrizzleError` with `code`, `status`, and structured metadata.

### Official plugins

The first plugins shipped as separate scoped packages (merged into `better-drizzle` in 0.2.0):

- `@better-drizzle/timestamps`
- `@better-drizzle/soft-delete`

### Documentation

- Docs site and landing page at [better-drizzle.com](https://better-drizzle.com), with a support matrix. ([#9](https://github.com/almeidazs/better-drizzle/pull/9))

### Contributors

- [@almeidazs](https://github.com/almeidazs)
- [@leoisadev1](https://github.com/leoisadev1) made their first contribution in [#9](https://github.com/almeidazs/better-drizzle/pull/9)
