# Support matrix

> Dialect support for reads, relation loading, writes, upsertMany, locks, raw SQL, transactions, and package compatibility.

URL: https://better-drizzle.com/docs/reference/support-matrix

This page is the practical compatibility matrix for the current better-drizzle release, which supports only Drizzle ORM 1.x.

> **Drizzle ORM 1.x only:** better-drizzle supports **only Drizzle ORM 1.x** (`drizzle-orm`
> `>=1.0.0-rc.4 <1.0.0-rc.5`; later release candidates are not supported yet) with its `defineRelations(...)`
> relations API. `drizzle-orm` 0.x is **not supported** by the current
> release - projects on 0.x must stay on better-drizzle `0.2.x`. See
> [upgrading](/docs/guides/upgrading-0-3).

## Package compatibility

Everything ships in one package, so there is a single version line to track.

| Requirement        | Version                                    |
| ------------------ | ------------------------------------------ |
| `drizzle-orm` peer | `>=1.0.0-rc.4 <1.0.0-rc.5` (1.x only)      |
| `drizzle-orm` 0.x  | Not supported - use better-drizzle `0.2.x` |
| `typescript` peer  | `^5`                                       |
| Node               | `>=20`                                     |

The current release is tested against `drizzle-orm@1.0.0-rc.4`. The Drizzle instance must be created with `relations` from `defineRelations(...)`.

Projects on `drizzle-orm` 0.x stay on better-drizzle `0.2.x`, whose `drizzle-orm` peer is `^0.30.0`. See [upgrading](/docs/guides/upgrading-0-3) for the move to 1.x.

## Entrypoints

| Entrypoint                   | Contents                                    | ESM | CJS | Types |
| ---------------------------- | ------------------------------------------- | --- | --- | ----- |
| `better-drizzle`             | `better()`, `definePlugin()`, errors, types | Yes | Yes | Yes   |
| `better-drizzle/plugins`     | plugin authoring API                        | Yes | Yes | Yes   |
| `better-drizzle/eslint`      | ESLint plugin (static guardrails)           | Yes | Yes | Yes   |
| `better-drizzle/rules`       | runtime guardrails plugin                   | Yes | Yes | Yes   |
| `better-drizzle/soft-delete` | soft delete plugin                          | Yes | Yes | Yes   |
| `better-drizzle/timestamps`  | timestamps plugin                           | Yes | Yes | Yes   |
| `better-drizzle/zod`         | Zod schema + validation plugin              | Yes | Yes | Yes   |
| `better-drizzle/ata`         | JSON Schema + ata validation plugin         | Yes | Yes | Yes   |

Upgrading from the scoped `@better-drizzle/*` packages? See the
[0.2 upgrade guide](/docs/guides/upgrading-0-2).

## Core delegate API

| Capability                                       | PostgreSQL | SQLite | MySQL |
| ------------------------------------------------ | ---------- | ------ | ----- |
| `findMany`, `findFirst`, `findOne`, `findUnique` | Yes        | Yes    | Yes   |
| `count`, `exists`, `paginate`, `cursor`          | Yes        | Yes    | Yes   |
| `create`, `update`, `delete`, `upsert`           | Yes        | Yes    | Yes   |
| `createMany`, `updateMany`, `deleteMany`         | Yes        | Yes    | Yes   |
| `updateEach` (single `CASE` statement)           | Yes        | Yes    | Yes   |
| `repository(name)`                               | Yes        | Yes    | Yes   |
| `extends(...)`, `$withContext(meta)`             | Yes        | Yes    | Yes   |
| `$withState(...)`, `$withoutPlugins()`           | Yes        | Yes    | Yes   |
| `$where(...)`                                    | Yes        | Yes    | Yes   |
| hooks and plugins                                | Yes        | Yes    | Yes   |

## Relation loading

| Capability                                                            | PostgreSQL | SQLite   | MySQL    |
| --------------------------------------------------------------------- | ---------- | -------- | -------- |
| nested `include` / `select` (batched loader)                          | Yes        | Yes      | Yes      |
| many-to-many declared with `.through()`                               | Yes        | Yes      | Yes      |
| filtered relations (relation-level `where`), `one` through a junction | Rejected   | Rejected | Rejected |
| per-parent `take` / `skip` (`row_number()`)                           | Yes        | Yes      | Yes      |
| `include.count.select` (correlated subqueries)                        | Yes        | Yes      | Yes      |
| relation writes: `connect`                                            | Yes        | Yes      | Yes      |
| relation writes: `disconnect`, `set`                                  | Yes        | Yes      | Yes      |
| relation loading combined with `lock`                                 | Rejected   | N/A      | Rejected |

