Support matrix
Dialect support for reads, relation loading, writes, upsertMany, locks, raw SQL, transactions, and package compatibility.
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.
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 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.
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.
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.
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.
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.
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.