better-drizzle
Reference

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.

RequirementVersion
drizzle-orm peer>=1.0.0-rc.4 <1.0.0-rc.5 (1.x only)
drizzle-orm 0.xNot 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

EntrypointContentsESMCJSTypes
better-drizzlebetter(), definePlugin(), errors, typesYesYesYes
better-drizzle/pluginsplugin authoring APIYesYesYes
better-drizzle/eslintESLint plugin (static guardrails)YesYesYes
better-drizzle/rulesruntime guardrails pluginYesYesYes
better-drizzle/soft-deletesoft delete pluginYesYesYes
better-drizzle/timestampstimestamps pluginYesYesYes
better-drizzle/zodZod schema + validation pluginYesYesYes
better-drizzle/ataJSON Schema + ata validation pluginYesYesYes

Upgrading from the scoped @better-drizzle/* packages? See the 0.2 upgrade guide.

Core delegate API

CapabilityPostgreSQLSQLiteMySQL
findMany, findFirst, findOne, findUniqueYesYesYes
count, exists, paginate, cursorYesYesYes
create, update, delete, upsertYesYesYes
createMany, updateMany, deleteManyYesYesYes
updateEach (single CASE statement)YesYesYes
repository(name)YesYesYes
extends(...), $withContext(meta)YesYesYes
$withState(...), $withoutPlugins()YesYesYes
$where(...)YesYesYes
hooks and pluginsYesYesYes

Relation loading

CapabilityPostgreSQLSQLiteMySQL
nested include / select (batched loader)YesYesYes
many-to-many declared with .through()YesYesYes
filtered relations (relation-level where), one through a junctionRejectedRejectedRejected
per-parent take / skip (row_number())YesYesYes
include.count.select (correlated subqueries)YesYesYes
relation writes: connectYesYesYes
relation writes: disconnect, setYesYesYes
relation loading combined with lockRejectedN/ARejected

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

CapabilityPostgreSQLSQLiteMySQL
contains, startsWith, endsWith (LIKE)YesYesYes
mode: 'insensitive' (patterns)ILIKElower(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

CapabilityPostgreSQLSQLiteMySQL
.explain() on read helpersYesYesYes
analyze, verbose, costs, timing, summaryYesignoredignored
relation stages under deferredRelationsYesYesYes

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

CapabilityPostgreSQLSQLiteMySQL
param() + .prepare() on every read helperYesYesYes
named server-side statements (.prepare(name))Yesname ignoredname ignored
in / notIn paramsYesPREPARED_UNSUPPORTEDPREPARED_UNSUPPORTED
params in array filters and JSONB pathsYesN/AN/A

See prepared statements.

JSONB filters and mutations

CapabilityPostgreSQLSQLiteMySQL
typed JSONB scalar pathsYesrejectedrejected
JSONB path mutations (jsonb_set)Yesrejectedrejected
JSONB full-document replacementYesYesYes

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

CapabilityPostgreSQLSQLiteMySQL
array filters (has, hasEvery, hasSome, hasNone, containedBy, isEmpty, length, some / every / none)Yesrejectedrejected
array mutations (append, prepend, remove, replace, addUnique)Yesrejectedrejected

Rejected calls throw ARRAY_QUERY_UNSUPPORTED or ARRAY_MUTATION_UNSUPPORTED. See arrays.

Conflict-handling features

CapabilityPostgreSQLSQLiteMySQL
create({ skipDuplicates: true })YesYesdialect-dependent
createMany({ skipDuplicates: true })YesYesdialect-dependent
skipDuplicates: ['column'] explicit targetsYesYesNo
upsertManyYesYesYes, with a unique-key target
upsertMany whereYesYesrejected

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

CapabilityPostgreSQLSQLiteMySQL
select on readsYesYesYes
include on readsYesYesYes
select / include on single-row writesYesYesYes
select on upsertManyYesYesNo RETURNING: only count
relation include on upsertManyNoNoNo

Raw SQL options

CapabilityPostgreSQLSQLiteMySQL
$raw / $executeRawYesYesYes
$rawUnsafe with allowUnsafeYesYesYes
comment supportBest supportlimitedlimited
timeoutMsdriver-dependentdriver-dependentdriver-dependent
raw hooksYesYesYes

Transactions

CapabilityPostgreSQLSQLiteMySQL
transaction()YesYesYes
nested transactions / savepointsYesYesYes
afterCommit / afterRollbackYesYesYes
retry optionsYesYesYes
isolationLevelYesignoreddriver-dependent
readOnlyYesignoreddriver-dependent
transaction commentPostgreSQL-orientedignoreddriver-dependent

Cloudflare D1

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

CapabilityD1
transactions, SAVEPOINTNot available: client.transaction() and relation writes run as separate statements with no rollback
createMany, upsertMany, updateEach over 100 bind parametersSplit into chunks that fit, not atomic
relation loading over 100 bind parametersBatched
.explain()Throws OPERATION_ERROR

See Cloudflare D1.

Row locks

CapabilityPostgreSQLSQLiteMySQL
lock: 'update' / lock: 'share'YesrejectedYes
lock: 'noKeyUpdate' / lock: 'keyShare'Yesrejectedrejected
skipLocked / noWaitYesN/AYes
tables (lock specific tables)Yesrejectedrejected
locks.transactionsOnlyYesYesYes

Official plugins

PluginPostgreSQLSQLiteMySQL
better-drizzle/rulesYesYesYes
better-drizzle/timestampsYesYesYes
better-drizzle/soft-deleteYesYesYes
better-drizzle/zodYesYesYes
better-drizzle/ataYesYesYes

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.

On this page