Changelog
Every better-drizzle release, newest first. Also available as Markdown and RSS. To move between versions, see the upgrading guide.
0.4.0 (unreleased)
Upcoming
0.4.0 adds tenant isolation: the experimental better-drizzle/tenant plugin, built on a new core primitive for plugin authors, 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-deleteused to filter only the root query, so deleted rows appeared ininclude, relationselect, relation filters (some,every,none,is,isNot),include._count, and relation sorts. They are hidden there now. Thedeletedargument still controls the root rows only.connect,disconnect, andsetno longer reach deleted rows.$withoutPlugins()still reads deleted rows at every level. See soft delete. - 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 aBetterDrizzleErrorwith codeOPERATION_ERRORand operation"transaction".onTransactionErrorhooks still receive the normalized error. See transactions. (#81) - Soft deletes and restores keep other scopes. The inner update of a soft
delete()/deleteMany()and ofrestore()/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/tenantscopes 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, andstrict. See 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.extendClientreceivesscopeandwithScope(state, meta?); hooks, transforms, intercepts, and raw hooks receivescopes. Clients without scope plugins keep their previous code paths. See 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 passmeta.tenantBypass = { reason: '$withoutPlugins' }.- New error codes.
TENANT_REQUIRED(403),TENANT_MISMATCH(403),TENANT_RAW_DENIED(403), andTENANT_ALREADY_SCOPED(400). See error codes. ClientExtensionTypeResolver. A type-level resolver for client extensions typed from the schema, passed as the seventhdefinePlugingeneric. Its optionalhiddenremoves table keys from the root client type.better()returnsBetterRootClient. It equalsBetterDrizzleClientunless a plugin hides models (the tenant plugin'sstrictoption), 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
datahas no column values after plugin transforms,updatereturns the current row,updateManyreturns{ count: 0 }, andupsertreturns the existing row, instead of failing with Drizzle'sNo values to set. (#82)
Contributors
- @almeidazs
- @joaotonaco in #81 and #82
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, sobetter(db)needs noschemaoption. (#57) - Prepared reads. Mark values with
param()and call.prepare()on any read. (#64) - Read cache (experimental). Opt-in Redis caching with automatic, relation-aware invalidation after writes and commits. (#60)
- Atomic updates.
increment,decrement,multiply,divide, andtogglewithout reading first. (#53) - PostgreSQL arrays and JSONB. Typed array filters and mutations, and typed JSONB path updates through
jsonb_set. (#50, #54) $where(). Compile typed better-drizzle conditions into raw Drizzle SQL. (#63)- Relation ordering. Sort by fields of
onerelations, at any depth, or by_countof to-many relations, with correlated subqueries. (#67) - UUIDs.
better-drizzle/uuidfills UUIDv7 ids on create, makes them optional in create types, andbuuid()stores them as 16 bytes on MySQL and SQLite. (#71) - ATA plugin (experimental). JSON Schema validation built on ata-validator, a faster alternative to the Zod plugin. (#51)
- Also:
createMany({ batchSize }), insensitiveequals/in/notIn, a timestampsnowclock with per-model overrides, andINVALID_ARGS(400) for invalid arguments.
Install
npm install better-drizzle@0.3.0 drizzle-orm@1.0.0-rc.4The 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.
This release has breaking changes. Follow the upgrading guide before you bump. The same notes live in the 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-ormpeer range is>=1.0.0-rc.4 <1.0.0-rc.5(was^0.30.0). It is capped below1.0.0-rc.5because the typecheck fails against that snapshot. Projects ondrizzle-orm0.x must stay onbetter-drizzle@~0.2.0. See upgrading. (8b09683,21cfd3e, #57) better()no longer takesschema. Tables and relations come from the Drizzle instance (db._.relations, built bydrizzle({ client, relations })). The options argument is optional, sobetter(db)works. A Drizzle instance withoutrelationsthrowsNo tables found on the Drizzle instance. .... Only tables in the relations config get a delegate. See upgrading. (8b09683)- Type parameters are
typeof relations.BetterDrizzleClient,BetterTableKey,WhereArg, explicitbetter<...>()calls, and every other exported helper type take the relations config instead of the schema module. See upgrading. (8b09683,64af0c4) - Many-to-many comes only from
.through(). Therelations: { inferManyToMany, manyToMany }option onbetter()is removed, and junction tables are no longer inferred. Declare many-to-many relations with Drizzle's.through(). See relations. (8b09683) - Some relation shapes are unsupported. A relation with a relation-level
where, or aonerelation declared through a junction, throwsRelation "x" on "t" cannot be loaded: ...(or... cannot be filtered: ...) when used. Drizzle's owndb.querystill handles them. See limitations. (8b09683,9614cdc) - Plugin
ctx.schemais the relations config. Insetup, hooks, transforms, and extensions it is keyed by table key, and each entry is{ table, name, relations }. Plugin code that iterated the schema module withisTable(...)must readctx.schema[key].table. (8b09683) - Column metadata follows Drizzle 1.x.
dataTypeis now"<type> <constraint>"(for examplenumber int32), so compare its first part. PostgreSQL arrays are the element column withdimensions > 0, and there is noPgArrayclass. See upgrading. (8b09683,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, andorderBy. So isselectcombined withincludeat the same level.updateEachoptions get the same check. See upgrading. (64af0c4,48df501) OrderTypewas removed frombetter-drizzleandbetter-drizzle/zod. Write'asc'/'desc'. (ce1a297)update()anddelete()returnThrowingWriteResult. The type drops.explain(), which writes never supported at runtime..throw()is unchanged. See throwing results. (ce1a297)cursor()cursors are typed.after/beforeaccept only a cursor object ornull, not strings.CursorPaginationResulttypesnextCursorandpreviousCursoras that cursor object (ornull), so they can be passed back without a cast. See cursor pagination. (fd84f34,ce1a297)create({ skipDuplicates })is typed nullable. WhenskipDuplicatesis set and notfalse, the result type includesnull, matching the runtime result for a skipped insert. Seecreate. (35d4137)lockis rejected inside nested relation args. Nestedincludeand relationselectargs no longer acceptlockin their types, and the Zod nested query schema drops it. The runtime always rejected it withLOCK_NOT_SUPPORTED, and that check stays for untyped callers. See locks. (c0013ae)findUniquerequires a uniquewhere. Thewheremust pin one row: an equality on every primary key column, on a unique column, or on every column of a composite unique key.{ equals }andparam()count as equalities. Any otherwhere, including{ id: undefined }, throwsUNIQUE_WHERE_REQUIREDbefore SQL runs. This covers regular reads,.explain(), and.prepare(). UsefindFirstfor non-unique lookups. SeefindUnique. (74c6cf7,ba1d106)- Invalid arguments throw
INVALID_ARGS(status 400). Input validation failures usedOPERATION_ERROR(status 500). These include unknown columns or relations,selectwithinclude,pagewithskip,beforewithafter, an invalidbatchSize, duplicateupdateEachbyvalues, malformed atomic, array, or JSONB mutations, relation command errors, and Zod parse failures. Dialect limits, schema limits, and wrapped failures keepOPERATION_ERROR. See error codes. (9b9b62b)
Plugins
better-drizzle/rules:mergeRuleswas removed. Usemerge({ extends, rules }).extendstakes one config or an array applied in order,rulesholds overrides applied last, andundefined/null/falseentries are skipped. The merge is shallow per rule. TheMergeRulesOptionstype is new. See rules. (a92a7ce)config.requires.columnsenforcesoptionalandtype. Anoptionalcolumn may be missing. Atypethat matches neither the column'scolumnType, its fulldataType, nor one part of it fails bootstrap withPLUGIN_REQUIRED_COLUMN_TYPE. Before,typewas informational andoptionalcolumns still failed. See setup and fail-fast requirements. (5fcee33)
Behavior changes
Reads
- Reads are lazy.
findMany,findFirst,findOne,findUnique,count,exists,paginate, andcursorreturn lazy thenables, like Drizzle's query builders. The query and its hooks run on the firstthen/catch/finally(orawait, orPromise.all), and at most once. A read that is never awaited does not run. Wrap reads withPromise.resolve(...)for Bun'sexpect(...).resolves/.rejects. See testing. (9614cdc) .explain()output. Cursor probes that need a row from the data query are reported underdeferredProbesinstead of running. A statement that only runs under a condition has aconditionstring..explain()alone never runs the read. See explain. (9614cdc,64af0c4)cursor()withoutorderBypages 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. (337cf06)mode: 'insensitive'works on SQLite and MySQL. It compiled toILIKEeverywhere, which is a syntax error outside PostgreSQL. SQLite and MySQL now compile tolower(column) like lower(pattern), including preparedparam()patterns. PostgreSQL keepsILIKE. See string filters. (25be62b)mode: 'insensitive'applies toequals,in,notIn, and scalarnot. These string operators used to ignoremode, even on PostgreSQL. They now lower both sides in SQL on every dialect:lower(column) = lower(value),lower(column) in (lower(...), ...), andnot (...)fornotInandnot.nulland empty lists keep their previous semantics, and a nestednot: { ... }still reads its ownmode. Preparedparam()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. (a0808d3)containsandstartsWithescape wildcards.%,_, and the SQL escape character are matched as literal text. This also applies to case-insensitive filters, nestednot, PostgreSQL JSONB paths and array elements, and prepared params. Use a DrizzleSQLfragment for wildcard patterns. See string filters. (784ac15)- Unknown
selectkeys throw before SQL runs. This includes keys set tofalseorundefinedand nested projections. Batch write projections (updateMany,deleteMany,updateEach,upsertMany) accept scalar columns only. See selecting fields. (fef4015) - JSON path filters require
jsonb. Drizzle 1.x types do not telljsonfromjsonb, so a path filter on ajsoncolumn type-checks but throwsJSON path filters require a jsonb column; json columns only support whole-document filters.See JSONB. (9614cdc)
Writes
update()anddelete()change at most one row. In0.2.xthey ran theirwhereagainst every matching row and returned the first. Awherethat pins the primary key, a unique column, or every column of a composite unique key runs as a plain statement. Any otherwhereis limited to one row: a keyedLIMIT 1subquery on PostgreSQL and SQLite (ctid/rowidwithout a primary key), andSELECT ... FOR UPDATEplus a write by primary key on MySQL, in the active or an implicit transaction. UseupdateMany/deleteManyfor several rows. See update and delete. (191dbb3,7e81d12,a306502,ba1d106)- Native
upserton unique keys. Awherewith only the columns of one unique key, and the same values increate, runs oneINSERT ... ON CONFLICT (columns) DO UPDATEon PostgreSQL and SQLite. On MySQL it runsON DUPLICATE KEY UPDATEwhen that key is the only one that can match. Awherewithout the primary key no longer takes the primary key path whencreateomits it too. That path inserted without checkingwhere; it now reads first. The read-then-write path updates the row it found. Seeupsert. (10179cd,191dbb3) - MySQL
upsertchecks other unique keys. The nativeON DUPLICATE KEY UPDATEpath 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,10179cd) - Text-backed timestamps are ISO strings.
timestamps()andsoftDelete()write ISO 8601 strings to columns whosedataTypeis a string (such as SQLitetext) andDateobjects to native date columns. See timestamps. (c0ceef0,a0b6e6e)
Transactions, errors, and hooks
- No rollback handling after
COMMIT. When anafterCommitcallback or anafterTransactionCommithook throws, there is noROLLBACKon SQLite and noafterRollbackoronTransactionErrorhook. The error is rethrown as-is and the data stays committed. See transactions. (9614cdc) - All success callbacks run. Every
afterCommitcallback,afterTransactionCommithook, and pluginafterRawhook runs even when an earlier one throws. The first error is still rethrown. (3271b63) - Driver errors are unwrapped. Drizzle 1.x wraps driver errors in
DrizzleQueryErrorwith the driver error ascause.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 readerror.cause. See errors. (2fddee2) - Library error codes are kept. Operation, raw, and transaction wrappers keep the
codeof aBetterDrizzleErrorthrown inside them, for example from a hook or a nested call, instead of replacing it withOPERATION_ERROR. (2fddee2)
Plugins
softDelete()filters every read and every write with awhere. In0.2.xonlyfindMany,findFirst,count, andexistshid deleted rows.findUnique,findOne,paginate,cursor,update,updateMany,updateEach,delete, anddeleteManynow do too, and each acceptsdeleted.deleteMany()is a soft delete and returns the count it marked. Writes with an emptywherestay no-ops,mode: 'hard'still matches deleted rows, andupsert/upsertManyand relation loads stay unfiltered. See soft delete. (1dad322)- Hard-delete rules check
deleteMany.requireTenantOnDelete,noHardDeleteOnSoftDeleteModel, andrequireHardDeleteReasonnow also checkdeleteMany. See rules. (1dad322) - Raw SQL rules check
$executeRaw.requireRawComment,requireRawTimeout,noRawMutation, andnoRawWithoutTransactionused to check only$raw. See rules. (86667cb) - Zod validates numeric "number mode" as
number.numeric(..., { mode: 'number' })anddecimal(..., { mode: 'number' })columns validate as numbers in$zod(and$ata). The default string mode still validates as strings. See zod. (01eeb27) - Zod cursor schemas reject string cursors.
$zod(and$ata) cursor args schemas accept only the cursor object ornull, matching the types. (5d1843c)
New features
Querying
- Relation ordering.
orderByaccepts fields ofonerelations, nested to any depth, and{ _count: 'asc' | 'desc' }formanyand.through()relations. Correlated subqueries keep the root row count and query count unchanged, including nested relation reads andpaginate(). A missingonerelation sorts asNULL; an empty to-many count is0. The cache plugin tracks relation and junction dependencies and preserves nested sort priority; Zod and ATA validate relation sorts. See ordering by relations. (#67) - Relation-sort limits.
cursor()and thecursorread argument reject relation sorts withINVALID_ARGS, including on the first page. Only_countis supported for to-many sorts, without a filter; sort subqueries do not apply soft-delete filters. Index the relation keys and check.explain()for the cost of correlated sorts. (#67) $where()on every delegate. It compiles a typedwhere, including logical operators and relation filters, into a DrizzleSQLcondition for rawdb.select()queries, joins, and subqueries. See reusing a filter in raw Drizzle. (309278a, #63)NULLordering.orderByaccepts{ direction, nulls: 'first' | 'last' }. PostgreSQL and SQLite use nativeNULLS FIRST/NULLS LAST, MySQL emulates it withIS NULL, and unsupported dialects fail when the client is created. See ordering. (b129c9f)- PostgreSQL array filters. Native array columns get a typed
ArrayFilterwithhas,hasEvery,hasSome,hasNone,containedBy,isEmpty,length(total cardinality),equals, andnot. They compile to@>,&&,<@, andcardinality()with bound params, and throwARRAY_QUERY_UNSUPPORTEDoutside PostgreSQL. See arrays. (17b91a7,f5b829c,0e5ab13, #50) - Array element predicates.
some,every, andnonetake the element's typed scalar filter. Simple predicates use GIN-compatible containment or overlap, lone comparisons useANY/ALL, and the rest useunnest(). See arrays. (598d33f,77bfb7b, #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. (0000afe,ce1a297)
Writing
- Atomic updates. Number columns accept
set,increment,decrement,multiply, anddivide, and boolean columns accepttoggle: true, inupdate,updateMany,updateEach,upsert, andupsertMany. Invalid envelopes, non-finite operands, and division by zero fail before SQL runs. Post-write hooks receive the compiled expressions ascompiled. See atomic updates. (3194d6d,6c87e8a,6b3c4c1, #53) - PostgreSQL array mutations. Array columns accept one of
append,prepend,remove,replace, oraddUniquein the same write methods.addUniqueis one statement that keeps input order and skips existing values. They throwARRAY_MUTATION_UNSUPPORTEDoutside PostgreSQL. See arrays. (0e5ab13, #50) - JSONB path mutations. Dotted paths and the
{ json: ... }wrapper update nested keys through chainedjsonb_set(..., true)calls inupdate,updateMany,updateEach,upsert, andupsertMany. 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 throwJSONB_MUTATION_UNSUPPORTEDoutside PostgreSQL. See JSONB path mutations. (134d156,27678db,15a81a7, #54) updateManyanddeleteManyreturn rows. Both returnBatchResult<Payload>({ count, data? }) and accept a scalarselect. PostgreSQL and SQLite return the affected rows throughRETURNING. Empty results omitdata, and MySQL returns only{ count }. SeeupdateManyanddeleteMany. (e4c9b8e,187864f)createManybatchSize. Splits large inserts into one statement per chunk, likeupsertMany.countsums the rows actually inserted (including withskipDuplicates), returned rows keep input order, and hooks fire once per call. SeecreateMany. (32688f4)upsertManyon MySQL. Compiles toON DUPLICATE KEY UPDATE.targetmust be the primary key or one unique key, rows that set (or defaults that fill) another unique key are rejected,whereis rejected, andcountis the number of rows sent. SeeupsertManydetails. (ba58421,5864e7f)upsertManystructuredwhere.whereaccepts a structured filter as well as a DrizzleSQLfragment. It decides whether the conflicting row is updated and does not filter incoming inserts. PostgreSQL and SQLite only. SeeupsertMany. (ba0dfba)
Prepared statements
- Prepared reads. Mark values with
param(name)(exported frombetter-drizzle), call.prepare(name?)on any read, and run it withexecute(values). Plugins andbeforeQueryrun once at prepare time. Intercepts,afterQuery, plugin after hooks, andonErrorrun on each execution. Statements support per-executionmeta,.throw(), andexplain(values). Writes cannot be prepared. See prepared statements. (a2100f6,f212662, #64) - Params in filters and pagination.
param()works as any filter operand and astake,skip,page,perPage,limit,cursor,after, andbefore. Its value type is inferred from where it is used.in/notInparams bind one array and are PostgreSQL-only. Params inside relationinclude/selectargs are not supported. See where params can go. (d63fed4,3fc71cb,58aa710) - Prepared types.
PreparedParam,PreparedParams<typeof statement>,PreparedResult<typeof statement>, andBindable<T>are exported. (d63fed4,a2100f6) - Prepared error codes.
PREPARED_PARAM_MISSING,PREPARED_PARAM_UNKNOWN, andPREPARED_UNSUPPORTED, all with status 400. See errors. (6393092)
Pagination
paginate()takespageandperPage. They are shorthand forskip+limit.pagecannot be combined withskip.$zod(and$ata) pagination schemas accept them too. See offset pagination. (cd10154)- Cursor pagination over nullable keys. Cursor tokens include every
orderByfield, so a nullable sort key works when you repeat the same order and add a unique, non-null tie-breaker. See cursor pagination. (63a924c)
Errors
New BetterDrizzleErrorCode values (see error codes):
| Code | Status | Thrown when |
|---|---|---|
INVALID_ARGS | 400 | operation arguments are invalid (9b9b62b) |
UNIQUE_WHERE_REQUIRED | 400 | a findUnique where does not pin one row (74c6cf7) |
PREPARED_PARAM_MISSING | 400 | execute() misses a value for a param (6393092) |
PREPARED_PARAM_UNKNOWN | 400 | execute() gets a value for an unknown param (6393092) |
PREPARED_UNSUPPORTED | 400 | the query shape or dialect cannot be prepared (6393092) |
ARRAY_QUERY_UNSUPPORTED | 400 | an array filter runs outside PostgreSQL (f5b829c) |
ARRAY_MUTATION_UNSUPPORTED | 400 | an array mutation runs outside PostgreSQL (0e5ab13) |
JSONB_MUTATION_UNSUPPORTED | 400 | a JSONB path mutation runs outside PostgreSQL (134d156) |
PLUGIN_REQUIRED_COLUMN_TYPE | 500 | a plugin's required column has the wrong type (5fcee33) |
CACHE_INVALID_OPTIONS | 500 | the cache plugin gets invalid options (03e2810) |
CACHE_SERIALIZATION_ERROR | 500 | a cached value cannot be serialized or deserialized (03e2810) |
CACHE_STORE_ERROR | 500 | the cache store fails to read, write, or delete (03e2810) |
CACHE_VALUE_TOO_LARGE | 500 | a serialized value exceeds maxSize (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
RawOptionsExtensionsinterface. Raw hooks receive unknown options unchanged inrawOptions. The cache plugin'scache: { invalidate }raw option uses this. See writing plugins. (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/zodaddsZodModelExtensionResolverin this form. See writing plugins. (64af0c4,606b068) PluginModelInfoexposes keys and relations.primaryKeylists the primary key column keys, andrelationsmaps each relation to{ model, kind, foreignKey, through? }. (03e2810)- Typed batch results.
updateManyanddeleteManyreturnBatchResult<PayloadForArgs<...>>instead ofBatchResult<never>. See typing results. (e4c9b8e) - JSDoc matches runtime behavior. Comments that contradicted the runtime were corrected, and examples no longer show the removed
schemaoption. (ce1a297,f78e498)
Plugins
UUIDs
- New
better-drizzle/uuidplugin.uuids()fills UUIDv7 (default) or UUIDv4 ids oncreate,createMany,upsert, andupsertManywhen they areundefined, and the id becomes optional in create input types. Ids are insert-only onupsertManyconflicts.uuidv7()anduuidv4()are exported. See UUIDs. (#71) buuid()columns.better-drizzle/uuid/mysql(binary(16)) andbetter-drizzle/uuid/sqlite(blob) store UUIDs as 16 bytes, typed asstring. Zod and ATA validate them as UUID strings. (#71)- Generated columns for plugin authors. Plugins declare
generated: { column, models?, accepts? }; the column becomes optional in create inputs, is listed inmodel.generated, and is optional in Zod and ATA create schemas in any plugin order. See generated columns. (#71)
Cache (experimental)
- New
better-drizzle/cacheplugin andbetter-drizzle/cache/redisstore. Opt-in read caching with versioned keys. Every read acceptscache(true, a custom key, or{ ttl, negativeTtl, tags, key, vary, refresh, afterHooks }), and models can opt in throughmodels. 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. (d39942f,dba31b1, #60) - Manual invalidation.
client.$cache.invalidate({ models, tags, keys })andclient.$cache.clear(), pluscache: { invalidate }hints on writes and raw calls. See invalidating by hand. (d39942f,03e2810) - Custom stores.
storetakes anyCacheStore. The Redis store is one implementation. See custom stores. (d39942f,eaa1cbd) - Prepared reads are cached by their values. Placeholders hash as named tokens, and the execution values join the key. (
a7cf39f) - Experimental. Options, the
$cacheAPI, the store interface, and the entry format can change in a patch release during0.3.x. See stability. (adc5bb8)
ATA (experimental)
- New
better-drizzle/ataplugin. JSON Schema validation of operation inputs and results with ata-validator, intended as a faster alternative tobetter-drizzle/zod.ata-validator^1.30.1is an optional peer. Schemas are ondb.<table>.$ataas plain objects. They compile on first use, or up front withprecompile: true. See ata. (9041ec1,e236826,077dc0d,ecdb8b4,d9482b0, #51) - Columns JSON Schema cannot describe.
Date,BigInt, andBuffercolumns are checked by residue predicates after schema validation. Array columns and their filters are supported. See ata. (9041ec1,3cb9a0a,ed05f2b) - Relation-aware results. Result envelopes are validated through
afterCreate,afterQuery, andafterUpdate. (322fe81) - Small public surface. The entrypoint exports
ata(default and named), its types, andversion. The registry, column, row,where, and query-schema builders are internal. (0620970) - Experimental. Options, the
$ataAPI, and the generated schemas can change in a patch release. See stability. (1f5e73e)
Zod
- Drizzle 1.x column schemas.
$zodschemas 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. (606b068) - New inputs validated. PostgreSQL array filters and element predicates, atomic update envelopes,
page/perPage, andorderBynulls. (c6b2b24,0e5e9ab,d1916f2,cd10154,b129c9f)
Soft delete
deletedon every filtered operation. Each filtered read and write acceptsdeleted.deleteMany()soft deletes. See the behavior change above and soft delete. (1dad322)
Timestamps
- ISO strings for text columns. See the behavior change above. (
a0b6e6e) - Clock and per-model options.
now?: () => Datesets the clock for every timestamp the plugin writes; it is called once per write, socreatedAtandupdatedAtmatch 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 inmodelsthat the model lacks makesbetter()throwPLUGIN_REQUIRED_COLUMN_MISSING.TimestampModelOptionsis exported frombetter-drizzle/timestamps. See timestamps. (18b8e31)
Rules
merge({ extends, rules })replacesmergeRules. See breaking changes. (a92a7ce)- Raw and
deleteManycoverage. Raw rules check$executeRaw, and hard-delete rules checkdeleteMany. See behavior changes. (86667cb,1dad322) - Docs for every rule. Each rule, preset level, and option is documented. See rules. (
a420e77)
ESLint
Plugin API
- Intercepts.
intercept(ctx)wraps execution after before hooks, transforms, and the clientbeforeQueryhook, so it sees the final args. Plugins compose outermost-first.next()runs the operation,annotate()passes values to after hooks asannotations, andskipAfterHooks()skips them. On prepared executions,ctx.paramsholds the values..explain()never runs intercepts. Intercepts are stable API. See intercepts. (03e2810,ac02e78,eaa1cbd) operationArgsin intercepts. The intercept context carries the same typed pluginoperationArgsas before hooks. See typed operation args. (03e2810)- Raw option extensions. See transactions and raw SQL. (
03e2810) requires.columnschecks. See breaking changes. (5fcee33)
Performance
Published overhead figures are on the benchmarks page.
- One query per cursor page. Populated single-primary-key
cursor()pages derivehasPreviousfrom an inlineEXISTScheck in the data query instead of a second probe. Empty pages and complex shapes keep the exact fallback probe. See benchmarks. (a7b38a5) - Native upsert on unique keys. See writes. (
10179cd) - Plain single-row writes on composite unique keys.
update()anddelete()skip theLIMIT 1subquery whenwherepins every column of a composite unique key. Unique constraints and non-partial unique indexes are precomputed per table. (a306502) - Less work per transaction. Read specs are built lazily, once per delegate and kind, and the
deletespec is cached on first use. Transactions create delegates for every table and no longer pay for operations they never run. (f212662,3809019)
Fixes
- Binary filters. Bare
Buffervalues now work inwhereclauses, including lookups by unique binary keys. BothBufferandUint8Arrayvalues keep the simple read and cursor query paths. (#68) - Relation keys set to
undefinedin writes.create,update, andupsertthrewINVALID_ARGSwhen a relation key was present butundefined, such as optional form data spread intodata. They are now skipped likeundefinedcolumns;nullstill throws. (#69) versionwas stale. Every entrypoint reported0.1.1. All entrypoints now report the package version. (058dd28)- JSONB
in,notIn, andmode. JSONB path filters accepted them in the types, but the compiler ignored them, soinmatched every row. Lists now compile to a type-guardedINper JSON type, andmode: 'insensitive'usesILIKE. See JSONB. (eb0a633) - MySQL single-row writes.
update()anddelete()with a non-uniquewhereread 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) - MySQL
upsertManybuilder.onDuplicateKeyUpdateis called on its insert builder, which it needs asthis. (5864e7f) - Soft delete on text columns. Deletion timestamps are ISO strings for text-backed columns. (
c0ceef0) - Rules limit detection. Removed fallback keys that no current operation passes. (
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,64af0c4) - Security. Resolved CodeQL findings in error message parsing, the ata registry, and the rules plugin. (
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,ccb2d70,ec99046,33178d3, #48) - New pages. null and undefined, arrays, prepared statements, atomic updates, typing results, testing, observability, troubleshooting, recipes, cache, ata, and this changelog. (
fa4b450,2e6c2c8,aa43117,2123019,587b769,2447d2c,ca09a00,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 warning that transforms do not isolate tenants. (
9e752e6,d25e22f,736b898,8edd825,a420e77,d1562c4,95bfede,eaa1cbd,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,9e7b373,4ee204b,a623279,1ef96c1) - Benchmarks.
bench:reportsamples both sides in one window and publishes ratios. The JSONB suite runs again and checks plan parity. The raw cursor scenario computeshasPreviousfor real. New suites cover arrays, JSONB mutations, atomic updates, cache workloads (bench:cache), validation plugins, and prepared reads. See parity. (ed71127,a2e468a,d58c894,c133b4d,84583f7,27678db,f29295a,cb411d8,9599064,1715289,3e5d92c) - CI. A database job runs the PostgreSQL, MySQL, and Redis suites (
test:databases) against service containers. (700d5d2) - Release workflow. Publishing waits for the full CI workflow, and the already-published check reads the package name from
package.json. (2232352) - Engines.
engines.nodeis>=18, matching the build target. (9865aa5) - Package metadata. New
./ata,./cache, and./cache/redisexports, and npm keywords. (dba31b1,a623279) - Skill pack.
skills/better-drizzlewas rewritten for app developers, with querying, writing, plugins, troubleshooting, security, and contributing references, and now covers$where(), prepared reads, andata. See AI. (8974e21,1f5e73e) - Local development. Docker Compose adds a Redis service, and
.env.exampledocuments each key. (c9b57aa,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.
All 174 commits since v0.2.0
ad48e9edocs: release 0.3.0967043fstyle(web): remove the duplicate search from the docs sidebar02bbce5feat(web): redesign the landing page and logod5c1b7adocs: add the 0.3.0 changelog18b8e31feat(timestamps): add a clock option and per-model overridesa0808d3feat(query): apply insensitive mode to equality and list filters32688f4feat(createMany): support batchSize9b9b62bfeat(errors)!: report invalid arguments as INVALID_ARGSc0013aerefactor(types)!: reject lock inside nested relation args9865aa5build: declare the supported Node.js range1f5e73edocs: mark ata experimental and mention $where in the skill pack48df501fix(types): reject unknown updateEach options10179cdperf(upsert): use native conflict handling for unique keysba1d106feat(delegate): treat { equals } filters as pinned keys187864fdocs: document batch mutation results and align benchmark paritye4c9b8efeat: return affected rows from updateMany and deleteManyba0dfbafeat: support structured conflict filters in upsertManyfef4015fix: reject unknown select keys before executing queries784ac15fix: escape literal wildcards in contains and startsWith filterseaa1cbddocs: complete 0.3.0 upgrade notes and document cache stores0620970refactor(ata)!: stop exporting internal helpers3809019perf(delegate): build the delete operation spec on first use74c6cf7feat(delegate)!: require a unique where in findUnique7e81d12fix(delegate): lock MySQL single-row writes in a transactiona306502perf(delegate): detect composite unique keys for single-row writes5d1843cfix(zod,ata): reject string cursors in cursor args schemas9345101docs(agents): record single-row writes and insensitive filter dialectsce1a297refactor(types)!: tighten write results, cursor tokens, and JSDoc191dbb3fix(delegate): limit update and delete to a single row25be62bfix(query): support insensitive string filters on SQLite and MySQL2232352ci(release): gate publishing on CI and fix the published check109e459docs(agents): record prepared statement internalsaa43117docs(prepared): document prepared statements3e5d92cperf(bench): compare prepared reads with Drizzle prepared statementsd07d9dftest(prepared): cover prepared reads on SQLite, PostgreSQL, and MySQLa7cf39ffeat(cache): key prepared reads by their valuesf212662feat(delegate): add .prepare() to read resultsac02e78feat(plugins): pass prepared values to interceptsa2100f6feat(prepared): compile reads into Drizzle prepared statements58aa710refactor(operations): split cursor pages into build and finish steps3fc71cbfeat(query): bind prepared params in the where compilerd63fed4feat(types): accept param() in filters and pagination options6393092feat(errors): add prepared statement error codes309278afeat: add $where() method for compiling typed conditions in raw Drizzle queries700d5d2ci: run the PostgreSQL, MySQL, and Redis suitesadc5bb8docs(cache): mark the cache plugin as experimentald635403docs(multi-tenancy): warn that transforms do not isolate tenants95bfededocs(upgrading): cover 0.3.0 breaking and behavior changes21cfd3efix(deps): cap the drizzle-orm peer below 1.0.0-rc.5dc85b94docs(cache): clarify invalidation guarantees and performance costscb411d8perf(bench): add cache workloads with result parity checksf3f180ctest(cache): cover invalidation races and query identity regressionsd39942ffeat(cache): add versioned read caching and Redis storage3271b63fix(client): finish success callbacks despite observer failures03e2810feat(plugins): add operation intercepts and raw option extensionsca09a00docs(cache): document the cache plugin and plugin intercepts143d1b0test(cache): cover cache plugin, Redis store, and plugin interceptsdba31b1build(cache): register cache and cache/redis subpathsc9b57aachore(dev): add Redis service to docker composed26beb8style: format merged and numeric-mode tests5864e7ffix(upsertMany): guard the MySQL builder method typea8cabaedocs(skill): drop reference to removed type test files37c6215docs: document soft delete coverage, page inputs, and MySQL upsertManyba58421feat(upsertMany): support MySQL with ON DUPLICATE KEY UPDATEcd10154feat(paginate): accept page and perPage1dad322feat(soft-delete): filter every read and write with a wherec8a8790docs(skill): drop resolved known issues01eeb27fix(zod,ata): type numeric number mode as a number2fddee2fix: unwrap Drizzle 1.x errors and keep library error codesfc2aa00chore(types): remove inline type test files861551fdocs: align AI page and query options with the skill rewrite8974e21docs(skill): rewrite skill pack for app developers1d8e642chore(docker): remove unused Postgres init schema9effb9cchore(env): document each .env.example key1d90dc8docs: correct JSONB shorthand and junction caveats58f6da2test: cover Drizzle 1.x migration failure modesf78e498docs(types): drop removed schema option from JSDoc examplesd1562c4docs: expand Drizzle 1.x upgrade guide and fix stale pagesf56a6d8docs(agent): update migration guidance and audit notes2b4736edocs: explain the Drizzle 1.x API and upgrade path89a98daperf(bench): migrate parity suites and stabilize array samplesa0b6e6efix(plugins): align timestamps and guardrails with Drizzle 1.xed05f2bfix(ata): validate Drizzle 1.x residues and overrides606b068fix(zod): support Drizzle 1.x column schemas64af0c4fix(types): preserve delegate inference with Drizzle 1.x9614cdcfix(core): restore query parity across Drizzle 1.x dialects8b09683feat(core): bind Drizzle 1.x relation metadata15a81a7fix(jsonb): typecheck upsertMany paths and document mutations27678dbfix(jsonb): harden path mutations and add parity coverage63a924cfix(orderBy): handle nullable cursor ordering134d156feat(jsonb): add typed JSONB path mutations via jsonb_setb129c9ffeat(orderBy): add null directions0000afefeat(jsonb): add dotted path filter shorthand57f2b19fix(security): resolve codeql findings2123019docs: document atomic updatesf29295abench(writes): measure atomic update paritycd69e4ftest(core): cover atomic write operationsd1916f2feat(validation): support atomic update envelopes6b3c4c1feat(hooks): expose compiled atomic expressions6c87e8afeat(core): compile atomic scalar mutations3194d6dfeat(types): add atomic scalar update inputs03bab62docs(ata): align navigation icon with zod322fe81fix(ata): validate relation-aware operations1715289test(ata): benchmark validation as a share of a real operation3cb9a0afeat(ata): array columns and their filters9599064test(ata): benchmark the two validation plugins on one tabled9482b0chore(ata): require the ata-validator release the where clause needs077dc0dfeat(ata): the plugin, its registry and the operation schemase236826feat(ata): where clause schemaecdb8b4chore(ata): ata-validator as an optional peer, like zod9041ec1feat(ata): column mapping and row validatione90c0f8docs(agents): document array predicate compilation2447d2cdocs(guides): add common query recipes2769b85docs(arrays): explain element predicate semanticse716815perf(arrays): benchmark element predicate parity254561ctest(arrays): cover element predicate edge cases0e5e9abfeat(zod): validate array element predicates77bfb7bfeat(query): compile array element predicates598d33ffeat(types): expose array element predicates587b769docs(guides): add typing, testing, observability, and troubleshooting guidesfa4b450docs(querying): add a null and undefined pagebcf7a2ddocs(arrays): document GIN indexing and helper usagea2e5b56chore: note array filter constraints in AGENTS.md0e5ab13feat(arrays): refine PostgreSQL array filters, zod schemas, and types7d9de7cdocs(arrays): improve raw SQL examplesf196cc3docs(agents): record array filter constraints2e6c2c8docs(arrays): document PostgreSQL array filters84583f7perf(arrays): add PostgreSQL parity benchmarkdbbfb77test(arrays): cover typed PostgreSQL array filtersc6b2b24feat(zod): validate PostgreSQL array filtersf5b829cfeat(arrays): compile PostgreSQL array predicates17b91a7feat(arrays): add typed array filtersb20db3fperf(web): use cnfast9705f39perf(web): skip twoslash hovers on object keys and Drizzle tabs53f0406style(web): restore oxfmt formatting5001150chore(deps): sync lockfile with twoslash dependencies5a214f8feat(doc): add twoslash support736b898docs(guides): expand frameworks, extensions, repositories, parity, and limitationsd25e22fdocs(plugins): document hooks, eslint rule ids, and plugin options9e752e6docs(reference): complete errors, model API, query options, and client pages5fcee33feat(plugins): honor optional and type in requires.columns86667cbfix(rules): check $executeRaw in raw rules and drop unused limit keysfd84f34fix(types): accept only cursor objects in cursor() after/before337cf06fix(cursor): page by primary key ascending when orderBy is omitted35d4137fix(types): type create() as nullable when skipDuplicates is set834a2b7fix(web): theme-aware sponsors, light logo, and mobile grid overflow90dfa9estyle: replace em dashes with plain hyphensdcd5c96chore: note sidebar and example conventions in AGENTS.mdec99046docs: destructure results in pagination, write, and explain examplesccb2d70docs: replace the comparison page with Why better-drizzle?f9286b3docs: reorder the sidebar into a basic-to-advanced learning path4ee204bfeat(web): slim docs header that hides on scroll down97f4be7fix(web): highlight docs code with the oniguruma enginea420e77docs(rules): document every rule, preset level, and optiona92a7cefeat(rules)!: replace mergeRules with merge({ extends, rules })0bd18b5feat(web): highlight text selection with the brand color0f8e9a6docs(README): fix broken links and make it betteree941b3chore(deps): sync lockfile optional peer metadataa2e468adocs: republish benchmark numbers from a single measured runed71127feat(benchmark): add an interleaved overhead reportd58c894fix(benchmark): repair the JSONB suite and assert plan parityc133b4dfix(benchmark): compute hasPrevious on the raw cursor sidea7b38a5perf(cursor): resolve navigation metadata in one indexed queryce4326achore(oxlint): fix error ""core.plugins is possibly undefined""a623279feat(web): improve SEO metadata across the docs site1ef96c1feat(web): add robots.txt and sitemapc0ceef0fix(soft-delete): serialize text deletion timestampsb4f3652docs(agents): document docs code block conventions33178d3docs(web): add raw Drizzle comparison tabs to API examples8edd825docs(web): expand JSONB filters guide with examples and semantics9e7b373feat(web): copy docs code blocks on click with a shine effecteb0a633feat(query): support in, notIn and insensitive mode on JSONB paths23527aedocs(web): update documentation site for v0.2.0058dd28fix(version): align version constants with 0.2.0
Full changelog: https://github.com/almeidazs/better-drizzle/compare/v0.2.0...v0.3.0
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.
Breaking change for plugin imports. Follow Upgrading to 0.2 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, andbetter-drizzle/eslint. The scoped@better-drizzle/*packages are discontinued.better-drizzle/pluginsis the new plugin authoring entry point. Every entry point ships ESM, CommonJS, and declarations. (0858c46,a066b56)
Features
- Relation reads. Nested
includeandselectwithwhere,orderBy,take,skip, andcursorat any depth, loaded by a batched loader (one query per relation node). (42184d3) - Relation writes.
connecton create, andconnect,disconnect, andseton update, in an implicit transaction. (42184d3,64964a5) - Many-to-many. Simple junction tables are inferred. (
5942400) include._count. Relation totals as correlated subqueries, without extra round trips. (997f471)cursor()pagination.before/aftercursors withhasNextandhasPrevious;paginate()stays offset-based. (37b6c8d)- Row locks.
lockon reads for PostgreSQL and MySQL, withskipLocked,noWait, andlocks.transactionsOnly. (b64c100) .explain(). Every read returns a lazy thenable with a cross-dialect.explain(); relation stages are reported as deferred. (5c451f9,ce0e3e4)extends(). Client-level helpers that survive$withContext()and transaction clients. (290e1cb)- PostgreSQL JSONB filters. Typed path filters on JSONB columns. (
7dd1cf9) better-drizzle/rules. Runtime guardrails (for example, writes withoutwhere). (c7ae257)better-drizzle/eslint. The statically checkable rules as an ESLint plugin. (2c0fe7b)better-drizzle/zod. Generated Zod schemas ondb.<table>.$zodand opt-out runtime validation. (28822b0)- Agent skill. A first-party skill pack for AI coding agents. (
cca8f2e)
Fixes
- To-many relation filters correlate on the parent row. (#27)
- Relation filters under
includereference the aliased parent table. (421d320) - The zod plugin keeps relation commands during validation. (
07b0e34)
Tooling
- Builds move to tsdown; linting and formatting move to Ultracite (oxlint and oxfmt). (
0c6216f,369d7b7) - A real-database integration suite and a full parity benchmark suite. (
0a433ac,c705d68)
Contributors
- @almeidazs
- @pablofdezr made their first contribution in #27
Full changelog: https://github.com/almeidazs/better-drizzle/compare/better-drizzle@0.1.1...v0.2.0
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 oneUPDATE ... SET column = CASE ... ENDstatement. Takesby,data,update, an optionalwhere, optional scalarselect, andonEmpty, and rejects duplicatebyvalues. (#12)$withContext(meta). Returns a client that merges defaultmetainto every operation, raw SQL call, and transaction lifecycle payload. (68f3f9a)
Packaging
engines.nodeis>=18. (#13)- The package is built with
tsc, and thedrizzle-ormpeer range starts at0.30.0. (bb76ca6)
Documentation
Full changelog: https://github.com/almeidazs/better-drizzle/commits/better-drizzle@0.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, andrepository(name)lookup. - Duplicate handling.
createandcreateManyacceptskipDuplicates(trueor a list of columns). (#10) upsertMany. Native-first batch upsert with conflict targets, update strategies, chunking, and optionalselect. It fails fast on unsupported dialects instead of falling back to per-row loops. (#11)- Transactions. Nested savepoints, retries, explicit rollback,
afterCommit, andafterRollback. - Raw SQL. Safe
$rawand$executeRawwith tagged templates, plus$rawUnsafebehind an explicit option. - Plugins. Typed
operationArgs, transforms, hooks, client extensions, and model extensions. (#1) - Errors.
BetterDrizzleErrorwithcode,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, with a support matrix. (#9)
Contributors
- @almeidazs
- @leoisadev1 made their first contribution in #9