Skip to content

v3.0.0

Choose a tag to compare

@github-actions github-actions released this 03 Oct 03:12
· 20 commits to main since this release
Immutable release. Only release title and notes can be modified.
v3.0.0
9d690c2

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

  • Database and Statement: the new names of DatabaseSync and StatementSync from 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 types DatabaseInstance, DatabaseOptions, DatabaseLimits, and StatementInstance are aliases of DatabaseSyncInstance, DatabaseSyncOptions, DatabaseSyncLimits, and StatementSyncInstance.

Changed

  • BREAKING: Class names follow the rename: DatabaseSync.name and a database's constructor.name are now "Database", StatementSync.name and a statement's constructor.name are "Statement", and the iterator from iterate() is a StatementIterator (was StatementSyncIterator), as in node:sqlite from 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 DatabaseSync and instanceof StatementSync still work.
  • open() after close() restores the connection's settings: a setAuthorizer() callback was not reinstalled, so after close() and open() a deny-all authorizer stopped denying anything; limits set through db.limits went back to the constructor's values; and extension loading turned on with enableLoadExtension(true) was off again. All three now carry over, as in node:sqlite from Node.js PR #66042.
  • TEXT longer than V8's maximum string length throws ERR_STRING_TOO_LONG: reading such a value with get(), all(), or iterate(), passing one to a user-defined or aggregate function, or reading one through DatabasePool threw Error: Unknown failure with no code. In a user-defined or aggregate function the error was a C++ exception that unwound through SQLite, after which close() always failed with ERR_INVALID_STATE (database cannot be closed while in a callback). It now throws ERR_STRING_TOO_LONG, as in node:sqlite from Node.js PR #66209, and the connection stays usable.
  • Experimental DatabasePool keeps its name: the bundler renamed the class, so DatabasePool.name was "_DatabasePool" and util.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: each sqlite3_backup_step() now returns to the main thread before the next one is queued, as in node:sqlite. The progress callback is therefore called after every step that leaves pages remaining; previously calls could be coalesced. Small rate values cost more: on tmpfs, a 128 MB backup took 580–690 ms at rate: 1 (was 195–210 ms; node:sqlite 580–670 ms) and 132–150 ms at the default rate: 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, as node:sqlite and DatabasePool already 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 without useBigIntArguments received -9223372036854775808 as the imprecise Number -9223372036854776000, because the range check used std::abs(), which is undefined for that value. It now throws like every other integer outside the safe range, as in node: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 + 5n became 5n), all without an error. A start or step value that does not fit now throws, and aggregate() throws for such a BigInt start. node:sqlite keeps the JavaScript value itself and has neither limit.
  • An unparsable URL href throws ERR_INVALID_URL: new Database({ href: "not a url" }) and backup(db, { href: "not a url" }) threw ERR_INVALID_URL_SCHEME; they now throw ERR_INVALID_URL, as node:sqlite does. Ports Node.js PR #66026.

Fixed

  • SQLite errors swallowed after a throwing function in exec(): when a user-defined function threw inside exec(), the next SQLite error on that connection was ignored, so for example an INSERT that violated a primary key returned undefined instead of throwing. Only an error with a JavaScript exception actually pending is now ignored, as in node:sqlite from 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 same Database waited 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 at rate: 100).
  • Worker terminated during backup(): terminating a worker thread while it ran a backup with a progress callback 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 with FATAL ERROR: Error::Error napi_define_properties, because node-addon-api's conversion of the termination exception into a Napi::Error is fatal when JavaScript can no longer run. The worker now exits with the requested code. Rejection messages for a throwing progress callback are unchanged.
  • close() during backup(): 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 with ERR_SQLITE_ERROR: errcode 1 (SQL logic error), or errcode 5 (database is locked) if the backup was waiting on a lock when close() 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 example Readable.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 in node:sqlite.
  • expand() wrote columns onto Object.prototype: with enhance(), a query whose columns came from a table named __proto__ (directly or through a view) set those columns on Object.prototype for the whole process, and a table named constructor set them on Object. 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 step function threw, or an argument could not be converted, in an aggregate whose accumulator was an object, array, or Buffer, get() returned undefined instead 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 returned SQLITE_BUSY or SQLITE_LOCKED queued 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 replace setTimeout do 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.
  • DataView aggregate accumulator: an aggregate whose start or step value was a DataView failed with Invalid 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 a Uint8Array, as for a Buffer.
  • DatabasePool error without a code: a named parameter whose key contained a NUL byte was rejected with a TypeError that had no code; it now has ERR_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 an Error that had no code; it now has ERR_INVALID_STATE, as in Database and node:sqlite.
  • Unknown named parameter message cut off at a NUL: for a key such as "tenant\0x", the Database error message ended at the NUL (Unknown named parameter 'tenant); it now contains the whole key.
  • SECURITY.md read-only example opened read-write: it passed readonly: true, which Database ignores; it now passes readOnly: true. Its extension example called db.allowExtension(), which does not exist, instead of passing allowExtension: true to the constructor.
  • defensive documented as off by default: the type docs and API reference said defensive defaults to false. It defaults to true; 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, while node:sqlite binds it and can replace the ":tenant" binding. The Statement and DatabasePool type docs give the details. If parameter objects are built from untrusted keys, reject keys that contain NUL.
  • memory:check hung with clang's ASan runtime (developer tooling): the sanitizer harness preloaded libclang_rt.ubsan_standalone next 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 *_abort handlers, so a separate UBSan runtime is now preloaded only with GCC's libasan.

Full Changelog: v2.6.0...v3.0.0