Changes in 0.3
Runtime behavior changes and new features in better-drizzle 0.3 that the typecheck does not catch.
Read this after the 0.3 upgrade steps. These changes compile unchanged but can change what your code observes at runtime.
Behavior changes
These do not show up in the typecheck, but they can change what your code observes at runtime:
-
softDelete()filters more operations. In0.2.xonlyfindMany,findFirst,count, andexistshid deleted rows. NowfindUnique,findOne,paginate,cursor,update,updateMany,updateEach,delete, anddeleteManydo too. Anupdate({ where: { id } })that hits a soft-deleted row now matches nothing; passdeleted: 'with'to keep the old behavior.deleteMany()is now a soft delete instead of a physical one, and returns the count of rows it marked. Writes with an emptywherestay no-ops, andmode: 'hard'still matches deleted rows. TherequireTenantOnDelete,noHardDeleteOnSoftDeleteModel, andrequireHardDeleteReasonrules now also checkdeleteMany. See soft delete. -
Reads are lazy.
findMany,findFirst,findOne,findUnique,count,exists,paginate, andcursorreturn lazy thenables, like Drizzle's own query builders. The query and its hooks run when the result is awaited (or passed tothen,catch,finally, orPromise.all), and at most once. A read that is created but never awaited no longer runs..explain()alone never runs the read. Bun'sexpect(...).resolves/.rejectsonly accept native promises, so wrap reads withPromise.resolve(...)there. See testing. -
.explain()output. Cursor probes that depend on a row from the data query are reported underdeferredProbesinstead of being executed, and a statement that only runs under a condition carries aconditionstring. See explain. -
Text-backed timestamps.
timestamps()andsoftDelete()write ISO 8601 strings to columns whosedataTypeis a string (such as SQLitetextcolumns) andDateobjects to native date columns. -
Failures after
COMMIT. When anafterCommitcallback or anafterTransactionCommithook throws, better-drizzle no longer runs rollback handling. There is noROLLBACKon SQLite and noafterRollbackoronTransactionErrorhook. The original error is rethrown as-is, and the data stays committed. -
mode: 'insensitive'on equality.equals,in,notIn, and a scalarnotused to ignoremodeand stayed case-sensitive. They now comparelower(column)withlower(value)on every dialect, including JSONB paths and array element predicates. Aparam()list inin/notInstays PostgreSQL-only. -
MySQL
upsert. The nativeON DUPLICATE KEY UPDATEpath is used only when the table declares no unique key besides the primary key. MySQL fires that clause on any unique key, so other tables use a read-then-write path instead of possibly updating a different row. -
Native
upserton unique keys. Awherethat holds only the columns of one unique key, with the same values increate, now runs oneON CONFLICT ... DO UPDATEstatement on PostgreSQL and SQLite (andON DUPLICATE KEY UPDATEon MySQL when that key is the only one that can match). Awherewithout the primary key no longer takes the primary key path whencreateomits it too: it used to insert without checkingwhere, and now reads first. -
update()anddelete()change at most one row. In0.2.xthey ran theirwhereagainst every matching row and returned only the first. Now only one row changes, on every dialect: awherethat pins the primary key or a unique key runs as a plain statement, and any otherwhereis limited to one row (aLIMIT 1subquery on PostgreSQL and SQLite; on MySQL, aSELECT ... FOR UPDATEand a write by primary key in the active or an implicit transaction). Code that relied onupdate/deletetouching several rows must useupdateMany/deleteMany. Theupsertread-then-write path also updates the row it found. See update and delete. -
cursor()withoutorderBypages by primary key ascending. Before, the default direction depended on the call (descending, or ascending withbefore), so the first page and later pages could walk different orders. Pass an explicitorderByto keep a descending order. See cursor pagination. -
Zod and ATA cursor schemas reject string cursors.
$zodand$atacursor args schemas accept only the cursor object ornull, matching the types, so a stringafter/beforefails validation instead of reaching the runtime. -
Zod and ATA type numeric "number mode" columns as
number.numeric(..., { mode: 'number' })anddecimal(..., { mode: 'number' })columns validate as numbers, not strings. Columns in the default string mode still validate as strings. See zod and ata. -
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); PostgreSQL keepsILIKE. See string filters. -
Raw SQL rules also check
$executeRaw.requireRawComment,requireRawTimeout,noRawMutation, andnoRawWithoutTransactionused to check only$raw. Existing$executeRawcalls can now report violations. See rules. -
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. Checkcatchblocks that matchedOPERATION_ERRORfor those cases. See errors. -
Invalid arguments throw
INVALID_ARGS. Input validation failures, such as unknown columns or relations,selectwithinclude,pagewithskip,beforewithafter, an invalidbatchSize, duplicateupdateEachbyvalues, malformed atomic, array, or JSONB mutations, relation command errors, and Zod plugin parse failures, usedOPERATION_ERROR(status 500). They now useINVALID_ARGS(status 400). Updatecatchblocks that matchedOPERATION_ERRORfor them. See errors. -
Success callbacks all run. Every
afterCommitcallback, everyafterTransactionCommithook, and every pluginafterRawhook now runs even when an earlier one throws; the first error is still rethrown. Previously the first failure skipped the rest. -
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 (aparam()counts). Any otherwhere, including one where anundefinedvalue dropped the key, such as{ id: undefined }, throwsUNIQUE_WHERE_REQUIREDbefore any SQL. UsefindFirstfor lookups by non-unique columns.
New in 0.3.0
- Prepared statements: mark values
with
param(name), call.prepare(name?)onfindUnique,findFirst,findOne,findMany,count,exists,paginate, orcursor, and run it withexecute(values). Param and result types come from the read (PreparedParams/PreparedResult). Plugins andbeforeQueryrun once at prepare time; result hooks and intercepts run on every execution. better-drizzle/cache(experimental): versioned read caching with automatic invalidation and a Redis store atbetter-drizzle/cache/redis.better-drizzle/ata(experimental): JSON Schema validation of operation inputs and results, built on ata-validator and intended as a faster alternative tobetter-drizzle/zod. The entrypoint exportsata(default and named), its types, andversion; the registry, column, row,where, and query-schema builders it uses internally are not exported.$where(): every delegate compiles a typedwhereinto a DrizzleSQLcondition, so the same filter works in rawdb.select()queries, joins, and subqueries.- Atomic updates:
increment,decrement,multiply,divide, andseton number columns, andtoggleon boolean columns. - PostgreSQL arrays: typed array filters,
some/every/noneelement predicates, andappend,prepend,remove,replace, andaddUniquemutations. - JSONB paths: dotted path filters such as
{ metadata: { 'profile.age': { gte: 18 } } },in/notInon string and number paths,mode: 'insensitive'on string paths (examples), and path mutations inupdate,updateMany,updateEach,upsert, andupsertManythat compile tojsonb_set(...). NULLordering:orderByaccepts{ direction, nulls: 'first' | 'last' }. See reads.paginate()acceptspageandperPage.createManybatchSize: splits large inserts into one statement per chunk, likeupsertMany. SeecreateMany.upsertManyon MySQL throughON DUPLICATE KEY UPDATE. SeeupsertManydetails.- Plugin intercepts:
intercept(ctx)wraps execution after transforms, so a plugin can serve a result without SQL or annotate after hooks. - Timestamps
nowandmodelsoptions: an injectable clock for deterministic tests, and per-model column names orfalseto turn the plugin off for a model.