@boxdb/client

中文文档
0.7.0发布于 2026/10/31MITTypeScriptES Module零依赖构建来源已验证近 30 天下载 2084

BoxDB queries and SQLite driver backed by the Rust core

原始使用文档
文档目录9 节
  1. Install
  2. Build from source
  3. Queries
  4. Transactions and cancellation
  5. Change history
  6. Options, values and diagnostics
  7. File diagnostics and recovery
  8. SQL migrations
  9. Integrations

BoxDB for TypeScript

An asynchronous Node.js and Bun SQLite client backed by the BoxDB Rust core. Each connection owns a native worker: SQL execution and lock waits leave the JavaScript event loop free. Binary SQLite files, readable YAML storage, extended column types and built-in SQL extensions use the same core.

Install

npm install @boxdb/client

Requires Node.js 22.18+ or Bun on macOS ARM64 (macOS 11+) or Linux x64 or ARM64 with glibc 2.35+ (Ubuntu 22.04+ or 64-bit Raspberry Pi OS Bookworm+). All three native addons are bundled in the npm package; installation needs no Rust compiler, install scripts or extra downloads. Alpine/musl, Windows, Intel macOS and 32-bit ARM are not supported by this npm release. This client does not run in browsers.

Build from source

Requires Node.js 22.18+, Bun, Rust, native build tools and libclang; see the repository README. From the repository root:

bun install --frozen-lockfile
bun run build
bun run check
bun run test

just ts-build and just ts-check ts-test also build and verify all packages. For an optimized addon, run bun run --cwd packages/typescript build:release, then build the adapter packages. Builds target the current OS and architecture; the npm release builds are described in the release guide.

Queries

Query rows are unknown[]. Validate them before use with a schema library such as Zod:

npm install zod
import { BoxDB, Changeset } from "@boxdb/client";
import { z } from "zod";

const User = z.object({ id: z.bigint(), name: z.string() });

await using db = await BoxDB.open("data.db", { busyTimeoutMs: 5000 });
await db.exec("CREATE TABLE IF NOT EXISTS users(id INTEGER PRIMARY KEY, name TEXT)");
const inserted = await db.query`INSERT INTO users(name) VALUES(${"Ada"})`.run();

const rows = User.array().parse(
  await db.query`
  SELECT * FROM ${db.identifier("users")}
  ${db.fragment`WHERE id = ${inserted.lastInsertRowid}`}
  LIMIT ${100}
  `,
); // { id: bigint; name: string }[] after runtime validation

Scalar interpolations become bound parameters; do not put SQL quotes around them. identifier("main", "users") quotes separate components; identifier("user.name") quotes one name containing a dot. Fragments snapshot parameters and compose without executing SQL.

Queries execute once, lazily. await query and query.execute({ signal }) return row objects as unknown[]. query.get({ signal }) returns the first row or undefined, typed as unknown. query.values({ signal }) returns positional rows as unknown[], preserving duplicate column names. Validate each result against the shape selected by the query. query.run({ signal }) returns { changes, lastInsertRowid } and requires a statement without returned columns. These methods share the execution result or error; the first execution chooses its signal. query.compile() returns a copy of { sql, parameters } without executing.

For SQL from an adapter or migration tool:

const result = await db.execute("SELECT name FROM users WHERE id=:id", { id: 1 });
// { rows, columns, changes, lastInsertRowid }
const names = User.pick({ name: true }).array().parse(result.rows);
const positional = await db.execute("SELECT 1 AS x, 2 AS x", [], { raw: true });
await db.exec("CREATE INDEX IF NOT EXISTS users_name ON users(name);");

execute accepts one statement and positional or named bindings and returns rows: unknown[] in both object and raw modes. exec accepts scripts without parameters. Queries accept no result type parameter; derive application types from validated data. The client has no dependency on a validation library. Explicit prepared statements and streaming are not part of this API.

Transactions and cancellation

Use TypeScript 5.2+; transpile await using for runtimes without native syntax support (including Node.js 22).

await using tx = await db.begin({ mode: "immediate", signal });
await tx.query`INSERT INTO users(name) VALUES(${"Grace"})`;
{
  await using nested = await tx.begin();
  await nested.query`INSERT INTO users(name) VALUES(${"temporary"})`;
  // Exiting without commit rolls back this savepoint.
}
await tx.commit();

