@easbot/database
v0.3.23
Published
Unified database abstraction for EASBot — SQLite (better-sqlite3 / node:sqlite / @tursodatabase/database) with first-class transactions (begin/commit/rollback + nested SAVEPOINT) and PostgreSQL behind a single DatabaseInterface.
Maintainers
Readme
English | 中文
@easbot/database
EASBot 数据库抽象层:在统一 DatabaseInterface 之后屏蔽后端差异,提供 SQLite(better-sqlite3 / node:sqlite / @tursodatabase/database 三选一)与 PostgreSQL 双方言实现,并内置嵌套事务(SAVEPOINT)、参数占位符翻译、错误码体系。
详细架构 / 后端选择策略 / 错误码 / 性能预算 / 单测基线见项目发布文档
docs/components/database.md。包内docs/EAS_DATABASE_DESIGN.md为历史私有副本,新信息请以上述链接为准。
应用层应当通过 DatabaseFactory 获取连接,再基于 DatabaseInterface 自建业务 DAO(Agent / Task / Memory 等实体的持久化不归本包)。
特性
- 统一接口:
DatabaseInterface覆盖query / queryOne / execute / exec / execBatch / begin / withTransaction,底层方言差异被屏蔽 - 同步 facade:
SyncSqliteConnection暴露better-sqlite3形态的同步 API(exec / prepare / pragma / transaction / getJournalMode / ...),给同步热路径使用(Tree-sitter / 本地 embedding / 主线程 SQLite 调用) - 后端可插拔:SQLite 三大 backend 自动探测(
better-sqlite3→node:sqlite→@tursodatabase/database),并支持显式指定;PostgreSQL 通过pg池化连接 - 占位符统一为
?:内置translatePlaceholders(sql, 'numbered')把?翻译为$1, $2 ...,兼容 pg 等 numbered 占位符方言 - 嵌套事务:同一连接上
withTransaction嵌套调用走SAVEPOINT sp_<N>;最外层负责BEGIN/COMMIT/ROLLBACK,内层句柄仅做状态机 - 自动兜底:
withTransaction(fn)在 callback 抛错时自动 rollback;同一 handle 二次 commit / rollback 幂等;readonly 数据库拒绝 begin - 错误体系:
DatabaseError根类 +ConnectionError / QueryError / TransactionError子类,cause 链保留驱动层原始异常
两种接入模式
模式 A:异步 facade(DatabaseFactory / DatabaseInterface)
适用于异步调用栈——MCP server / 跨网络 / web handler:
import { DatabaseFactory } from '@easbot/database';
const db = DatabaseFactory.create({
flavor: 'sqlite',
sqlite: { path: 'app.db', backend: 'auto', walMode: true },
});
await db.initialize();
const rows = await db.query<{ id: string }>('SELECT id FROM users WHERE status = ?', ['active']);模式 B:同步 facade(SyncSqliteConnection)—— 新
适用于同步热路径——Tree-sitter WASM 解析 / 本地 embedding 推理 / 主线程 SQLite 调用:
import { createSyncSqliteConnection } from '@easbot/database';
const conn = createSyncSqliteConnection({
path: 'app.db',
backend: 'better-sqlite3', // 或 'node-sqlite' / '@tursodatabase/database'
walMode: true,
});
conn.initialize();
// 完全同步 API,对标 better-sqlite3 / codegraph SqliteDatabase
const stmt = conn.prepare('SELECT id FROM users WHERE status = ?');
const rows = stmt.all('active');
conn.close();典型应用:packages/codebase 的 DatabaseManager 60+ 同步 DB 调用点通过 SyncSqliteConnection 接入 @easbot/database,API 形态完全兼容 better-sqlite3 同步 binding,零改动。详见决策 0036-storage-backend.md。
backend 与 facade 选择
| 场景 | 推荐 facade | 推荐 backend |
|---|---|---|
| 同步热路径(Tree-sitter / 本地推理 / 主线程 SQLite) | SyncSqliteConnection | better-sqlite3 |
| MCP server / 异步 CLI | DatabaseInterface(async) | better-sqlite3 |
| cross-platform 构建失败回退 | 任意 | node:sqlite(Node 22.5+ 内置) |
| 嵌入式 / 远期 Turso 兼容 | 任意 | @tursodatabase/database |
| PostgreSQL 切换 | DatabaseInterface(async) | pg |
安装
pnpm add @easbot/database或作为 monorepo 工作区依赖:
{
"dependencies": {
"@easbot/database": "workspace:*"
}
}注意:better-sqlite3 是默认 backend,作为 optional peerDependency。如果运行环境只能使用 node:sqlite(Node 22.5+ 内置),可让 better-sqlite3 不安装;resolver 会自动降级。
使用方式
1. 通过 DatabaseFactory 创建 SQLite 实例
import { DatabaseFactory } from '@easbot/database';
// backend: 'auto' | 'better-sqlite3' | 'node:sqlite' | '@tursodatabase/database'
const db = DatabaseFactory.create({
flavor: 'sqlite',
sqlite: {
path: 'app.db',
backend: 'auto',
walMode: true,
foreignKeys: true,
},
});
await db.initialize();
// 统一 ? 占位符(SQLite / PostgreSQL 通用)
const users = await db.query<{ id: string; name: string }>(
'SELECT id, name FROM users WHERE status = ? ORDER BY name',
['active'],
);2. 嵌套事务 + 自动回滚
await db.withTransaction(async (tx) => {
await tx.execute('UPDATE accounts SET balance = balance - ? WHERE id = ?', [100, 'alice']);
await tx.execute('UPDATE accounts SET balance = balance + ? WHERE id = ?', [100, 'bob']);
// 嵌套:SAVEPOINT sp_2;失败仅回滚内层
try {
await db.withTransaction(async (inner) => {
await inner.execute('INSERT INTO audit_log (...) VALUES (...)', [...]);
throw new Error('rollback me');
});
} catch {
/* ignore — outer tx still alive */
}
// 显式 SAVEPOINT(控制粒度更细)
await tx.savepoint('checkpoint_1');
/* ... */
await tx.rollbackTo('checkpoint_1');
await tx.release('checkpoint_1');
});
// 退出最外层 withTransaction 自动 commit;中途抛错自动 rollback3. 切换 PostgreSQL
import { DatabaseFactory, translatePlaceholders } from '@easbot/database';
const pg = DatabaseFactory.create({
flavor: 'postgresql',
host: '127.0.0.1',
port: 5432,
database: 'app',
username: 'app',
password: '***',
ssl: false,
poolSize: 10,
});
await pg.initialize();
// 应用层始终使用 `?`;后端自动翻译为 `$1, $2 ...`
await pg.query('SELECT * FROM users WHERE id = ?', [42]);4. 应用层自建业务 DAO(不属于本包)
数据库抽象只暴露"数据库能力"。业务实体(如 Agent / Task / Memory)的 schema、字段映射、JSON 序列化策略属于上层职责——应用层应自己基于 DatabaseInterface 写 DAO,例如:
import type { DatabaseInterface } from '@easbot/database';
interface Widget {
id: string;
name: string;
color: string;
}
class WidgetDao {
constructor(private readonly db: DatabaseInterface) {}
async create(w: Widget) {
await this.db.execute('INSERT INTO widgets (id, name, color) VALUES (?, ?, ?)', [
w.id,
w.name,
w.color,
]);
}
async findById(id: string): Promise<Widget | null> {
const rows = await this.db.query<{ id: string; name: string; color: string }>(
'SELECT id, name, color FROM widgets WHERE id = ?',
[id],
);
return rows[0] ?? null;
}
}后端 backend 选择策略
| backend | 性能 | 何时适用 |
|---|---|---|
| better-sqlite3 | ✅ 最佳(sqg.dev 基准验证) | 默认;要求 node-gyp 构建 |
| node:sqlite | 次之(比 better-sqlite3 慢 10–20%) | Node 22.5+ 内置;零原生依赖;适合 cross-platform 构建失败场景 |
| @tursodatabase/database | 中(Turso 历史性能较差) | 嵌入式 / 远期兜底;可选依赖 |
DatabaseFactory.create({ flavor: 'sqlite', sqlite: { backend: 'auto' } }) 时,resolver 按上表顺序探测;显式指定 backend 跳过探测。
错误码
| code | 类 | 含义 |
|---|---|---|
| CONNECTION | ConnectionError | backend 创建 / 初始化失败(WAL 失败、native binding 缺失等) |
| QUERY | QueryError | SQL 执行失败(语法错、列不存在、类型不匹配) |
| TX | TransactionError | 事务状态机错误(二次操作、readonly begin 失败、SAVEPOINT 抛错) |
错误实例保留 cause 链,可向上层聚合时透出原始驱动抛错。
不在范围内
- 业务实体持久化(Agent / Task / Memory / Knowledge Graph 等)—— 由各自领域包提供
- MCP / Skill 暴露—— 由
@easbot/mcp、@easbot/skills、easbot主 CLI 承载 - 向量数据库接口—— 占位 stub 未实装,留作将来
CLI
本包不暴露独立 CLI;调用方通过 DatabaseFactory / DatabaseInterface API 集成。
性能优化
1. prepared statement LRU(SQLite)
SqliteDatabase 内部维护一个容量 256 的 LRU 缓存,命中已 prepare 过的 SQL 跳过 tokenize + parse 阶段:
- 热路径:所有重复 SQL 0-cost 命中;
- 容量满:按 LRU 淘汰最久未访问;
- 失效策略:仅在检测到 DDL(
CREATE/DROP/ALTER/TRUNCATE/REPLACE)时清空,普通 DML 与PRAGMA不影响 cache。
2. 占位符翻译缓存(PostgreSQL)
PostgresDatabase 对 translatePlaceholders 结果按 SQL 字符串做 256 容量 LRU 缓存,避免每次 query 都扫描 SQL 替换 ?:
- 无占位符 SQL 一次性 short-circuit(直接返回原 SQL,params 传
undefined避免 pg 警告噪音); - 有占位符 SQL:命中即跳过
String.replace + closure计数。
3. execBatch 单 round-trip(PostgreSQL)
PG 上 execBatch 用 ; 分号拼接多条 statement 一次性发送,由 PG 自身批量执行;相比逐条 client.query() 节省 N-1 次网络往返。SQLite 仍走逐条 backend.exec(),因底层是同步 API 不存在网络收益。
嵌套事务语义对齐(SQLite ⇄ PostgreSQL)
两条路径在事务行为上完全一致:
| 深度 | SQLite | PostgreSQL |
|---|---|---|
| begin() depth=1 | BEGIN | BEGIN(独占 client) |
| begin() depth>1 | SAVEPOINT sp_<N> | SAVEPOINT sp_<N>(复用最外层 client) |
| 内层 commit() | 仅清理状态 | RELEASE SAVEPOINT sp_<N> |
| 内层 rollback() | ROLLBACK TO SAVEPOINT sp_<N> | ROLLBACK TO SAVEPOINT sp_<N> |
| 外层 commit() | COMMIT | COMMIT + 释放 client |
| 外层 rollback() | ROLLBACK | ROLLBACK + 释放 client |
| 嵌套失败后 | 内层 SQL 撤回,外层不受影响 | 同左 |
应用层无需关心方言;db.withTransaction 嵌套调用行为完全一致。
边界与错误处理
- 未
initialize()调用:query / execute / exec / begin全部抛ConnectionError; close()后再调用:同上抛ConnectionError,内部状态全部重置;- readonly SQLite:
begin()立即抛TransactionError,避免 SQLite 原生报错信息含糊; - PG
pool.connect()失败:begin()抛TransactionError,且lastTxDepth归位,下次begin()从 depth=1 重新开始(不污染计数); - 同一 handle 二次 commit / rollback:幂等守门(
finished标记),不重复发 SQL; - finalize 后再 query / savepoint:抛
TransactionError,提示事务已终结; - SAVEPOINT / 表名包含非 ASCII / 注入字符:
isSafeIdent拒绝,抛TransactionError阻断拼接。
测试矩阵
| 测试套件 | 覆盖 | 文件 |
|---|---|---|
| sqlite-database.test.ts | backend 探测 / query / execute / 连接管理 / LRU cache / DDL 失效 | tests/__tests__/ |
| transaction.test.ts | basic / 幂等 / 嵌套 SAVEPOINT / 显式 savepoint / readonly 守卫 | tests/__tests__/ |
| utils.test.ts | isSafeIdent / translatePlaceholders / DatabaseError 体系 | tests/__tests__/ |
| postgresql.test.ts | 占位符翻译缓存 / 嵌套 SAVEPOINT / 句柄幂等 / 错误分类 / connect 失败归位(用 pg 模块桩) | tests/__tests__/ |
| sqlite-cache.test.ts | LRU 容量上限淘汰 / exec 精确清 cache / readonly 边界 | tests/__tests__/ |
| backends.test.ts | backend 探测 / 显式选择 / 应用层 DAO 模式 | tests/__tests__/ |
| examples/test-database.ts | 端到端冒烟(35 assertions) | examples/ |
| examples/test-pgsql.ts | PG 端到端(19 assertions,含嵌套 SAVEPOINT) | examples/ |
运行:
pnpm test:run # 66 vitest tests
node --import tsx examples/test-database.ts # SQLite 35/35
node --import tsx examples/test-pgsql.ts # PG 19/19 (需 docker compose up -d pg)版本变更
0.4.x(本版本)
修复:
- PG 嵌套事务 SAVEPOINT 语义:内层
begin()现在真发SAVEPOINT sp_<N>;内层 commit 发RELEASE SAVEPOINT sp_<N>;rollback 发ROLLBACK TO SAVEPOINT sp_<N>。与 SQLite 路径行为完全对齐(之前实现只发BEGIN/COMMIT,嵌套语义错乱)。 - PG
pool.connect()失败归位lastTxDepth:begin 失败时正确归位计数,避免下次 begin 从脏 depth 开始。 - PG
client.release(destroy=true):BEGIN 失败时销毁损坏 client,避免脏连接回流 pool。 - PG
close()错误传播:之前pool.end()抛错被 silently swallow,现在抛ConnectionError。 - PG
translatePlaceholdersLRU 缓存:避免每次 query 重扫 SQL;无占位符 SQL 传undefined避免 pg 噪音警告。 - PG
execBatch单 round-trip:分号拼接多 statement 一次发送,节省网络往返。 - SQLite
exec()精确清 cache:从"任何 exec 都清"改为"DDL 才清"(PRAGMA/ DML 不影响 schema),热路径 stmt cache 保留。 examples/重构:移除自造 expect mock,引入assertEq / assertTrue / expectThrows / expectInstanceOf标准断言;PG 示例补嵌套 SAVEPOINT 测试与 PG 类型(number/string)兼容。- 测试覆盖:新增
utils.test.ts(isSafeIdent / translatePlaceholders / 错误体系)、postgresql.test.ts(9 个 PG mock 测试覆盖嵌套 + 归位)、sqlite-cache.test.ts(LRU 容量上限 + 精确清 cache)。 tsup.config.ts清理:移除 60+ 行冗余 console.log、移除node:*内置模块手工 external(tsup 在 platform=node 下自动 external)、删除any返回类型。vitest.config.ts修正:coverage.include路径从src/tool/**(不存在)改为src/**;setupFiles路径从tests/setup.ts(不被 include)改为tests/__tests__/global.setup.ts(避开 test include)。
许可
MIT © houjallen / EASBot
