Repository navigation
v3.0.0
·
20 commits
to main
since this release
Immutable
release. Only release title and notes can be modified.
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.
Added
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.
Changed
- 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.
Fixed
- 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.
Full Changelog: v2.6.0...v3.0.0