Relation loading and row locks are mutually exclusive by design - nested
relation stages run as separate queries, so a root lock would not cover them.
The combination fails fast instead of silently dropping the lock.

## String filters

| Capability                                                    | PostgreSQL                                                    | SQLite                      | MySQL                       |
| ------------------------------------------------------------- | ------------------------------------------------------------- | --------------------------- | --------------------------- |
| `contains`, `startsWith`, `endsWith` (`LIKE`)                 | Yes                                                           | Yes                         | Yes                         |
| `mode: 'insensitive'` (patterns)                              | `ILIKE`                                                       | `lower(col) LIKE lower(?)`  | `lower(col) LIKE lower(?)`  |
| `mode: 'insensitive'` (`equals`, `in`, `notIn`, scalar `not`) | `lower(col) = lower(?)`                                       | `lower(col) = lower(?)`     | `lower(col) = lower(?)`     |
| `in` / `notIn` with `param()` and `mode: 'insensitive'`       | `lower(col) = any(select lower(v) from unnest($1::text[]) v)` | No (`PREPARED_UNSUPPORTED`) | No (`PREPARED_UNSUPPORTED`) |

Without `mode`, case sensitivity follows the dialect: SQLite `LIKE` ignores
ASCII case by default, `=` compares case-sensitively, and MySQL follows the
column collation. With `mode: 'insensitive'`, both sides are lowered in SQL,
so case folding follows the database. SQLite's
`lower()` only folds ASCII letters unless the ICU extension is loaded.

## Query plans

| Capability                                         | PostgreSQL | SQLite  | MySQL   |
| -------------------------------------------------- | ---------- | ------- | ------- |
| `.explain()` on read helpers                       | Yes        | Yes     | Yes     |
| `analyze`, `verbose`, `costs`, `timing`, `summary` | Yes        | ignored | ignored |
| relation stages under `deferredRelations`          | Yes        | Yes     | Yes     |

SQLite uses `EXPLAIN QUERY PLAN`; MySQL uses the best available `EXPLAIN`
form. `.explain()` throws on Cloudflare D1. Unsupported flags are reported back under `ignoredOptions` rather than
silently dropped.

## Prepared statements

| Capability                                      | PostgreSQL | SQLite                 | MySQL                  |
| ----------------------------------------------- | ---------- | ---------------------- | ---------------------- |
| `param()` + `.prepare()` on every read helper   | Yes        | Yes                    | Yes                    |
| named server-side statements (`.prepare(name)`) | Yes        | name ignored           | name ignored           |
| `in` / `notIn` params                           | Yes        | `PREPARED_UNSUPPORTED` | `PREPARED_UNSUPPORTED` |
| params in array filters and JSONB paths         | Yes        | N/A                    | N/A                    |

See [prepared statements](/docs/querying/prepared-statements).

## JSONB filters and mutations

| Capability                         | PostgreSQL | SQLite   | MySQL    |
| ---------------------------------- | ---------- | -------- | -------- |
| typed JSONB scalar paths           | Yes        | rejected | rejected |
| JSONB path mutations (`jsonb_set`) | Yes        | rejected | rejected |
| JSONB full-document replacement    | Yes        | Yes      | Yes      |

Typed JSONB filters require `jsonb(...).$type<T>()` and support scalar dot paths only. For typed path mutations, both dotted paths and the `{ json: ... }` wrapper check declared object paths and values. Untyped columns keep open path names and JSON-encodable values in both forms. Mutations can write object or array values, create absent ancestors, and preserve existing object ancestors and unrelated keys; overlapping paths are rejected. Full-document replacements are not dialect-gated. Path filters require a `jsonb` column; `json` columns support whole-document filters only.

## Native arrays

| Capability                                                                                                             | PostgreSQL | SQLite   | MySQL    |
| ---------------------------------------------------------------------------------------------------------------------- | ---------- | -------- | -------- |
| array filters (`has`, `hasEvery`, `hasSome`, `hasNone`, `containedBy`, `isEmpty`, `length`, `some` / `every` / `none`) | Yes        | rejected | rejected |
| array mutations (`append`, `prepend`, `remove`, `replace`, `addUnique`)                                                | Yes        | rejected | rejected |

Rejected calls throw `ARRAY_QUERY_UNSUPPORTED` or `ARRAY_MUTATION_UNSUPPORTED`. See [arrays](/docs/querying/arrays).

## Conflict-handling features