Commit is explicit. Disposal and rollback() discard unfinished work. Root transactions use immediate by default, acquiring the writer before transaction work starts. Explicit deferred and exclusive modes remain available.

Use db.begin({ readOnly: true }) for a deferred transaction that rejects SQL writes with BOXDB_READONLY. In binary WAL databases it can read a consistent snapshot while another connection writes. The native guard covers queries and scripts, including attempts to disable query_only; the previous connection setting is restored after the transaction. Read-only connections automatically start read-only transactions. Combining read-only with an immediate or exclusive mode throws TypeError.

await using tx = await db.begin({ readOnly: true });
const total = z.object({ count: z.bigint() }).parse(
  await tx.query`SELECT count(*) AS count FROM users`.get(),
);
await tx.commit();

Nested transactions use savepoints and inherit the outer read-only restriction and deadline. They have no independent mode. Use the innermost transaction handle for work inside a transaction; parent-handle SQL fails while a nested handle owns the connection. Root requests, including db.begin(), wait for the entire transaction to finish. Root SQL then executes independently in autocommit, in submission order. SQL transaction-control statements must use handles too.

Transactions have a five-second lifetime by default, starting after BEGIN succeeds. Set transactionTimeoutMs on the client or override it with timeoutMs on db.begin(); both accept integers from 1 to 2147483647 milliseconds. Savepoints inherit the outer deadline and do not extend it. Expiry interrupts running SQL, cancels queued transaction work and rolls back. Waiting root requests resume only after cleanup; further queries or commits through the expired handle fail with BOXDB_TRANSACTION_TIMEOUT.

await using db = await BoxDB.open(":memory:", { transactionTimeoutMs: 10_000 });
await using tx = await db.begin({ timeoutMs: 2_000 });
const pending = db.query`SELECT 1 AS answer`.execute();
await tx.commit();
await pending; // Executes outside the completed transaction.

Awaiting a root request before finishing your own transaction waits until its timeout rolls it back. Use tx for operations needed before commit or rollback. The deadline starts cancellation; freeing the connection still requires native SQL interruption and rollback to complete.

tx.active reports whether its transaction still exists. A failed commit leaves an active SQLite transaction available for repair or retry. If SQLite itself rolls back a transaction after an interrupt, further work through that handle fails instead of escaping into autocommit.

const controller = new AbortController();
const pending = db.query`SELECT * FROM users`.execute({ signal: controller.signal });
controller.abort(new Error("request no longer needed"));
await pending; // Rejects with the cancellation reason if cancellation wins.

Signals cancel queued requests before execution and interrupt running SQL or lock waits. A transaction inherits its signal; aborting an idle transaction also rolls it back. Rollback cleanup cannot be cancelled by that signal. Cancellation cannot undo a statement that has already committed; use a transaction for atomic multi-statement work.

close() rejects new root requests, drains accepted work and waits for active transactions to finish. Their handles can still commit or roll back. close({ mode: "abort" }) cancels pending and running work, rolls back active transactions and waits for cleanup. An existing draining close can be escalated to abort. await using db performs a draining close.

Change history

Transactions can record their row changes and run commit triggers; see change history for the SQL side.

await using tx = await db.begin({ context: { label: "Rename task" } });
await tx.query`UPDATE tasks SET title = ${"New title"} WHERE id = ${3n}`;
await tx.commit(); // A BEFORE COMMIT trigger can store boxdb_changeset().

const row = z.object({ changes: z.instanceof(Changeset) }).parse(
  await db.query`SELECT changes FROM history ORDER BY id DESC LIMIT 1`.get(),
);
const report = await db.apply(row.changes, { invert: true }); // Undo in its own transaction.

begin({ context }) stores values readable through boxdb_context(key) for the transaction's lifetime; begin({ record: true }) records changes so tx.changeset() returns a Changeset or null before commit. tx.apply() applies a changeset inside the transaction and db.apply() in a new immediate transaction; both accept invert and onConflict (abort, skip or replace) and return { applied, skipped, conflicts }. CHANGESET columns return Changeset instances, which also bind as parameters; changeset.bytes holds the encoded changeset.

Options, values and diagnostics

