npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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.

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,底层方言差异被屏蔽
  • 同步 facadeSyncSqliteConnection 暴露 better-sqlite3 形态的同步 API(exec / prepare / pragma / transaction / getJournalMode / ...),给同步热路径使用(Tree-sitter / 本地 embedding / 主线程 SQLite 调用)
  • 后端可插拔:SQLite 三大 backend 自动探测(better-sqlite3node: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/codebaseDatabaseManager 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;中途抛错自动 rollback

3. 切换 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/skillseasbot 主 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)

PostgresDatabasetranslatePlaceholders 结果按 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 SQLitebegin() 立即抛 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 translatePlaceholders LRU 缓存:避免每次 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