| Capability                                    | PostgreSQL | SQLite | MySQL                           |
| --------------------------------------------- | ---------- | ------ | ------------------------------- |
| `create({ skipDuplicates: true })`            | Yes        | Yes    | dialect-dependent               |
| `createMany({ skipDuplicates: true })`        | Yes        | Yes    | dialect-dependent               |
| `skipDuplicates: ['column']` explicit targets | Yes        | Yes    | No                              |
| `upsertMany`                                  | Yes        | Yes    | Yes, with a unique-key `target` |
| `upsertMany` `where`                          | Yes        | Yes    | rejected                        |

`upsertMany` is native-first on every dialect. On MySQL it compiles to `ON DUPLICATE KEY UPDATE`, so the `target` must be the primary key or one unique key, and the rows cannot set another unique key. See [limitations](/docs/guides/limitations#upsertmany-is-native-first).

## Result projection support

| Capability                                | PostgreSQL | SQLite | MySQL                        |
| ----------------------------------------- | ---------- | ------ | ---------------------------- |
| `select` on reads                         | Yes        | Yes    | Yes                          |
| `include` on reads                        | Yes        | Yes    | Yes                          |
| `select` / `include` on single-row writes | Yes        | Yes    | Yes                          |
| `select` on `upsertMany`                  | Yes        | Yes    | No `RETURNING`: only `count` |
| relation `include` on `upsertMany`        | No         | No     | No                           |

## Raw SQL options

| Capability                      | PostgreSQL       | SQLite           | MySQL            |
| ------------------------------- | ---------------- | ---------------- | ---------------- |
| `$raw` / `$executeRaw`          | Yes              | Yes              | Yes              |
| `$rawUnsafe` with `allowUnsafe` | Yes              | Yes              | Yes              |
| `comment` support               | Best support     | limited          | limited          |
| `timeoutMs`                     | driver-dependent | driver-dependent | driver-dependent |
| raw hooks                       | Yes              | Yes              | Yes              |

## Transactions

| Capability                       | PostgreSQL          | SQLite  | MySQL            |
| -------------------------------- | ------------------- | ------- | ---------------- |
| `transaction()`                  | Yes                 | Yes     | Yes              |
| nested transactions / savepoints | Yes                 | Yes     | Yes              |
| `afterCommit` / `afterRollback`  | Yes                 | Yes     | Yes              |
| retry options                    | Yes                 | Yes     | Yes              |
| `isolationLevel`                 | Yes                 | ignored | driver-dependent |
| `readOnly`                       | Yes                 | ignored | driver-dependent |
| transaction `comment`            | PostgreSQL-oriented | ignored | driver-dependent |

## Cloudflare D1

D1 is detected automatically from `drizzle-orm/d1`. It reports as SQLite, with these differences:

| Capability                                                        | D1                                                                                                    |
| ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| transactions, `SAVEPOINT`                                         | Not available: `client.transaction()` and relation writes run as separate statements with no rollback |
| `createMany`, `upsertMany`, `updateEach` over 100 bind parameters | Split into chunks that fit, not atomic                                                                |
| relation loading over 100 bind parameters                         | Batched                                                                                               |
| `.explain()`                                                      | Throws `OPERATION_ERROR`                                                                              |

See [Cloudflare D1](/docs/guides/cloudflare-d1).

## Row locks

| Capability                                 | PostgreSQL | SQLite   | MySQL    |
| ------------------------------------------ | ---------- | -------- | -------- |
| `lock: 'update'` / `lock: 'share'`         | Yes        | rejected | Yes      |
| `lock: 'noKeyUpdate'` / `lock: 'keyShare'` | Yes        | rejected | rejected |
| `skipLocked` / `noWait`                    | Yes        | N/A      | Yes      |
| `tables` (lock specific tables)            | Yes        | rejected | rejected |
| `locks.transactionsOnly`                   | Yes        | Yes      | Yes      |

## Official plugins

| Plugin                       | PostgreSQL | SQLite | MySQL |
| ---------------------------- | ---------- | ------ | ----- |
| `better-drizzle/rules`       | Yes        | Yes    | Yes   |
| `better-drizzle/timestamps`  | Yes        | Yes    | Yes   |
| `better-drizzle/soft-delete` | Yes        | Yes    | Yes   |
| `better-drizzle/zod`         | Yes        | Yes    | Yes   |
| `better-drizzle/ata`         | Yes        | Yes    | Yes   |

`better-drizzle/eslint` is dialect-independent - it is a static linter, not a
runtime plugin.

## Policy

When a feature is not safely supported by the current dialect, better-drizzle prefers to fail fast with a structured error instead of silently degrading to a slower or weaker implementation.
