Changelog
Every change in better-drizzle 0.3.0 compared with 0.2.0, with links to the docs and commits.
0.3.0 - Unreleased
Upcoming release
0.3.0 is not published yet and has no release date. This page compares
the current main branch with v0.2.0 and can change before the release.
For step-by-step migration instructions, see the
upgrading guide.
0.3.0 moves better-drizzle to Drizzle ORM 1.x (relational queries v2). It also adds prepared statements, two experimental plugins (cache and ata), atomic updates, PostgreSQL array and JSONB path support, and stricter types and runtime checks.
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
$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
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
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)
Contributors
Thanks to @mertcanaltin (#51), @flaxodotdev (#54), and @joaotonaco (#63).