All notable changes to this project will be documented in this file.
3.1.0 (2026-10-03)
API compatible with node:sqlite from Node.js v26.10.0, plus createModule() and the changes listed under 3.0.0 that are on Node.js v26.x-staging but not yet in a release. SQLite is unchanged at 3.53.4.
createModule(): virtual tables whose rows come from a JavaScript iterable, viaDatabase.prototype.createModule(name, { columns, rows, directOnly, useBigIntArguments }), wrappingsqlite3_create_module_v2(). Ports Node.js PR #65787, PR #66195, and PR #66215, which are onv26.x-stagingbut not yet in a Node.js release.
process.exit()in a worker duringexec(): a user-defined function that calledprocess.exit()whileexec()ran in a worker aborted the process withFATAL ERROR: Error::Error napi_define_properties. The worker now exits with the requested code.- Expanded SQL longer than V8's maximum string length: with a
sqlite.db.querydiagnostics subscriber, running a statement whose expanded SQL exceeded V8's maximum string length (for example, one with a bound blob over about 256 MB, which expands to hex) threwError: Unknown failurefrom a C++ exception that unwound through SQLite, and leaked the expanded SQL. The statement now succeeds and publishes no event. ReadingexpandedSQLon such a statement threwError: Unknown failurewith nocode; it now throwsERR_STRING_TOO_LONG, as oversized TEXT values do since 3.0.0.node:sqliteaborts the process when readingexpandedSQLon such a statement (checked on v24.21.0). - C++ exceptions in aggregate callbacks: a node-addon-api failure inside an aggregate's
step,inverse, or final callback unwound through SQLite. Asteporinverseresult whosethengetter threw madeexec()leaveclose()failing withERR_INVALID_STATE(database cannot be closed while in a callback), andget()/all()threw an unrelated error instead of the getter's. Exiting the process or a worker while a window aggregate with aUint8Arrayaccumulator was mid-query aborted withterminate called after throwing an instance of 'Napi::Error'. Function and aggregate callbacks now catch these exceptions; the statement throws the original error, and exit proceeds normally.
3.0.0 (2026-10-02)
API compatible with node:sqlite from Node.js v26.10.0, plus the Database and Statement class rename from Node.js PR #65988, which landed on v26.x-staging, and two fixes from Node.js main (Node.js PR #66042, Node.js PR #66209). None of these is in a Node.js release yet. Database.prototype.createModule(), also only on v26.x-staging so far, is not ported yet. The rename changes the classes' name values, so this is a major release. SQLite is unchanged at 3.53.4.
DatabaseandStatement: the new names ofDatabaseSyncandStatementSyncfrom Node.js PR #65988. The old names are still exported and are the same classes (DatabaseSync === Database,StatementSync === Statement); Node.js deprecates them in documentation only (DEP0210, DEP0211). The typesDatabaseInstance,DatabaseOptions,DatabaseLimits, andStatementInstanceare aliases ofDatabaseSyncInstance,DatabaseSyncOptions,DatabaseSyncLimits, andStatementSyncInstance.
- BREAKING: Class names follow the rename:
DatabaseSync.nameand a database'sconstructor.nameare now"Database",StatementSync.nameand a statement'sconstructor.nameare"Statement", and the iterator fromiterate()is aStatementIterator(wasStatementSyncIterator), as innode:sqlitefrom Node.js PR #65988. In 2.6.0 the exported constructors were named"DatabaseSync2"and"StatementSync2", because the bundler renamed them, while instances reported"DatabaseSync"and"StatementSync". Code that compares these names against the old strings must change;instanceof DatabaseSyncandinstanceof StatementSyncstill work. open()afterclose()restores the connection's settings: asetAuthorizer()callback was not reinstalled, so afterclose()andopen()a deny-all authorizer stopped denying anything; limits set throughdb.limitswent back to the constructor's values; and extension loading turned on withenableLoadExtension(true)was off again. All three now carry over, as innode:sqlitefrom Node.js PR #66042.- TEXT longer than V8's maximum string length throws
ERR_STRING_TOO_LONG: reading such a value withget(),all(), oriterate(), passing one to a user-defined or aggregate function, or reading one throughDatabasePoolthrewError: Unknown failurewith nocode. In a user-defined or aggregate function the error was a C++ exception that unwound through SQLite, after whichclose()always failed withERR_INVALID_STATE(database cannot be closed while in a callback). It now throwsERR_STRING_TOO_LONG, as innode:sqlitefrom Node.js PR #66209, and the connection stays usable. - Experimental
DatabasePoolkeeps its name: the bundler renamed the class, soDatabasePool.namewas"_DatabasePool"andutil.inspect()printed a pool as_DatabasePool {}. The build now keeps the source names of all exported classes and functions. backup()runs one step per threadpool job: eachsqlite3_backup_step()now returns to the main thread before the next one is queued, as innode:sqlite. Theprogresscallback is therefore called after every step that leaves pages remaining; previously calls could be coalesced. Smallratevalues cost more: on tmpfs, a 128 MB backup took 580–690 ms atrate: 1(was 195–210 ms;node:sqlite580–670 ms) and 132–150 ms at the defaultrate: 100(was 124–132 ms).- Strings with NUL bytes are no longer truncated: binding
"a\0b"stored"a", and user-defined and aggregate functions received only the text before the first NUL byte of a TEXT argument. Both now use the full string, asnode:sqliteandDatabasePoolalready did. Queries that bound such strings now store and match different values. - INT64_MIN passed to a function throws
ERR_OUT_OF_RANGE: a user-defined or aggregate function withoutuseBigIntArgumentsreceived -9223372036854775808 as the imprecise Number -9223372036854776000, because the range check usedstd::abs(), which is undefined for that value. It now throws like every other integer outside the safe range, as innode:sqlite. - Aggregate values that do not fit the stored state throw
ERR_OUT_OF_RANGE: between steps, an aggregate's accumulator is stored in a 4096-byte buffer, and a BigInt as int64. A string or Buffer over 4095 bytes used to be truncated, an object or array whose JSON reached 4095 bytes was replaced with{"_truncated":true}, and a BigInt outside the int64 range wrapped (2n ** 64n + 5nbecame5n), all without an error. Astartorstepvalue that does not fit now throws, andaggregate()throws for such a BigIntstart.node:sqlitekeeps the JavaScript value itself and has neither limit. - An unparsable URL
hrefthrowsERR_INVALID_URL:new Database({ href: "not a url" })andbackup(db, { href: "not a url" })threwERR_INVALID_URL_SCHEME; they now throwERR_INVALID_URL, asnode:sqlitedoes. Ports Node.js PR #66026.
- SQLite errors swallowed after a throwing function in
exec(): when a user-defined function threw insideexec(), the next SQLite error on that connection was ignored, so for example anINSERTthat violated a primary key returnedundefinedinstead of throwing. Only an error with a JavaScript exception actually pending is now ignored, as innode:sqlitefrom Node.js PR #66209. - Statements starved during
backup(): every backup step holds the source connection's mutex, and the whole backup ran as one threadpool loop, so a synchronous statement on the sameDatabasewaited for most of the backup (204 ms of a 208 ms backup of a 128 MB WAL database). It now waits for at most one step (under 1 ms atrate: 100). - Worker terminated during
backup(): terminating a worker thread while it ran a backup with aprogresscallback aborted the process (terminate called after throwing an instance of 'Napi::Error'). The backup now stops at the next step without settling its promise, and the worker exits. process.exit()in a worker's backup progress callback: aborted the process withFATAL ERROR: Error::Error napi_define_properties, because node-addon-api's conversion of the termination exception into aNapi::Erroris fatal when JavaScript can no longer run. The worker now exits with the requested code. Rejection messages for a throwingprogresscallback are unchanged.close()duringbackup(): closing the source database freed the SQLite backup handle while a backup step was using it on a worker thread, and before the first step it freed the source connection that step was about to attach to (heap use-after-free, confirmed with AddressSanitizer; present in 2.6.0).close()now waits for a running step to finish, and a step that has not started does nothing. The promise rejects withERR_SQLITE_ERROR: errcode 1 (SQL logic error), or errcode 5 (database is locked) if the backup was waiting on a lock whenclose()was called.- Iterator used after its statement was collected:
iterate()returned an iterator that did not keep its statement alive, so an iterator over a statement the caller no longer referenced (for exampleReadable.from(db.prepare(sql).iterate())) could segfault or return rows from a different statement once the statement was garbage-collected. The iterator now holds its statement, as innode:sqlite. expand()wrote columns ontoObject.prototype: withenhance(), a query whose columns came from a table named__proto__(directly or through a view) set those columns onObject.prototypefor the whole process, and a table namedconstructorset them onObject. Anyone who controls the schema of a database the application reads with.expand()could set properties on every object. Such tables now appear as ordinary own properties of the row, and a column named__proto__keeps its value instead of being dropped.- Aggregate errors lost with an object or Buffer accumulator: when a
stepfunction threw, or an argument could not be converted, in an aggregate whose accumulator was an object, array, or Buffer,get()returnedundefinedinstead of throwing. SQLite finalizes the aggregate after the failed step, and rebuilding the accumulator there cleared the pending error. The error now reaches the caller. backup()spun a CPU core while waiting on a lock: while another connection held a lock on the source or destination, each step that returnedSQLITE_BUSYorSQLITE_LOCKEDqueued the next one at once, so a waiting backup kept one core busy until the lock was released. Retries now wait 1 ms, doubling up to 100 ms, on a libuv timer, so fake timers that replacesetTimeoutdo not affect them.backup()resolved with 0 pages: if its first step found a lock, the backup copied every page but resolved with 0, because the page count was read only after that first step.DataViewaggregate accumulator: an aggregate whosestartorstepvalue was aDataViewfailed withInvalid argument, thrown as a C++ exception through SQLite's C code, which can crash on musl. Its bytes are now kept, and the next step receives them as aUint8Array, as for aBuffer.DatabasePoolerror without a code: a named parameter whose key contained a NUL byte was rejected with aTypeErrorthat had nocode; it now hasERR_INVALID_ARG_TYPE, like the pool's other argument errors. An unknown named parameter, including a bare key such as"t\0x"next to a"t"key, was rejected with anErrorthat had nocode; it now hasERR_INVALID_STATE, as inDatabaseandnode:sqlite.Unknown named parametermessage cut off at a NUL: for a key such as"tenant\0x", theDatabaseerror message ended at the NUL (Unknown named parameter 'tenant); it now contains the whole key.SECURITY.mdread-only example opened read-write: it passedreadonly: true, whichDatabaseignores; it now passesreadOnly: true. Its extension example calleddb.allowExtension(), which does not exist, instead of passingallowExtension: trueto the constructor.defensivedocumented as off by default: the type docs and API reference saiddefensivedefaults tofalse. It defaults totrue; only the docs changed.- NUL in named-parameter keys documented: SQLite matches a parameter name only up to a NUL, so a key such as
":tenant\0x"never binds its own value here, whilenode:sqlitebinds it and can replace the":tenant"binding. TheStatementandDatabasePooltype docs give the details. If parameter objects are built from untrusted keys, reject keys that contain NUL. memory:checkhung with clang's ASan runtime (developer tooling): the sanitizer harness preloadedlibclang_rt.ubsan_standalonenext to clang's ASan runtime, and with clang 21 node hung at startup, before any test ran. clang's ASan runtime already defines UBSan's*_aborthandlers, so a separate UBSan runtime is now preloaded only with GCC's libasan.
2.6.0 (2026-09-17)
API compatible with node:sqlite from Node.js v26.9.0, plus the changes below that landed on v26.x-staging but are not yet in a Node.js release. Binding undefined now succeeds where it previously threw, so this is a minor release. SQLite is unchanged at 3.53.4.
undefinedbinds as SQL NULL: passingundefinedfor a parameter binds NULL instead of throwingERR_INVALID_ARG_TYPE, so an explicitundefinedand an omitted named parameter now agree.undefinedis not an object, so it is still bound as an anonymous parameter rather than being treated as the named-parameter bag. This also removes a better-sqlite3 migration gotcha that surfaced with query builders like Knex, which emitundefinedfor columns missing from a multi-row insert. Ports Node.js PR #65709.
- Changeset detached by a user-defined SQL function:
applyChangeset()copies every non-empty changeset before applying it, not just when afilteroronConflictcallback is supplied. SQLite reaches JavaScript through a user-defined SQL function in aCHECKconstraint or trigger too, so a changeset could previously be detached or zeroed while SQLite was still reading it. Ports Node.js PR #65870.
2.5.0 (2026-09-08)
API compatible with node:sqlite from Node.js v26.8.1, plus four changes landed on v26.x-staging but not yet in a Node.js release. Two of them make calls throw that previously succeeded, so this is a minor release. SQLite is unchanged at 3.53.4.
- Closing a session from a callback now throws:
session.close()andsession[Symbol.dispose]()throwERR_INVALID_STATE(session cannot be closed while in a callback) when called from any SQLite callback on the same connection: an authorizer, a user-defined function, or asqlite.db.querydiagnostics subscriber. Previously an idle session could be closed from an authorizer or user-defined function. SQLite's session module runsPRAGMA table_xinfofrom inside its pre-update hook while it is still walking the connection's session list, so deleting a session there is a use-after-free, and Node cannot tell which callbacks run inside that hook. Disposing an already-closed session from a callback remains a no-op. Ports Node.js PR #65454. - Non-integer callback
lengthnow throws:function()andaggregate()throwERR_INVALID_ARG_TYPEwhenfn.length,options.step.length, oroptions.inverse.lengthhas been redefined to a non-integer. Previously a non-number was silently treated as varargs. Ports Node.js PR #65595.
- Options getters that close the database: a property getter on an options bag runs arbitrary JavaScript in the middle of the call, so
prepare(),function(),aggregate(),deserialize(),createSession(),applyChangeset(), andbackup()now re-check that the database is still open after reading their options (and, forfunction()andaggregate(), the callback'slength) and throwERR_INVALID_STATEif a getter closed it. PreviouslyapplyChangeset()segfaulted,createSession()returned a session on a closed connection, and the others surfaced SQLite misuse errors. Ports Node.js PR #65595. deserialize()buffer resized from an options getter: if the getter shrinks or detaches thebufferargument's backing store,deserialize()throwsERR_INVALID_STATEinstead of copying past the end of it (previously a segfault). Same upstream PR.- Changeset detached mid-apply: when a
filteroronConflictcallback is supplied,applyChangeset()copies the changeset before applying it, so a callback that detaches or overwrites the input buffer can no longer hand SQLite freed or zeroed memory. Changesets over 2 GiB are rejected withERR_OUT_OF_RANGE. Ports Node.js PR #65286. - Double free when
function()oraggregate()registration fails: whensqlite3_create_function_v2()orsqlite3_create_window_function()rejects a registration (for example a callback whoselengthexceedsSQLITE_MAX_FUNCTION_ARG, 1000 in this build), SQLite invokes the user data'sxDestroyitself. The port freed it a second time and crashed the process instead of throwingERR_SQLITE_ERROR. Found while porting the fixes above; not present in Node.js. - Portable Linux prebuild:
scripts/prebuild-linux-glibc.shno longer runsapt-getinside thenode:22-bullseyeimage, which already ships GCC 10.2, make, and Python 3.9. Debian 11'sbullseye-securityRelease file expired on 2026-09-07 (Bullseye LTS ended 2026-08-31), soapt-get updatefailed and blocked the glibc 2.31 prebuild locally and in both release workflows. The script now also removes its build container when a step fails. The glibc 2.31 target is unchanged.
2.4.0 (2026-09-02)
- Experimental
DatabasePool(@photostructure/sqlite/experimental): a fixed-size pool of warm SQLite connections whose open, prepare, bind, step, finalize, and close work runs on libuv worker threads. It exposes only the connection-independentrun,get,all, andbatchoperations; a strict authorizer (the default) rejects SQL that depends on which physical connection the pool leases. The stable root export is unchanged. Its compatibility policy is separate from the stable API — see the async pool guide for authorizer policy, connection setup, ordering, memory, and libuv sizing tradeoffs.
2.3.0 (2026-08-10)
API compatible with node:sqlite from Node.js v26.7.0, plus three APIs landed upstream but not yet in a Node.js release line. SQLite is unchanged at 3.53.4.
StatementSync.prototype.close(): Finalizes a prepared statement deterministically instead of waiting for garbage collection or database close. ThrowsERR_INVALID_STATEif the statement is already finalized, if it is currently executing, or if called from inside an authorizer callback —sqlite3_finalize()modifies the connection, which SQLite forbids there, soclose()joins the same guard the other statement methods use. Ported from Node.js PR #64232.StatementSync.prototype[Symbol.dispose](): Enablesusing stmt = db.prepare(...). Unlikeclose(), it is idempotent and never throws; the two casesclose()rejects for safety become no-ops, leaving the statement to be finalized later by GC or database close. Also from Node.js PR #64232.ArrayBufferandSharedArrayBufferparameter binding: Both now bind as BLOBs, matching Node.js PR #62061. Previously onlyArrayBufferViews (Buffer, TypedArray, DataView) were accepted.
These three landed on nodejs/node@main but are not in the v26.x-staging line this package syncs from, so they ship here ahead of their Node.js release. They are covered by this package's own tests; the corresponding upstream tests will arrive with a future sync.
- Use-after-free when a session outlives its database:
Sessionholds a rawDatabaseSync *, and N-API finalization order between the two wrappers is unspecified. If the database was finalized first, every surviving session was left pointing at freed memory and the next session method dereferenced it. Both sides now clear the link, and an orphaned session reportsdatabase is not open. Confirmed with Valgrind before and after. Ports Node.js PR #63797 and #64783 in the shape our N-API port allows — upstream keeps the database alive with a strong reference, which we cannot do: aNapi::Referencemember on a GC-finalizedObjectWrapcorrupts V8 JIT pages on Alpine/musl (see commit 4da0638). ArrayBufferbound as SQLNULL: AnArrayBufferorSharedArrayBufferpassed as the sole argument torun()/get()/all()was treated as a named-parameter object rather than a value, leaving the real parameter unbound. The insert silently storedNULLinstead of the blob.
- Smaller published tarball: a
filesallowlist inpackage.jsonreplaces.npmignore, dropping the package from 78 files to 46. Everythingbinding.gypcompiles still ships, sonode-gyp-build's source fallback is unaffected on platforms without a prebuild. Gone are the TypeScript sources (the published source maps already embedsourcesContent), the reference copies of Node.js's ownnode_sqlite.cc/.h,Makefile,SECURITY.md, andosv-scanner.toml. - Releases are staged for approval instead of published directly (release process):
Build & Releasenow signs and pushes the version commit and tag, then dispatches a tag-boundStage npm Releaseworkflow that rebuilds all eight prebuilds from the tag, packs one tarball, installs and loads it on every supported platform, and stages it on npm for a maintainer to approve with 2FA. Only the staging job holds npm publishing authority: it checks out no source, installs no dependencies, and runs no third-party action. See RELEASE.md. - Upstream sync: Node.js
v26.x-staging@68dc114→v26.x-staging@079339a. Beyond the session lifetime fix above, this range addsIsOpen()guards toenableLoadExtension()andsetAuthorizer()(Node.js PR #64812) and marks the statement iterator done at exhaustion — all three already matched our port, which had them first. Upstream'sBaseObjectPtrguards inExec()/applyChangeset()(Node.js PR #64535) do not apply: an N-APIObjectWrapreceiver is rooted by the handle scope for the whole synchronous call, verified under Valgrind. - Close-inside-callback error message: now
database cannot be closed while in a callback, matching the wording upstream adopted in Node.js PR #64743. Previouslydatabase cannot be closed inside a user-defined function callback. The errorcode(ERR_INVALID_STATE) is unchanged; only the message text differs, so any test matching the old string needs updating. - Node.js compatibility tests sync from the same branch as the sources:
sync:testsdefaulted tomainwhilesync:nodetracksvNN.x-staging, so the suite ran the next major's tests against current-line sources and reported failures for APIs that did not exist in the baseline. Both now resolve the same staging branch.test-sqlite-udf-close.jshad also been downloaded but never adapted, so its four cases — the ones that pin the close-inside-callback message below — were absent fromnpm run test:node; the adapted file is now generated, andsync-node-tests.tsonly runs its sync when invoked directly, so its exports can be reused without triggering one. memory:checkruns again (developer tooling): the sanitizer harness had three independent faults, each masking the next. It exportedLD_PRELOADfor the whole script, sobinding.gyp'snode -phelper ran under LeakSanitizer, exited non-zero on an unrelated leak, and failedconfigure; it drove the build throughnpx node-gyp, which races on creating the.depsdirectories; and it preloaded only an ASan runtime, so the UBSan*_aborthandlers were missing at load. The preload is now applied to the test command alone, the build goes throughnpm run build:native:rebuild, and a UBSan runtime is preloaded next to the ASan runtime. (With clang's ASan runtime, that pairing hangs node at startup; fixed in 3.0.0.) It also probes candidate ASan runtimes and skips any that cannot complete a leak check — clang's compiler-rt runtime wedges in LSan'sStopTheWorldon clang 21 + Linux 7.x, where GCC's libasan works.- Benchmark comparison refreshed (developer tooling): pinned
better-sqlite313.0.3 and regenerated the published throughput table and charts.
2.2.0 (2026-07-25)
No API changes. Upstream refresh and dependency updates.
- SQLite 3.53.4: Updated from 3.53.3. A bug-fix release addressing defects found in 3.53.0–3.53.3, largely by automated analysis — bounds hardening in the JSON/JSONB parsers plus fixes in the session and RBU modules (release notes). No API changes, but the amalgamation is compiled into the shipped binary, so any SQLite bump gets a minor release: consumers choose when to take it.
- Upstream sync: Node.js
v26.x-staging@955e669→v26.x-staging@68dc114, now API compatible withnode:sqlitefrom Node.js v26.5.0. The onlynode_sqlite.ccchange in this range reads the column count after the firststep()inStatementSync.all()(Node.js PR #64219); our port already resolved column metadata lazily on the first row, so no change was needed. - TypeScript held at 6.x:
.ncurc.cjsnow pinstypescriptto the 6.x line. TypeScript 7 is not yet supported bytypedoc(0.28.20 peers<= 6.0.x) ortypescript-eslint(8.63.0 peers< 6.1.0).
2.1.0 (2026-07-13)
No API changes. Build hardening, supply-chain verification, and one undefined-behavior fix.
- Compiler and linker hardening: POSIX builds now follow the OpenSSF hardening baseline — stack protector,
_FORTIFY_SOURCE=2, format-string hardening, full RELRO, non-executable stack, and arch-gated control-flow integrity (Intel CET on x64, PAC/BTI on arm64). Windows ARM64 gains/Qspectreand/guard:signret, the backward-edge protection it previously lacked. - Vendored SQLite integrity: the amalgamation sync now verifies the download against a SHA3-256 pinned in-tree and refuses to vendor a mismatch, instead of compiling whatever it fetched.
- Empty changeset undefined behavior:
session.changeset()/.patchset()on a session with no recorded changes calledmemcpy(NULL, NULL, 0), which is undefined behavior even at zero length. Results are unchanged (still a zero-lengthUint8Array); the UB is gone. Surfaced by the new UndefinedBehaviorSanitizer pass in CI.
2.0.0 (2026-07-11)
API compatible with node:sqlite from Node.js v26.4.0.
DatabaseSync.prototype.serialize([dbName])andDatabaseSync.prototype.deserialize(buffer, [options]): Serialize a database to aUint8Arrayand load one back, matching thenode:sqliteAPIs added in Node.js PR #59967. Wrapssqlite3_serialize/sqlite3_deserializeand finalizes any open prepared statements before replacing database content.
- BREAKING: Dropped support for Node.js 20 (end-of-life April 2026);
@photostructure/sqlitenow requires Node.js 22 or newer (package.jsonenginesis>=22). This is why this release is 2.0.0 rather than a 1.x minor. - SQLite 3.53.3: Updated from 3.53.0. Three patch releases (3.53.1–3.53.3), bug fixes only, no API impact (release notes).
- Upstream sync: Node.js
v25.x-staging@ffa9b8f→v26.x-staging@c96c838. Beyondserialize()/deserialize(), upstream added a column-name caching path and asimdutffast path for ASCII column text inStatementSync— both V8/internal-only optimizations with no N-API equivalent, so not ported. Subsequentnode:sqlitebug fixes — closing the connection after a failedopen(), changesetxFilter/callback-lifetime hardening, and reading the column count after the firststep()inall()— are already covered by our port's structure and needed no change. - Statement finalization on
db.close(): LiveStatementSyncinstances are now eagerly detached when their database closes, so further method calls throwERR_INVALID_STATEwith"statement has been finalized"(matchingnode:sqlite) instead of"Database connection is closed". Statement error messages were also normalized to lowercase"statement has been finalized"throughout. - Build hardening (
SQLITE_ENABLE_API_ARMOR): The bundled SQLite is now compiled with API armor, so misuse of the C API — for example by a loaded extension such as sqlite-vec — returnsSQLITE_MISUSEinstead of risking undefined behavior or a process abort the caller cannot catch. Negligible runtime cost; the public JavaScript API is unaffected. - Callback reentrancy hardening: operations SQLite forbids from inside its own callbacks (notably
close/deserialize, plusprepare/exec/step/serialize/setAuthorizerfrom an authorizer) now throwERR_INVALID_STATEinstead of corrupting connection state. Intentional divergence fromnode:sqlite(nodejs/node#63207). - Config setters frozen mid-step:
setReadBigInts,setReturnArrays, and thesetAllow*parameter setters throwERR_INVALID_STATEif called while the statement is executing.
- Backup teardown stability: In-flight
backup()operations are now safe when a Node environment is shutting down. Backup jobs avoid resolving/rejecting promises or routing expected SQLite failures through node-addon-api's async worker error path after teardown begins. - Authorizer error identity: the exact value thrown by an authorizer callback (subclass,
code, message, thrown primitives) now propagates unchanged through prepare/exec/step/serialize/deserialize/changeset/extension load, instead of being replaced by a generic error. - TEXT with embedded NUL bytes: returned in full via byte-length conversion instead of being truncated at the first NUL.
- Faster multi-row reads: per-statement column-key caching, byte-length string conversion, per-column exception checks removed from the row builder, and a native iterator fast path for flat/raw modes.
-fno-plton Linux removes PLT indirection from Node-API calls in the hot path.
- Docs: bulk-read performance tradeoff documented honestly; Node 22 requirement propagated across docs and examples.
- Benchmark suite reworked for fair, reproducible driver comparison (deterministic workloads, median confidence intervals, per-scenario ratios, SVG charts) plus correlation-gated memory-leak detection.
- Dependencies: node-addon-api 8.9.0, TypeScript 6, ESLint 10, prettier 3.8.5,
@types/node26. - CI: pinned-action updates (CodeQL, TruffleHog, OSV-Scanner, actions/checkout).
1.2.1 (2026-04-27)
- Packaging: Excluded test extension artifact (
test_extension.so) from published npm tarball. The CI pipeline'sdownload-artifactsteps lacked apatternfilter, causing the musl test fixture to be merged intoprebuilds/alongside production binaries.
1.2.0 (2026-04-15)
API compatible with node:sqlite from Node.js v25.9.0.
- SQLite 3.53.0: Updated from 3.52.0. Adds
json_array_insert()/jsonb_array_insert()SQL functions,ALTER TABLEsupport for adding/removingNOT NULLandCHECKconstraints,REINDEX EXPRESSIONSto rebuild expression indexes,VACUUM INTOreserve=NURI parameter, and new C APIs (sqlite3_str_truncate,sqlite3_str_free,sqlite3_carray_bind_v2,SQLITE_PREPARE_FROM_DDL,SQLITE_DBCONFIG_FP_DIGITS). Floating-point text conversion default changed from 15 to 17 significant digits. Full release notes. - Upstream sync: Node.js
v25.x-staging@ca2d6ea→ffa9b8f(includes content through Node.js v25.9.0). Upstream made a cosmetic lambda-capture fix inApplyChangeset's filter callback; our port already used equivalent by-value captures.
- Docs: corrected stale "DataView parameter binding is not currently supported" note;
BLOBbinding acceptsTypedArrayorDataViewinput and returnsUint8Array.
- Test sync: skip
test-sqlite-serialize.js— Node.jsDatabaseSync.prototype.serialize()/deserialize()APIs are not yet ported.
1.1.0 (2026-03-13)
- SQLite 3.51.3: Reverted from 3.52.0 (retracted by the SQLite team)
1.0.0 (2026-03-07)
Promotion to v1.0.0 following API stabilization and 0.5.0 release.
API compatible with node:sqlite from Node.js v25.8.0.
db.limitsproperty: Get and set SQLite limits (length, sqlLength, column, exprDepth, compoundSelect, vdbeOp, functionArg, attach, likePatternLength, variableNumber, triggerDepth) at runtime. SupportsInfinityto reset to compile-time maximum. Also acceptslimitsoption inDatabaseSyncconstructor.- Statement iterator invalidation: Calling
stmt.run(),stmt.get(),stmt.all(), orstmt.iterate()now invalidates any active iterator on the same statement, throwingERR_INVALID_STATE
- SQLite 3.52.0: Updated from 3.51.2
0.5.0 (2026-02-06)
- Statement modes via
enhance():stmt.pluck(),stmt.raw(),stmt.expand()for better-sqlite3 compatibility.pluck()returns only the first column value from queries.raw()returns rows as arrays instead of objects.expand()returns rows namespaced by table, correctly handling duplicate column names across JOINs- All three modes are mutually exclusive, matching better-sqlite3's toggle semantics
stmt.database: Back-reference from prepared statements to their parent database instanceEnhancedStatementMethodstype: TypeScript interface forpluck(),raw(),expand(), anddatabase
0.4.0 (2026-02-04)
API compatible with node:sqlite from Node.js v25.6.1.
enhance()function: Adds better-sqlite3-style.pragma()and.transaction()methods to any compatible database instanceisEnhanced()type guard: Check if a database has enhanced methods- Transaction helper: Automatic BEGIN/COMMIT/ROLLBACK with savepoint support for nested transactions
- Pragma convenience method: Simple API for reading and setting SQLite pragmas with
simpleoption - Node.js test sync script:
npm run sync:testsdownloads and adapts upstream Node.js SQLite tests - Percentile extension:
SQLITE_ENABLE_PERCENTILEnow enabled, addingpercentile(),median(),percentile_cont(),percentile_disc()SQL functions (Node.js v25+) - Prepare options:
db.prepare(sql, options)now accepts per-statement options (readBigInts,returnArrays,allowBareNamedParameters,allowUnknownNamedParameters) to override database-level defaults. This is a Node.js v25+ feature;node:sqliteon v24 and earlier silently ignores these options. - StatementColumnMetadata type:
stmt.columns()now returns richer metadata includingcolumn,database,table, andtypeproperties alongsidename - SQLite 3.51.2: Updated from 3.51.1
- BREAKING: Removed API extensions to achieve exact parity with
node:sqlite:- Removed
stmt.finalize()method (use database close for cleanup) - Removed
stmt.finalizedproperty - Removed
stmt[Symbol.dispose](still available onDatabaseSyncandSession) - Removed
db.backup()instance method (use standalonebackup(db, path)function instead)
- Removed
- BREAKING:
Session.changeset()andSession.patchset()now returnUint8Arrayinstead ofBufferto matchnode:sqliteAPI - BREAKING: Defensive mode now defaults to
trueinstead offalseto match Node.js v25+ behavior. Use{ defensive: false }to restore old behavior.
- Alpine Linux / musl stability: Fixed native crashes by removing N-API reference cleanup from destructors that corrupted V8 JIT state
- Session lifecycle management: Fixed use-after-free, double-free, and mutex deadlock when databases are garbage collected before their sessions
- Worker thread stability: Added cleanup hooks and exception handling for worker thread termination
- Callback error preservation:
applyChangeset()now preserves the original error message when JavaScript callbacks throw createTagStore()now throws errors withcode: 'ERR_INVALID_STATE'property when database is closed, matching Node.js error format
0.3.0 (2025-12-17)
- BREAKING:
SQLTagStore.sizechanged from method to getter for Node.js API parity (Node.js PR #60246)- Before:
sql.size() - After:
sql.size - Note: This change was merged into Node.js main on December 11, 2025 and will appear in a future Node.js release. Current Node.js v24.x still uses
sql.size()as a method.
- Before:
0.2.1 (2025-12-01)
- Windows ARM64 prebuilt binaries
- Error message handling on Windows ARM64 (ABI compatibility)
- Error handling consistency across platforms
0.2.0 (2025-12-01)
- Node.js v25 API sync: SQLite 3.51.1, native
Symbol.disposein C++, Session class exposed in public API - New database open options:
readBigInts,returnArrays,allowBareNamedParameters,allowUnknownNamedParameters,defensive,open - Defensive mode:
enableDefensive()method to prevent SQL from deliberately corrupting the database - Statement enhancements:
setAllowUnknownNamedParameters()method,finalizedproperty - Type identification:
sqlite-typesymbol property on DatabaseSync (Node.js PR #59405) - SQLite error properties:
sqliteCode,sqliteExtendedCode,code,sqliteErrorString,systemErrno - ARM64 prebuilds: macOS Apple Silicon and Windows ARM64 binaries
- Tagged template literals:
db.createTagStore()for cached prepared statements (Node.js PR #58748) - Authorization API:
db.setAuthorizer()for security callbacks (Node.js PR #59928) - Standalone backup:
backup(srcDb, destFile, options?)for database backups with progress callbacks
- DataView parameter binding (previously returned garbage data)
- DataView and TypedArray return values in user-defined functions
- RETURNING clause metadata handling
- Null and empty values in user function return value conversion
- Native stability: N-API reference cleanup in aggregates/destructors, thread-local napi_env storage, statement-to-database reference tracking, deferred exception handling in authorizers
0.0.1 (2025-06-13)
- Initial release of
@photostructure/sqlite, standalone SQLite for Node.js 20+ - Compatible with Node.js built-in SQLite module API
- Core SQLite operations with
DatabaseSyncandStatementSyncclasses - User-defined scalar and aggregate functions with window function support
- Database backup and restoration
- SQLite sessions and changesets for change tracking
- Extension loading with automatic platform-specific file resolution
- TypeScript definitions
- Cross-platform prebuilt binaries for Windows, macOS, and Linux (x64, ARM64)
- Test suite with 89+ tests
- Memory safety validation with Valgrind and sanitizers
- Performance benchmarking suite comparing to better-sqlite3
- Automated synchronization from Node.js upstream SQLite implementation
- CI/CD pipeline with security scanning and multi-platform builds
- Synchronous API: Blocking database operations for scripts and tools
- Parameter binding: All SQLite data types including BigInt
- Error handling: Detailed error messages with SQLite error codes
- Resource limits: Control memory usage and query complexity
- Safe integer handling: JavaScript-safe integer conversion with overflow detection
- Multi-process support: Concurrent access from multiple Node.js processes
- Worker thread support: Works in worker threads
- URI filename support: SQLite URI syntax for advanced database configuration
- Strict tables: SQLite strict table mode
- Double-quoted strings: Configurable SQL syntax compatibility
- Node.js 20.0.0 and later
- Windows (x64, ARM64)
- macOS (x64, ARM64)
- Linux (x64, ARM64), (glibc 2.28+, musl)