BoxDB.open(path, options) accepts format: "binary" | "readable", readOnly, busyTimeoutMs, transactionTimeoutMs, maxPendingQueries, and signal. Existing contents determine the format. A read-only readable connection reads an opening snapshot without taking the writer's lock.

Inputs accept null, strings, finite numbers, bigint, boolean and Uint8Array. Bigint parameters accept the combined INT64/UINT64 range (-2^63 through 2^64 - 1); positive values above INT64_MAX bind as UINT64. Use CAST(? AS UINT8/16/32) to select a narrower unsigned width. Safe integer numbers and booleans bind as integers; fractional numbers bind as REAL. Serialize objects explicitly. Every signed or unsigned integer cell, write count and row ID returns as bigint; REAL remains number. Convert bigint explicitly when serializing JSON. Unsafe integer inputs throw instead of rounding.

Results retain native decimal values as exact text. DECIMAL arithmetic, comparisons and ordering run in the engine; NUMERIC is not supported. UUID and temporal values return canonical strings: INSTANT uses UTC, TIME has no date or time zone, DATETIME has no time zone, and temporal strings preserve microsecond precision. JSON and arrays remain opaque strings; SQL JSON processing is disabled. Blobs are Buffer values. See the native decimal guide and temporal types.

BoxDBError preserves code, resultCode, extendedCode and cause. Engine failures have BOXDB_* names, such as BOXDB_CONSTRAINT_UNIQUE. Validation, closed connections and native infrastructure failures have separate categories; an exhausted queue uses BOXDB_QUEUE_FULL. findBoxDBError(error) follows causes and aggregate members without looping on cycles. Invalid arguments throw TypeError or RangeError. See the client error contract.

The optional queue limit bounds waiting operations, including root requests waiting for a transaction; cleanup always gets through. db.diagnostics reports pending/active/completed operations and the last queue wait and execution durations in milliseconds. A duration covers a complete scheduled operation, including preparation and native execution.

File diagnostics and recovery

For existing files, use await BoxDB.checkFile(path, "full"), await BoxDB.repairFile(source, destination) or await BoxDB.recoverFile(source, destination, "binary") independently of a live client. Reports distinguish invalid data from incomplete checks. Recovery can lose data even when its output passes validation; see diagnostics and recovery.

SQL migrations

import { MigrationSet } from "@boxdb/client";

const migrations = await MigrationSet.read("db/migrations");
await migrations.up(db);

Use MigrationSet.fromFiles([{ filename, sql }]) to bundle SQL in the application. The same set exposes status(db) and down(db); execution methods accept { signal }. These methods use the CLI's format and history, execute on the native worker and wait for active transactions on the root client. See migration sources, cancellation and rollback.

Integrations

Adapters are independent npm packages with their libraries as peer dependencies. They borrow the client and do not close it:

  • Drizzle adapter: asynchronous queries, transactions, batches and migrations.
  • Better Auth adapter: direct database adapter, schema generation and native atomic credential/counter operations.
  • db0 adapter: a SQLite connector accepting a database or transaction handle.

Every connection includes decimal arithmetic, Unicode text functions, regular expressions, sqlite-vec, FTS5 and R*Tree; see the SQL extension guide. Virtual tables require memory or binary storage.

版本与发布记录

@boxdb/client 共发布 15 个版本,首次发布于 2026/10/15;过去 12 个月发布了 15 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.7.0latest2026/10/318 天前
0.6.02026/10/308 天前
0.5.12026/10/2415 天前
0.5.02026/10/2316 天前
0.4.32026/10/2217 天前
0.4.22026/10/1821 天前

关于 @boxdb/client 的常见问题

@boxdb/client 是什么?

BoxDB queries and SQLite driver backed by the Rust core

如何安装 @boxdb/client?

在项目目录运行 npm install @boxdb/client,或使用 pnpm add / yarn add / bun add。

@boxdb/client 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@boxdb/client 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@boxdb/client 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22.18.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@boxdb/client 可以商用吗?

@boxdb/client 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@boxdb/client 的下载量有多少?

根据 npm 官方下载统计,@boxdb/client 在 2026/10/09 – 2026/11/07 这 30 天内被下载 2,084 次,日均约 69 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@boxdb/client 还在维护吗?

最新版本 0.7.0 发布于 2026/10/31(8 天前);至今共发布 15 个版本,其中过去 12 个月发布了 15 个;首次发布于 2026/10/15。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。