@easbot/database 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 houjallen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.en.md ADDED
@@ -0,0 +1,261 @@
1
+ [English](./README.en.md) | [中文](./README.md)
2
+
3
+ # @easbot/database
4
+
5
+ EASBot's database abstraction layer: hides backend differences behind a single `DatabaseInterface`, ships SQLite (with three swappable backends: `better-sqlite3` / `node:sqlite` / `@tursodatabase/database`) and PostgreSQL implementations, and provides first-class nested transactions (SAVEPOINT), parameter placeholder translation, and a coherent error taxonomy.
6
+
7
+ Application code should reach the database through `DatabaseFactory` and build domain DAOs (Agent / Task / Memory persistence) on top — those concerns do not belong to this package.
8
+
9
+ ## Features
10
+
11
+ - **Single interface**: `DatabaseInterface` covers `query / queryOne / execute / exec / execBatch / begin / withTransaction`; dialect specifics stay hidden.
12
+ - **Pluggable backends**: SQLite auto-detects one of three backends and falls through gracefully; PostgreSQL uses a `pg` pool.
13
+ - **Unified `?` placeholder**: `translatePlaceholders(sql, 'numbered')` rewrites to `$1, $2 …` so callers don't care about dialect.
14
+ - **Nested transactions**: re-entrant `withTransaction` calls on the same connection use `SAVEPOINT sp_<N>`; outermost owns `BEGIN/COMMIT/ROLLBACK`; inner handles only carry state.
15
+ - **Auto rollback**: `withTransaction(fn)` rolls back on callback throw; double-commit / double-rollback on the same handle is idempotent; readonly mode rejects `begin()`.
16
+ - **Error taxonomy**: `DatabaseError` root + `ConnectionError / QueryError / TransactionError` subclasses with preserved `cause` chains.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ pnpm add @easbot/database
22
+ ```
23
+
24
+ Or as a workspace dependency:
25
+
26
+ ```json
27
+ {
28
+ "dependencies": {
29
+ "@easbot/database": "workspace:*"
30
+ }
31
+ }
32
+ ```
33
+
34
+ Note: `better-sqlite3` is the default backend, declared as an `optional peerDependency`. If your runtime only has `node:sqlite` (Node 22.5+ builtin), leave `better-sqlite3` out and the resolver falls back automatically.
35
+
36
+ ## Usage
37
+
38
+ ### 1. SQLite via `DatabaseFactory`
39
+
40
+ ```typescript
41
+ import { DatabaseFactory } from '@easbot/database';
42
+
43
+ // backend: 'auto' | 'better-sqlite3' | 'node:sqlite' | '@tursodatabase/database'
44
+ const db = DatabaseFactory.create({
45
+ flavor: 'sqlite',
46
+ sqlite: {
47
+ path: 'app.db',
48
+ backend: 'auto',
49
+ walMode: true,
50
+ foreignKeys: true,
51
+ },
52
+ });
53
+ await db.initialize();
54
+
55
+ // Always use `?` placeholders
56
+ const users = await db.query<{ id: string; name: string }>(
57
+ 'SELECT id, name FROM users WHERE status = ? ORDER BY name',
58
+ ['active'],
59
+ );
60
+ ```
61
+
62
+ ### 2. Nested transactions + auto rollback
63
+
64
+ ```typescript
65
+ await db.withTransaction(async (tx) => {
66
+ await tx.execute('UPDATE accounts SET balance = balance - ? WHERE id = ?', [100, 'alice']);
67
+ await tx.execute('UPDATE accounts SET balance = balance + ? WHERE id = ?', [100, 'bob']);
68
+
69
+ // Nested: SAVEPOINT sp_2; failure only rolls back the inner scope
70
+ try {
71
+ await db.withTransaction(async (inner) => {
72
+ await inner.execute('INSERT INTO audit_log (...) VALUES (...)', [...]);
73
+ throw new Error('rollback me');
74
+ });
75
+ } catch {
76
+ /* ignore — outer tx still alive */
77
+ }
78
+
79
+ // Explicit SAVEPOINT for finer-grained control
80
+ await tx.savepoint('checkpoint_1');
81
+ /* … */
82
+ await tx.rollbackTo('checkpoint_1');
83
+ await tx.release('checkpoint_1');
84
+ });
85
+ // Exiting the outermost withTransaction auto-commits; throwing auto-rolls-back
86
+ ```
87
+
88
+ ### 3. Switch to PostgreSQL
89
+
90
+ ```typescript
91
+ import { DatabaseFactory } from '@easbot/database';
92
+
93
+ const pg = DatabaseFactory.create({
94
+ flavor: 'postgresql',
95
+ host: '127.0.0.1',
96
+ port: 5432,
97
+ database: 'app',
98
+ username: 'app',
99
+ password: '***',
100
+ ssl: false,
101
+ poolSize: 10,
102
+ });
103
+ await pg.initialize();
104
+
105
+ // Always `?` at the application layer; the backend rewrites it
106
+ await pg.query('SELECT * FROM users WHERE id = ?', [42]);
107
+ ```
108
+
109
+ ### 4. Build domain DAOs on top
110
+
111
+ The abstraction exposes only "database capabilities". Domain entities (Agent / Task / Memory schemas, JSON serialisation) are an application concern:
112
+
113
+ ```typescript
114
+ import type { DatabaseInterface } from '@easbot/database';
115
+
116
+ interface Widget {
117
+ id: string;
118
+ name: string;
119
+ color: string;
120
+ }
121
+
122
+ class WidgetDao {
123
+ constructor(private readonly db: DatabaseInterface) {}
124
+
125
+ async create(w: Widget) {
126
+ await this.db.execute('INSERT INTO widgets (id, name, color) VALUES (?, ?, ?)', [
127
+ w.id,
128
+ w.name,
129
+ w.color,
130
+ ]);
131
+ }
132
+
133
+ async findById(id: string): Promise<Widget | null> {
134
+ const rows = await this.db.query<{ id: string; name: string; color: string }>(
135
+ 'SELECT id, name, color FROM widgets WHERE id = ?',
136
+ [id],
137
+ );
138
+ return rows[0] ?? null;
139
+ }
140
+ }
141
+ ```
142
+
143
+ ## Backend selection strategy
144
+
145
+ | backend | Performance | When to use |
146
+ |---|---|---|
147
+ | `better-sqlite3` | Best (validated by the sqg.dev benchmark) | Default; requires a `node-gyp` build. |
148
+ | `node:sqlite` | 10–20 % slower than better-sqlite3 | Node 22.5+ builtin; zero native dependency; ideal when cross-platform builds fail. |
149
+ | `@tursodatabase/database` | Mid (Turso historically lags) | Embedded / long-term fallback; optional dependency. |
150
+
151
+ `DatabaseFactory.create({ flavor: 'sqlite', sqlite: { backend: 'auto' } })` runs the resolver in the order above; passing an explicit `backend` skips detection.
152
+
153
+ ## Error codes
154
+
155
+ | code | class | meaning |
156
+ |---|---|---|
157
+ | `CONNECTION` | `ConnectionError` | backend create / init failure (WAL failure, missing native binding, etc.) |
158
+ | `QUERY` | `QueryError` | SQL execution failure (syntax, missing column, type mismatch) |
159
+ | `TX` | `TransactionError` | transaction state-machine error (double-finalise, readonly begin failure, SAVEPOINT failure) |
160
+
161
+ Error instances carry the original driver exception via the `cause` chain, suitable for upstream aggregation.
162
+
163
+ ## Out of scope
164
+
165
+ - Domain entity persistence (Agent / Task / Memory / Knowledge Graph) — owned by each domain package.
166
+ - MCP / Skill exposure — owned by `@easbot/mcp`, `@easbot/skills`, and the `easbot` main CLI.
167
+ - Vector DB interfaces — placeholder stubs; tracked for future implementation.
168
+
169
+ ## CLI
170
+
171
+ This package exposes no standalone CLI; integrate via the public API instead.
172
+
173
+ ## Performance optimizations
174
+
175
+ ### 1. Prepared statement LRU (SQLite)
176
+
177
+ `SqliteDatabase` maintains a 256-capacity LRU cache that skips tokenize + parse for repeated SQL:
178
+
179
+ - Hot path: every repeated SQL is 0-cost;
180
+ - Capacity full: evict the least-recently-used entry;
181
+ - Invalidation: only DDL (`CREATE/DROP/ALTER/TRUNCATE/REPLACE`) clears the cache; regular DML and `PRAGMA` keep it intact.
182
+
183
+ ### 2. Placeholder translation cache (PostgreSQL)
184
+
185
+ `PostgresDatabase` caches `translatePlaceholders` results in a 256-capacity LRU keyed by SQL string, avoiding `String.replace + closure` on every query:
186
+
187
+ - Placeholder-less SQL short-circuits (returns original SQL, passes `undefined` to pg to suppress noisy warnings);
188
+ - Placeholder-bearing SQL hits the cache and skips the scan.
189
+
190
+ ### 3. `execBatch` single round-trip (PostgreSQL)
191
+
192
+ `execBatch` joins multiple statements with `;` and sends them in a single `client.query()` call, saving N-1 network round-trips vs. looping. SQLite stays on per-statement `backend.exec()` since its driver is synchronous and has no network gain.
193
+
194
+ ## Nested transaction semantics (SQLite ⇄ PostgreSQL)
195
+
196
+ Both backends are observationally identical to the application layer:
197
+
198
+ | Depth | SQLite | PostgreSQL |
199
+ |---|---|---|
200
+ | `begin()` depth=1 | `BEGIN` | `BEGIN` (dedicated client) |
201
+ | `begin()` depth>1 | `SAVEPOINT sp_<N>` | `SAVEPOINT sp_<N>` (reuses outer client) |
202
+ | Inner `commit()` | state cleanup only | `RELEASE SAVEPOINT sp_<N>` |
203
+ | Inner `rollback()` | `ROLLBACK TO SAVEPOINT sp_<N>` | `ROLLBACK TO SAVEPOINT sp_<N>` |
204
+ | Outer `commit()` | `COMMIT` | `COMMIT` + release client |
205
+ | Outer `rollback()` | `ROLLBACK` | `ROLLBACK` + release client |
206
+ | After inner failure | inner SQL undone, outer unaffected | same |
207
+
208
+ The application never sees the dialect.
209
+
210
+ ## Edge cases & error handling
211
+
212
+ - **Call before `initialize()`**: `query / execute / exec / begin` all throw `ConnectionError`.
213
+ - **Call after `close()`**: same; internal state is reset.
214
+ - **readonly SQLite**: `begin()` throws `TransactionError` immediately (clearer than the raw SQLite message).
215
+ - **PG `pool.connect()` failure**: `begin()` throws `TransactionError` AND rolls `lastTxDepth` back, so the next `begin()` starts at depth=1.
216
+ - **Double commit/rollback on same handle**: idempotent guard via `finished` flag, no extra SQL.
217
+ - **Query/savepoint after finalize**: throws `TransactionError`.
218
+ - **Unsafe SAVEPOINT / table names** (non-ASCII, injection payloads): `isSafeIdent` rejects, `TransactionError` blocks the concatenation.
219
+
220
+ ## Test matrix
221
+
222
+ | Suite | Coverage | File |
223
+ |---|---|---|
224
+ | `sqlite-database.test.ts` | backend probe / query / execute / connection / LRU / DDL invalidation | `tests/__tests__/` |
225
+ | `transaction.test.ts` | basic / idempotent / nested SAVEPOINT / explicit savepoint / readonly | `tests/__tests__/` |
226
+ | `utils.test.ts` | `isSafeIdent` / `translatePlaceholders` / `DatabaseError` taxonomy | `tests/__tests__/` |
227
+ | `postgresql.test.ts` | placeholder cache / nested SAVEPOINT / handle idempotency / error taxonomy / connect-fail rollback (mocked `pg`) | `tests/__tests__/` |
228
+ | `sqlite-cache.test.ts` | LRU capacity eviction / precise cache invalidation / readonly boundary | `tests/__tests__/` |
229
+ | `backends.test.ts` | backend probe / explicit selection / application-layer DAO pattern | `tests/__tests__/` |
230
+ | `examples/test-database.ts` | end-to-end smoke (35 assertions) | `examples/` |
231
+ | `examples/test-pgsql.ts` | PG end-to-end (19 assertions, including nested SAVEPOINT) | `examples/` |
232
+
233
+ Run:
234
+
235
+ ```bash
236
+ pnpm test:run # 66 vitest tests
237
+ node --import tsx examples/test-database.ts # SQLite 35/35
238
+ node --import tsx examples/test-pgsql.ts # PG 19/19 (requires docker compose up -d pg)
239
+ ```
240
+
241
+ ## Changelog
242
+
243
+ ### 0.4.x (current)
244
+
245
+ Fixes:
246
+
247
+ - **PG nested SAVEPOINT semantics**: inner `begin()` now actually issues `SAVEPOINT sp_<N>`; inner commit issues `RELEASE SAVEPOINT sp_<N>`; rollback issues `ROLLBACK TO SAVEPOINT sp_<N>`. Fully aligned with SQLite (previous behavior was broken — inner begin/commit were no-ops, breaking isolation).
248
+ - **PG `pool.connect()` failure rolls `lastTxDepth` back**: previously, a failed begin left the depth counter dirty, corrupting the next transaction.
249
+ - **PG `client.release(destroy=true)` on BEGIN failure**: damaged clients are removed from the pool instead of being returned.
250
+ - **PG `close()` propagates errors**: previously swallowed `pool.end()` errors; now throws `ConnectionError`.
251
+ - **PG `translatePlaceholders` LRU cache**: avoids re-scanning SQL on every query; placeholder-less SQL passes `undefined` to pg.
252
+ - **PG `execBatch` single round-trip**: semicolon-joined statements sent in one `client.query()`.
253
+ - **SQLite `exec()` precise cache invalidation**: changed from "clear on every exec" to "clear only on DDL" (`PRAGMA` / DML leave the cache intact).
254
+ - **`examples/` refactor**: replaced the bespoke `expect` mock with standard `assertEq / assertTrue / expectThrows / expectInstanceOf`; PG example now exercises nested SAVEPOINT and is type-tolerant (number/string).
255
+ - **Test coverage**: new `utils.test.ts` (isSafeIdent / translatePlaceholders / error taxonomy), `postgresql.test.ts` (9 mocked PG tests for nested + rollback), `sqlite-cache.test.ts` (LRU capacity + precise invalidation).
256
+ - **`tsup.config.ts` cleanup**: removed 60+ lines of debug `console.log`, removed manual `node:*` externalization (tsup auto-externalizes under `platform: 'node'`), removed `any` return type.
257
+ - **`vitest.config.ts` fixes**: `coverage.include` changed from nonexistent `src/tool/**` to `src/**`; `setupFiles` moved from `tests/setup.ts` (not under include) to `tests/__tests__/global.setup.ts`.
258
+
259
+ ## License
260
+
261
+ MIT © houjallen / EASBot
package/README.md ADDED
@@ -0,0 +1,313 @@
1
+ [English](./README.en.md) | 中文
2
+
3
+ # @easbot/database
4
+
5
+ EASBot 数据库抽象层:在统一 `DatabaseInterface` 之后屏蔽后端差异,提供 SQLite(better-sqlite3 / node:sqlite / @tursodatabase/database 三选一)与 PostgreSQL 双方言实现,并内置嵌套事务(SAVEPOINT)、参数占位符翻译、错误码体系。
6
+
7
+ > 详细架构 / 后端选择策略 / 错误码 / 性能预算 / 单测基线见项目发布文档 [`docs/components/database.md`](https://github.com/houjallen/easbot/blob/main/docs/components/database.md)。包内 `docs/EAS_DATABASE_DESIGN.md` 为历史私有副本,新信息请以上述链接为准。
8
+
9
+ 应用层应当通过 `DatabaseFactory` 获取连接,再基于 `DatabaseInterface` 自建业务 DAO(Agent / Task / Memory 等实体的持久化不归本包)。
10
+
11
+ ## 特性
12
+
13
+ - **统一接口**:`DatabaseInterface` 覆盖 `query / queryOne / execute / exec / execBatch / begin / withTransaction`,底层方言差异被屏蔽
14
+ - **同步 facade**:`SyncSqliteConnection` 暴露 `better-sqlite3` 形态的同步 API(`exec / prepare / pragma / transaction / getJournalMode / ...`),给同步热路径使用(Tree-sitter / 本地 embedding / 主线程 SQLite 调用)
15
+ - **后端可插拔**:SQLite 三大 backend 自动探测(`better-sqlite3` → `node:sqlite` → `@tursodatabase/database`),并支持显式指定;PostgreSQL 通过 `pg` 池化连接
16
+ - **占位符统一为 `?`**:内置 `translatePlaceholders(sql, 'numbered')` 把 `?` 翻译为 `$1, $2 ...`,兼容 pg 等 numbered 占位符方言
17
+ - **嵌套事务**:同一连接上 `withTransaction` 嵌套调用走 `SAVEPOINT sp_<N>`;最外层负责 `BEGIN/COMMIT/ROLLBACK`,内层句柄仅做状态机
18
+ - **自动兜底**:`withTransaction(fn)` 在 callback 抛错时自动 rollback;同一 handle 二次 commit / rollback 幂等;readonly 数据库拒绝 begin
19
+ - **错误体系**:`DatabaseError` 根类 + `ConnectionError / QueryError / TransactionError` 子类,cause 链保留驱动层原始异常
20
+
21
+ ## 两种接入模式
22
+
23
+ ### 模式 A:异步 facade(`DatabaseFactory` / `DatabaseInterface`)
24
+
25
+ 适用于异步调用栈——MCP server / 跨网络 / web handler:
26
+
27
+ ```typescript
28
+ import { DatabaseFactory } from '@easbot/database';
29
+
30
+ const db = DatabaseFactory.create({
31
+ flavor: 'sqlite',
32
+ sqlite: { path: 'app.db', backend: 'auto', walMode: true },
33
+ });
34
+ await db.initialize();
35
+ const rows = await db.query<{ id: string }>('SELECT id FROM users WHERE status = ?', ['active']);
36
+ ```
37
+
38
+ ### 模式 B:同步 facade(`SyncSqliteConnection`)—— **新**
39
+
40
+ 适用于同步热路径——Tree-sitter WASM 解析 / 本地 embedding 推理 / 主线程 SQLite 调用:
41
+
42
+ ```typescript
43
+ import { createSyncSqliteConnection } from '@easbot/database';
44
+
45
+ const conn = createSyncSqliteConnection({
46
+ path: 'app.db',
47
+ backend: 'better-sqlite3', // 或 'node-sqlite' / '@tursodatabase/database'
48
+ walMode: true,
49
+ });
50
+ conn.initialize();
51
+
52
+ // 完全同步 API,对标 better-sqlite3 / codegraph SqliteDatabase
53
+ const stmt = conn.prepare('SELECT id FROM users WHERE status = ?');
54
+ const rows = stmt.all('active');
55
+ conn.close();
56
+ ```
57
+
58
+ **典型应用**:`packages/codebase` 的 `DatabaseManager` 60+ 同步 DB 调用点通过 `SyncSqliteConnection` 接入 `@easbot/database`,API 形态完全兼容 `better-sqlite3` 同步 binding,**零改动**。详见决策 [`0036-storage-backend.md`](file:///e:/work/apps/eas/easbot/docs/decisions/0036-storage-backend.md)。
59
+
60
+ ## backend 与 facade 选择
61
+
62
+ | 场景 | 推荐 facade | 推荐 backend |
63
+ |---|---|---|
64
+ | 同步热路径(Tree-sitter / 本地推理 / 主线程 SQLite) | `SyncSqliteConnection` | `better-sqlite3` |
65
+ | MCP server / 异步 CLI | `DatabaseInterface`(async) | `better-sqlite3` |
66
+ | cross-platform 构建失败回退 | 任意 | `node:sqlite`(Node 22.5+ 内置) |
67
+ | 嵌入式 / 远期 Turso 兼容 | 任意 | `@tursodatabase/database` |
68
+ | PostgreSQL 切换 | `DatabaseInterface`(async) | `pg` |
69
+
70
+ ## 安装
71
+
72
+ ```bash
73
+ pnpm add @easbot/database
74
+ ```
75
+
76
+ 或作为 monorepo 工作区依赖:
77
+
78
+ ```json
79
+ {
80
+ "dependencies": {
81
+ "@easbot/database": "workspace:*"
82
+ }
83
+ }
84
+ ```
85
+
86
+ 注意:`better-sqlite3` 是默认 backend,作为 `optional peerDependency`。如果运行环境只能使用 `node:sqlite`(Node 22.5+ 内置),可让 `better-sqlite3` 不安装;resolver 会自动降级。
87
+
88
+ ## 使用方式
89
+
90
+ ### 1. 通过 `DatabaseFactory` 创建 SQLite 实例
91
+
92
+ ```typescript
93
+ import { DatabaseFactory } from '@easbot/database';
94
+
95
+ // backend: 'auto' | 'better-sqlite3' | 'node:sqlite' | '@tursodatabase/database'
96
+ const db = DatabaseFactory.create({
97
+ flavor: 'sqlite',
98
+ sqlite: {
99
+ path: 'app.db',
100
+ backend: 'auto',
101
+ walMode: true,
102
+ foreignKeys: true,
103
+ },
104
+ });
105
+ await db.initialize();
106
+
107
+ // 统一 ? 占位符(SQLite / PostgreSQL 通用)
108
+ const users = await db.query<{ id: string; name: string }>(
109
+ 'SELECT id, name FROM users WHERE status = ? ORDER BY name',
110
+ ['active'],
111
+ );
112
+ ```
113
+
114
+ ### 2. 嵌套事务 + 自动回滚
115
+
116
+ ```typescript
117
+ await db.withTransaction(async (tx) => {
118
+ await tx.execute('UPDATE accounts SET balance = balance - ? WHERE id = ?', [100, 'alice']);
119
+ await tx.execute('UPDATE accounts SET balance = balance + ? WHERE id = ?', [100, 'bob']);
120
+
121
+ // 嵌套:SAVEPOINT sp_2;失败仅回滚内层
122
+ try {
123
+ await db.withTransaction(async (inner) => {
124
+ await inner.execute('INSERT INTO audit_log (...) VALUES (...)', [...]);
125
+ throw new Error('rollback me');
126
+ });
127
+ } catch {
128
+ /* ignore — outer tx still alive */
129
+ }
130
+
131
+ // 显式 SAVEPOINT(控制粒度更细)
132
+ await tx.savepoint('checkpoint_1');
133
+ /* ... */
134
+ await tx.rollbackTo('checkpoint_1');
135
+ await tx.release('checkpoint_1');
136
+ });
137
+ // 退出最外层 withTransaction 自动 commit;中途抛错自动 rollback
138
+ ```
139
+
140
+ ### 3. 切换 PostgreSQL
141
+
142
+ ```typescript
143
+ import { DatabaseFactory, translatePlaceholders } from '@easbot/database';
144
+
145
+ const pg = DatabaseFactory.create({
146
+ flavor: 'postgresql',
147
+ host: '127.0.0.1',
148
+ port: 5432,
149
+ database: 'app',
150
+ username: 'app',
151
+ password: '***',
152
+ ssl: false,
153
+ poolSize: 10,
154
+ });
155
+ await pg.initialize();
156
+
157
+ // 应用层始终使用 `?`;后端自动翻译为 `$1, $2 ...`
158
+ await pg.query('SELECT * FROM users WHERE id = ?', [42]);
159
+ ```
160
+
161
+ ### 4. 应用层自建业务 DAO(不属于本包)
162
+
163
+ 数据库抽象只暴露"数据库能力"。业务实体(如 Agent / Task / Memory)的 schema、字段映射、JSON 序列化策略属于上层职责——应用层应自己基于 `DatabaseInterface` 写 DAO,例如:
164
+
165
+ ```typescript
166
+ import type { DatabaseInterface } from '@easbot/database';
167
+
168
+ interface Widget {
169
+ id: string;
170
+ name: string;
171
+ color: string;
172
+ }
173
+
174
+ class WidgetDao {
175
+ constructor(private readonly db: DatabaseInterface) {}
176
+
177
+ async create(w: Widget) {
178
+ await this.db.execute('INSERT INTO widgets (id, name, color) VALUES (?, ?, ?)', [
179
+ w.id,
180
+ w.name,
181
+ w.color,
182
+ ]);
183
+ }
184
+
185
+ async findById(id: string): Promise<Widget | null> {
186
+ const rows = await this.db.query<{ id: string; name: string; color: string }>(
187
+ 'SELECT id, name, color FROM widgets WHERE id = ?',
188
+ [id],
189
+ );
190
+ return rows[0] ?? null;
191
+ }
192
+ }
193
+ ```
194
+
195
+ ## 后端 backend 选择策略
196
+
197
+ | backend | 性能 | 何时适用 |
198
+ |---|---|---|
199
+ | `better-sqlite3` | ✅ 最佳(sqg.dev 基准验证) | 默认;要求 `node-gyp` 构建 |
200
+ | `node:sqlite` | 次之(比 better-sqlite3 慢 10–20%) | Node 22.5+ 内置;零原生依赖;适合 cross-platform 构建失败场景 |
201
+ | `@tursodatabase/database` | 中(Turso 历史性能较差) | 嵌入式 / 远期兜底;可选依赖 |
202
+
203
+ `DatabaseFactory.create({ flavor: 'sqlite', sqlite: { backend: 'auto' } })` 时,resolver 按上表顺序探测;显式指定 backend 跳过探测。
204
+
205
+ ## 错误码
206
+
207
+ | code | 类 | 含义 |
208
+ |---|---|---|
209
+ | `CONNECTION` | `ConnectionError` | backend 创建 / 初始化失败(WAL 失败、native binding 缺失等) |
210
+ | `QUERY` | `QueryError` | SQL 执行失败(语法错、列不存在、类型不匹配) |
211
+ | `TX` | `TransactionError` | 事务状态机错误(二次操作、readonly begin 失败、SAVEPOINT 抛错) |
212
+
213
+ 错误实例保留 `cause` 链,可向上层聚合时透出原始驱动抛错。
214
+
215
+ ## 不在范围内
216
+
217
+ - 业务实体持久化(Agent / Task / Memory / Knowledge Graph 等)—— 由各自领域包提供
218
+ - MCP / Skill 暴露—— 由 `@easbot/mcp`、`@easbot/skills`、`easbot` 主 CLI 承载
219
+ - 向量数据库接口—— 占位 stub 未实装,留作将来
220
+
221
+ ## CLI
222
+
223
+ 本包不暴露独立 CLI;调用方通过 `DatabaseFactory` / `DatabaseInterface` API 集成。
224
+
225
+ ## 性能优化
226
+
227
+ ### 1. prepared statement LRU(SQLite)
228
+
229
+ `SqliteDatabase` 内部维护一个容量 256 的 LRU 缓存,命中已 prepare 过的 SQL 跳过 tokenize + parse 阶段:
230
+
231
+ - 热路径:所有重复 SQL 0-cost 命中;
232
+ - 容量满:按 LRU 淘汰最久未访问;
233
+ - 失效策略:仅在检测到 DDL(`CREATE/DROP/ALTER/TRUNCATE/REPLACE`)时清空,普通 DML 与 `PRAGMA` 不影响 cache。
234
+
235
+ ### 2. 占位符翻译缓存(PostgreSQL)
236
+
237
+ `PostgresDatabase` 对 `translatePlaceholders` 结果按 SQL 字符串做 256 容量 LRU 缓存,避免每次 query 都扫描 SQL 替换 `?`:
238
+
239
+ - 无占位符 SQL 一次性 short-circuit(直接返回原 SQL,params 传 `undefined` 避免 pg 警告噪音);
240
+ - 有占位符 SQL:命中即跳过 `String.replace + closure` 计数。
241
+
242
+ ### 3. `execBatch` 单 round-trip(PostgreSQL)
243
+
244
+ PG 上 `execBatch` 用 `;` 分号拼接多条 statement 一次性发送,由 PG 自身批量执行;相比逐条 `client.query()` 节省 N-1 次网络往返。SQLite 仍走逐条 `backend.exec()`,因底层是同步 API 不存在网络收益。
245
+
246
+ ## 嵌套事务语义对齐(SQLite ⇄ PostgreSQL)
247
+
248
+ 两条路径在事务行为上完全一致:
249
+
250
+ | 深度 | SQLite | PostgreSQL |
251
+ |---|---|---|
252
+ | `begin()` depth=1 | `BEGIN` | `BEGIN`(独占 client) |
253
+ | `begin()` depth>1 | `SAVEPOINT sp_<N>` | `SAVEPOINT sp_<N>`(复用最外层 client) |
254
+ | 内层 `commit()` | 仅清理状态 | `RELEASE SAVEPOINT sp_<N>` |
255
+ | 内层 `rollback()` | `ROLLBACK TO SAVEPOINT sp_<N>` | `ROLLBACK TO SAVEPOINT sp_<N>` |
256
+ | 外层 `commit()` | `COMMIT` | `COMMIT` + 释放 client |
257
+ | 外层 `rollback()` | `ROLLBACK` | `ROLLBACK` + 释放 client |
258
+ | 嵌套失败后 | 内层 SQL 撤回,外层不受影响 | 同左 |
259
+
260
+ 应用层无需关心方言;`db.withTransaction` 嵌套调用行为完全一致。
261
+
262
+ ## 边界与错误处理
263
+
264
+ - **未 `initialize()` 调用**:`query / execute / exec / begin` 全部抛 `ConnectionError`;
265
+ - **`close()` 后再调用**:同上抛 `ConnectionError`,内部状态全部重置;
266
+ - **readonly SQLite**:`begin()` 立即抛 `TransactionError`,避免 SQLite 原生报错信息含糊;
267
+ - **PG `pool.connect()` 失败**:`begin()` 抛 `TransactionError`,且 `lastTxDepth` 归位,下次 `begin()` 从 depth=1 重新开始(不污染计数);
268
+ - **同一 handle 二次 commit / rollback**:幂等守门(`finished` 标记),不重复发 SQL;
269
+ - **finalize 后再 query / savepoint**:抛 `TransactionError`,提示事务已终结;
270
+ - **SAVEPOINT / 表名包含非 ASCII / 注入字符**:`isSafeIdent` 拒绝,抛 `TransactionError` 阻断拼接。
271
+
272
+ ## 测试矩阵
273
+
274
+ | 测试套件 | 覆盖 | 文件 |
275
+ |---|---|---|
276
+ | `sqlite-database.test.ts` | backend 探测 / query / execute / 连接管理 / LRU cache / DDL 失效 | `tests/__tests__/` |
277
+ | `transaction.test.ts` | basic / 幂等 / 嵌套 SAVEPOINT / 显式 savepoint / readonly 守卫 | `tests/__tests__/` |
278
+ | `utils.test.ts` | `isSafeIdent` / `translatePlaceholders` / `DatabaseError` 体系 | `tests/__tests__/` |
279
+ | `postgresql.test.ts` | 占位符翻译缓存 / 嵌套 SAVEPOINT / 句柄幂等 / 错误分类 / connect 失败归位(用 `pg` 模块桩) | `tests/__tests__/` |
280
+ | `sqlite-cache.test.ts` | LRU 容量上限淘汰 / exec 精确清 cache / readonly 边界 | `tests/__tests__/` |
281
+ | `backends.test.ts` | backend 探测 / 显式选择 / 应用层 DAO 模式 | `tests/__tests__/` |
282
+ | `examples/test-database.ts` | 端到端冒烟(35 assertions) | `examples/` |
283
+ | `examples/test-pgsql.ts` | PG 端到端(19 assertions,含嵌套 SAVEPOINT) | `examples/` |
284
+
285
+ 运行:
286
+
287
+ ```bash
288
+ pnpm test:run # 66 vitest tests
289
+ node --import tsx examples/test-database.ts # SQLite 35/35
290
+ node --import tsx examples/test-pgsql.ts # PG 19/19 (需 docker compose up -d pg)
291
+ ```
292
+
293
+ ## 版本变更
294
+
295
+ ### 0.4.x(本版本)
296
+
297
+ 修复:
298
+
299
+ - **PG 嵌套事务 SAVEPOINT 语义**:内层 `begin()` 现在真发 `SAVEPOINT sp_<N>`;内层 commit 发 `RELEASE SAVEPOINT sp_<N>`;rollback 发 `ROLLBACK TO SAVEPOINT sp_<N>`。与 SQLite 路径行为完全对齐(之前实现只发 `BEGIN/COMMIT`,嵌套语义错乱)。
300
+ - **PG `pool.connect()` 失败归位 `lastTxDepth`**:begin 失败时正确归位计数,避免下次 begin 从脏 depth 开始。
301
+ - **PG `client.release(destroy=true)`**:BEGIN 失败时销毁损坏 client,避免脏连接回流 pool。
302
+ - **PG `close()` 错误传播**:之前 `pool.end()` 抛错被 silently swallow,现在抛 `ConnectionError`。
303
+ - **PG `translatePlaceholders` LRU 缓存**:避免每次 query 重扫 SQL;无占位符 SQL 传 `undefined` 避免 pg 噪音警告。
304
+ - **PG `execBatch` 单 round-trip**:分号拼接多 statement 一次发送,节省网络往返。
305
+ - **SQLite `exec()` 精确清 cache**:从"任何 exec 都清"改为"DDL 才清"(`PRAGMA` / DML 不影响 schema),热路径 stmt cache 保留。
306
+ - **`examples/` 重构**:移除自造 expect mock,引入 `assertEq / assertTrue / expectThrows / expectInstanceOf` 标准断言;PG 示例补嵌套 SAVEPOINT 测试与 PG 类型(number/string)兼容。
307
+ - **测试覆盖**:新增 `utils.test.ts`(isSafeIdent / translatePlaceholders / 错误体系)、`postgresql.test.ts`(9 个 PG mock 测试覆盖嵌套 + 归位)、`sqlite-cache.test.ts`(LRU 容量上限 + 精确清 cache)。
308
+ - **`tsup.config.ts` 清理**:移除 60+ 行冗余 console.log、移除 `node:*` 内置模块手工 external(tsup 在 platform=node 下自动 external)、删除 `any` 返回类型。
309
+ - **`vitest.config.ts` 修正**:`coverage.include` 路径从 `src/tool/**`(不存在)改为 `src/**`;`setupFiles` 路径从 `tests/setup.ts`(不被 include)改为 `tests/__tests__/global.setup.ts`(避开 test include)。
310
+
311
+ ## 许可
312
+
313
+ MIT © houjallen / EASBot
package/dist/index.cjs ADDED
@@ -0,0 +1,7 @@
1
+ 'use strict';Object.defineProperty(exports,'__esModule',{value:true});var module$1=require('module'),m=require('fs'),h=require('path'),pg=require('pg');function _interopNamespace(e){if(e&&e.__esModule)return e;var n=Object.create(null);if(e){Object.keys(e).forEach(function(k){if(k!=='default'){var d=Object.getOwnPropertyDescriptor(e,k);Object.defineProperty(n,k,d.get?d:{enumerable:true,get:function(){return e[k]}});}})}n.default=e;return Object.freeze(n)}var m__namespace=/*#__PURE__*/_interopNamespace(m);var h__namespace=/*#__PURE__*/_interopNamespace(h);var V=Object.defineProperty;var j=(n,e,t)=>e in n?V(n,e,{enumerable:true,configurable:true,writable:true,value:t}):n[e]=t;var i=(n,e,t)=>j(n,typeof e!="symbol"?e+"":e,t);var U=()=>typeof document>"u"?new URL(`file:${__filename}`).href:document.currentScript&&document.currentScript.tagName.toUpperCase()==="SCRIPT"?document.currentScript.src:new URL("main.js",document.baseURI).href,c=U();var S=class extends Error{constructor(t,r,a){super(r);i(this,"code");i(this,"cause");this.name="DatabaseError",this.code=t,this.cause=a;}},o=class extends S{constructor(e,t){super("CONNECTION",e,t),this.name="ConnectionError";}},d=class extends S{constructor(e,t){super("QUERY",e,t),this.name="QueryError";}},l=class extends S{constructor(e,t){super("TX",e,t),this.name="TransactionError";}},T=(n,e)=>{if(e==="qmark")return n;let t=0;return n.replace(/\?/g,()=>`$${++t}`)},x=n=>/^[A-Za-z_][A-Za-z0-9_]*$/.test(n);var J=module$1.createRequire(c),B=(n,e)=>{try{let t=J(n);return e(t)}catch{return false}},f=()=>{if(B("better-sqlite3",n=>typeof n.default=="function"))return "better-sqlite3";if(B("node:sqlite",n=>typeof n.DatabaseSync=="function"))return "node:sqlite";if(B("@tursodatabase/database",n=>typeof(n.default??n)=="function"))return "@tursodatabase/database";throw new o(`No SQLite backend available. Please install one of:
2
+ - better-sqlite3 (recommended; highest perf)
3
+ - node:sqlite (built-in since Node.js 22.5+)
4
+ - @tursodatabase/database`)};var y=class{constructor(e){i(this,"stmt",e);}run(...e){return this.stmt.run(...e)}get(...e){return this.stmt.get(...e)}all(...e){return this.stmt.all(...e)}iterate(...e){return this.stmt.iterate(...e)}};var X=module$1.createRequire(c),F=n=>typeof n=="function"&&n.prototype!==void 0,w=class{constructor(e,t={}){i(this,"backend","better-sqlite3");i(this,"db");let r=X("better-sqlite3"),a=F(r)?r:r.default;if(!F(a))throw new TypeError(`better-sqlite3 import did not expose a constructor (got ${typeof r})`);this.db=new a(e,t);}exec(e){this.db.exec(e);}prepare(e){return new y(this.db.prepare(e))}pragma(e,t){return this.db.pragma(e,t)}transaction(e){return this.db.transaction(e)}close(){this.db.open&&this.db.close();}get open(){return this.db.open}};var te=module$1.createRequire(c),b=class{constructor(e){i(this,"backend","node:sqlite");i(this,"db");let t=te("node:sqlite");this.db=new t.DatabaseSync(e);}exec(e){this.db.exec(e);}prepare(e){return new y(this.db.prepare(e))}pragma(e,t){return this.db.exec(`PRAGMA ${e}`),t?.simple!==true?void 0:this.db.prepare("SELECT pragma_value AS v FROM pragma_value(?)").get(e)}transaction(e){return((...r)=>{this.db.exec("BEGIN");let a;try{a=e(...r);}catch(s){try{this.db.exec("ROLLBACK");}catch{}throw s}return this.db.exec("COMMIT"),a})}close(){this.db.isOpen&&this.db.close();}get open(){return this.db.isOpen}};var re=module$1.createRequire(c),M=n=>typeof n=="function"&&n.prototype!==void 0,k=class{constructor(e){i(this,"backend","@tursodatabase/database");i(this,"db",null);let t=re("@tursodatabase/database"),r=M(t)?t:t.default;if(!M(r))throw new TypeError(`@tursodatabase/database import did not expose a constructor (got ${typeof t})`);this.db=new r(e);}requireDb(){if(!this.db)throw new Error(`Turso backend not initialized (db=${String(this.db)})`);return this.db}exec(e){this.requireDb().exec(e);}prepare(e){return new y(this.requireDb().prepare(e))}pragma(e){this.requireDb().exec(`PRAGMA ${e}`);}transaction(e){return((...r)=>{let a=this.requireDb();a.exec("BEGIN");let s;try{s=e(...r);}catch(u){try{a.exec("ROLLBACK");}catch{}throw u}return a.exec("COMMIT"),s})}close(){if(this.db)try{this.db.close();}finally{this.db=null;}}get open(){return this.db!==null}};var ae=256,O=class{constructor(e){i(this,"backend",e);i(this,"cache",new Map);}get(e){let t=this.cache.get(e);if(t)return this.cache.delete(e),this.cache.set(e,t),t;let r=this.backend.prepare(e);if(this.cache.size>=ae){let a=this.cache.keys().next().value;a!==void 0&&this.cache.delete(a);}return this.cache.set(e,r),r}clear(){this.cache.clear();}},Q=n=>n==null?{}:typeof n!="object"?{}:{...n},ie=n=>n.map(Q);function E(n){if(!x(n))throw new l(`Unsafe SQL identifier: ${JSON.stringify(n)}`);return `"${n}"`}function se(n){let e=n.trimStart();if(e.length===0)return false;let a=((e.split(`
5
+ `,1)[0]??"").trimStart().split(/\s+/,1)[0]??"").toUpperCase();return a==="CREATE"||a==="DROP"||a==="ALTER"||a==="TRUNCATE"||a==="REPLACE"}var L=class{constructor(e,t){i(this,"db",e);i(this,"depth");i(this,"finished",false);this.depth=t;}isFinished(){return this.finished}async query(e,t){return this.assertOpen(),this.db.query(e,t)}async queryOne(e,t){return this.assertOpen(),this.db.queryOne(e,t)}async execute(e,t){return this.assertOpen(),this.db.execute(e,t)}async exec(e){this.assertOpen(),await this.db.exec(e);}async commit(){this.finished||(this.finished=true,this.depth===1&&this.db.commitRoot(),this.db.popTxDepth());}async rollback(){if(!this.finished){this.finished=true;try{this.depth===1?this.db.rollbackRoot():this.db.rollbackToSavepoint(`sp_${this.depth}`);}catch{}this.db.popTxDepth();}}async savepoint(e){this.assertOpen(),this.db.execSavepoint(E(e));}async release(e){this.assertOpen(),this.db.execRelease(E(e));}async rollbackTo(e){this.assertOpen(),this.db.execRollbackTo(E(e));}assertOpen(){if(this.finished)throw new l(`Transaction already finalized (depth=${this.depth})`)}},R=class{constructor(e){i(this,"config",e);i(this,"flavor","sqlite");i(this,"sqliteBackend");i(this,"backend",null);i(this,"stmtCache",null);i(this,"inTxDepth",0);i(this,"readOnlyFlag",false);i(this,"initialized",false);i(this,"walShmSymlink",null);i(this,"effectiveDbPath",null);let t=e.sqlite??{};this.sqliteBackend=t.backend===void 0||t.backend==="auto"?f():t.backend;}async initialize(){if(this.initialized)return;let e=this.config.sqlite??{},t=e.path??":memory:",r=e.walMode!==false,a=e.foreignKeys!==false,s=e.readonly===true,u=e.busyTimeoutMs??5e3,q=e.walShmPath,_=this.resolveOpenPath(t,q);try{this.backend=this.createBackend(_,{readonly:s});}catch(D){throw this.cleanupWalShmSymlink(),new o(`Failed to create SQLite backend (${this.sqliteBackend}) at "${_}"`,D)}this.readOnlyFlag=s,this.stmtCache=new O(this.backend);try{!s&&t!==":memory:"&&r&&this.backend.exec("PRAGMA journal_mode = WAL"),!s&&a&&this.backend.exec("PRAGMA foreign_keys = ON"),this.backend.exec(`PRAGMA busy_timeout = ${u}`);}catch(D){throw this.backend=null,this.stmtCache=null,this.cleanupWalShmSymlink(),new o("Failed to apply SQLite pragmas",D)}this.initialized=true;}resolveOpenPath(e,t){if(e===":memory:"||t===void 0||t===""||this.sqliteBackend!=="better-sqlite3")return e;let r=h__namespace.dirname(e);if(h__namespace.resolve(r)===h__namespace.resolve(t))return e;let a=h__namespace.basename(e),s=h__namespace.join(t,a);try{try{let u=m__namespace.lstatSync(s);(u.isSymbolicLink()||u.isFile())&&m__namespace.unlinkSync(s);}catch{}return m__namespace.symlinkSync(h__namespace.resolve(e),s),this.walShmSymlink=s,this.effectiveDbPath=s,s}catch(u){throw new o(`Failed to create symlink for WAL at "${t}" \u2192 "${e}". Check directory existence & write permission.`,u)}}cleanupWalShmSymlink(){if(this.walShmSymlink){try{m__namespace.unlinkSync(this.walShmSymlink);}catch{}this.walShmSymlink=null,this.effectiveDbPath=null;}}createBackend(e,t){switch(this.sqliteBackend){case "better-sqlite3":return new w(e,{readonly:t.readonly});case "node:sqlite":return new b(e);case "@tursodatabase/database":return new k(e);default:throw new o(`Unsupported SQLite backend: ${String(this.sqliteBackend)}`)}}async close(){if(!this.backend){this.stmtCache?.clear(),this.stmtCache=null,this.initialized=false,this.cleanupWalShmSymlink();return}try{this.backend.close();}finally{this.backend=null,this.stmtCache?.clear(),this.stmtCache=null,this.inTxDepth=0,this.initialized=false,this.cleanupWalShmSymlink();}}isConnected(){return this.backend?.open===true}isReadOnly(){return this.readOnlyFlag}requireBackend(){if(!this.backend)throw new o("SQLite database not initialized. Call initialize() first.");return this.backend}requireCache(){if(!this.stmtCache)throw new o("SQLite database not initialized. Call initialize() first.");return this.stmtCache}async query(e,t){let r=this.prepareOrThrow(e);try{return ie(r.all(...t??[]))}catch(a){throw new d(`SQLite query failed: ${this.errorMessage(a)}`,a)}}async queryOne(e,t){let r=this.prepareOrThrow(e);try{let a=r.get(...t??[]);return a==null?null:Q(a)}catch(a){throw new d(`SQLite queryOne failed: ${this.errorMessage(a)}`,a)}}async execute(e,t){let r=this.prepareOrThrow(e);try{return r.run(...t??[])}catch(a){throw new d(`SQLite execute failed: ${this.errorMessage(a)}`,a)}}prepareOrThrow(e){try{return this.requireCache().get(e)}catch(t){throw t instanceof o?t:new d(`SQLite prepare failed: ${this.errorMessage(t)}`,t)}}async exec(e){let t=this.requireBackend();try{t.exec(e),se(e)&&this.stmtCache?.clear();}catch(r){throw new d(`SQLite exec failed: ${this.errorMessage(r)}`,r)}}async execBatch(e){for(let t of e)await this.exec(t);}async begin(){let e=this.requireBackend();if(this.readOnlyFlag)throw new l("Cannot begin transaction on read-only SQLite database");let t=this.pushTxDepth();try{t===1?e.exec("BEGIN"):e.exec(`SAVEPOINT sp_${t}`);}catch(r){this.popTxDepth();let a=t===1?"BEGIN":`SAVEPOINT sp_${t}`;throw new l(`${a} failed`,r)}return new L(this,t)}async withTransaction(e){let t=await this.begin();try{let r=await e(t);return await t.commit(),r}catch(r){try{await t.rollback();}catch{}throw r}}getTransactionDepth(){return this.inTxDepth}commitRoot(){this.requireBackend().exec("COMMIT");}rollbackRoot(){this.requireBackend().exec("ROLLBACK");}rollbackToSavepoint(e){this.requireBackend().exec(`ROLLBACK TO SAVEPOINT ${e}`);}execSavepoint(e){this.requireBackend().exec(`SAVEPOINT ${e}`);}execRelease(e){this.requireBackend().exec(`RELEASE SAVEPOINT ${e}`);}execRollbackTo(e){this.requireBackend().exec(`ROLLBACK TO SAVEPOINT ${e}`);}pushTxDepth(){return this.inTxDepth+=1}popTxDepth(){this.inTxDepth>0&&(this.inTxDepth-=1);}errorMessage(e){return e instanceof Error?e.message:String(e)}};var le=256,$=class{constructor(){i(this,"cache",new Map);}get(e){let t=this.cache.get(e);if(t)return this.cache.delete(e),this.cache.set(e,t),t;let r=T(e,"numbered");if(this.cache.size>=le){let a=this.cache.keys().next().value;a!==void 0&&this.cache.delete(a);}return this.cache.set(e,r),r}clear(){this.cache.clear();}},ce=n=>n==null?{}:{...n},de=n=>n.map(e=>ce(e)),I=n=>{if(!x(n))throw new l(`Unsafe SQL identifier: ${JSON.stringify(n)}`);return `"${n}"`};function K(n,e){return n.includes("?")?{sql:T(n,"numbered"),params:e?[...e]:[]}:{sql:n,params:void 0}}var C=class{constructor(e,t){i(this,"db",e);i(this,"depth");i(this,"finished",false);this.depth=t;}get client(){return this.db.getClient(this.depth)}isFinished(){return this.finished}assertOpen(){if(this.finished)throw new l("Transaction already finalized")}async query(e,t){return this.assertOpen(),await W(this.client,e,t)}async queryOne(e,t){let r=await this.query(e,t);return r.length>0?r[0]??null:null}async execute(e,t){return this.assertOpen(),await G(this.client,e,t)}async exec(e){this.assertOpen(),await z(this.client,e);}async commit(){if(!this.finished)if(this.finished=true,this.depth===1)try{await this.client.query("COMMIT");}finally{this.db.releaseTxClient(false),this.db.clearTxCtx();}else await this.client.query(`RELEASE SAVEPOINT sp_${this.depth}`);}async rollback(){if(!this.finished)if(this.finished=true,this.depth===1)try{await this.client.query("ROLLBACK");}catch{}finally{this.db.releaseTxClient(false),this.db.clearTxCtx();}else try{await this.client.query(`ROLLBACK TO SAVEPOINT sp_${this.depth}`);}catch{}}async savepoint(e){this.assertOpen(),await this.client.query(`SAVEPOINT ${I(e)}`);}async release(e){this.assertOpen(),await this.client.query(`RELEASE SAVEPOINT ${I(e)}`);}async rollbackTo(e){this.assertOpen(),await this.client.query(`ROLLBACK TO SAVEPOINT ${I(e)}`);}};async function W(n,e,t){let{sql:r,params:a}=K(e,t);try{let s=await n.query(r,a);return de(s.rows)}catch(s){throw new d(`PostgreSQL query failed: ${s instanceof Error?s.message:String(s)}`,s)}}async function G(n,e,t){let{sql:r,params:a}=K(e,t);try{return {changes:(await n.query(r,a)).rowCount??0}}catch(s){throw new d(`PostgreSQL execute failed: ${s instanceof Error?s.message:String(s)}`,s)}}async function z(n,e){try{await n.query(e);}catch(t){throw new d(`PostgreSQL exec failed: ${t instanceof Error?t.message:String(t)}`,t)}}async function ue(n,e){if(e.length===0)return;let t=e.join(`;
6
+ `)+";";await z(n,t);}var v=class{constructor(e){i(this,"config",e);i(this,"flavor","postgresql");i(this,"sqliteBackend");i(this,"pool",null);i(this,"translatedSqlCache",new $);i(this,"txCtx",null);i(this,"lastTxDepth",0);}async initialize(){if(!this.pool)try{this.pool=new pg.Pool({host:this.config.host??"localhost",port:this.config.port??5432,user:this.config.username??"postgres",password:this.config.password??"",database:this.config.database??"eas_database",ssl:this.config.ssl??!1,max:this.config.poolSize??10}),(await this.pool.connect()).release();}catch(e){throw this.pool=null,new o("Failed to connect to PostgreSQL",e)}}async close(){if(!this.pool)return;let e=this.pool;this.pool=null;try{await e.end();}catch(t){throw new o("Failed to close PostgreSQL pool",t)}finally{this.translatedSqlCache.clear(),this.txCtx=null,this.lastTxDepth=0;}}isConnected(){return this.pool===null?false:!this.pool.ended&&!this.pool.closing}requirePool(){if(!this.pool)throw new o("PostgreSQL database not initialized. Call initialize() first.");return this.pool}async query(e,t){let r=await this.requirePool().connect();try{return await W(r,e,t)}finally{r.release();}}async queryOne(e,t){let r=await this.query(e,t);return r.length>0?r[0]??null:null}async execute(e,t){let r=await this.requirePool().connect();try{return await G(r,e,t)}finally{r.release();}}async exec(e){let t=await this.requirePool().connect();try{await z(t,e);}finally{t.release();}}async execBatch(e){if(e.length===0)return;let t=await this.requirePool().connect();try{await ue(t,e);}finally{t.release();}}async begin(){let e=this.requirePool(),t=this.pushTxDepth();if(t>1){let a=this.requireCtx(t);try{await a.client.query(`SAVEPOINT sp_${t}`);}catch(s){throw this.popTxDepth(),new l(`SAVEPOINT sp_${t} failed`,s)}return new C(this,t)}let r;try{r=await e.connect();}catch(a){throw this.popTxDepth(),new l("BEGIN failed: cannot acquire client",a)}try{await r.query("BEGIN");}catch(a){throw r.release(true),this.popTxDepth(),new l("BEGIN failed",a)}return this.txCtx={client:r,depth:t},new C(this,t)}requireCtx(e){if(!this.txCtx)throw new l(`No active transaction (depth=${e})`);return this.txCtx}getClient(e){return this.requireCtx(e).client}clearTxCtx(){this.txCtx=null,this.lastTxDepth=0,this.translatedSqlCache.clear();}releaseTxClient(e){let t=this.txCtx;if(t)try{t.client.release(e);}catch{}}async withTransaction(e){let t=await this.begin();try{let r=await e(t);return await t.commit(),r}catch(r){try{await t.rollback();}catch{}throw r}}pushTxDepth(){return this.lastTxDepth+=1}popTxDepth(){this.lastTxDepth>0&&(this.lastTxDepth-=1);}getTransactionDepth(){return this.lastTxDepth}};var P=class{static create(e){switch(e.flavor){case "sqlite":return new R(e);case "postgresql":return new v(e);default:{let t=e.flavor;throw new o(`Unsupported database flavor: ${String(t)}`)}}}};var he=256,A=class{constructor(e){i(this,"flavor","sqlite");i(this,"sqliteBackend");i(this,"backend",null);i(this,"stmtCache",null);i(this,"readOnlyFlag",false);i(this,"initialized",false);i(this,"walShmSymlink",null);i(this,"effectiveDbPath",null);i(this,"dbPath");this.sqliteBackend=e.backend===void 0||e.backend==="auto"?f():e.backend,this.dbPath=e.path??":memory:";}resolveBackendPath(e){return e===":memory:"?"":e}initialize(){if(this.initialized)return;let e=this.walModeDefault(),t=this.foreignKeysDefault(),r=this.readonlyDefault(),a=this.busyTimeoutDefault(),s=this.resolveOpenPath(this.dbPath,e?this.walShmPathDefault():void 0),u=s===":memory:"?"":s;try{this.backend=this.createBackend(u,{readonly:r});}catch(q){throw this.cleanupWalShmSymlink(),new o(`Failed to create SQLite backend (${this.sqliteBackend}) at "${s}"`,q)}this.readOnlyFlag=r,this.stmtCache=new N(this.backend);try{!r&&this.dbPath!==":memory:"&&e&&this.backend.exec("PRAGMA journal_mode = WAL"),!r&&t&&this.backend.exec("PRAGMA foreign_keys = ON"),this.backend.exec(`PRAGMA busy_timeout = ${a}`);}catch(q){throw this.backend=null,this.stmtCache=null,this.cleanupWalShmSymlink(),new o("Failed to apply SQLite pragmas",q)}this.initialized=true;}close(){if(!this.backend){this.stmtCache?.clear(),this.stmtCache=null,this.initialized=false,this.cleanupWalShmSymlink();return}try{this.backend.close();}finally{this.backend=null,this.stmtCache?.clear(),this.stmtCache=null,this.readOnlyFlag=false,this.initialized=false,this.cleanupWalShmSymlink();}}isOpen(){return this.backend?.open===true}isReadOnly(){return this.readOnlyFlag}getDbPath(){return this.dbPath}getBackend(){return this.sqliteBackend}getJournalMode(){this.assertInitialized();let e=this.requireBackend().pragma("journal_mode"),t=Array.isArray(e)?e[0]:e;if(t==null)return "";if(typeof t=="object"){let r=t.journal_mode;return typeof r=="string"?r.toLowerCase():String(r??"").toLowerCase()}return String(t).toLowerCase()}prepare(e){this.assertInitialized();try{return this.requireCache().get(e)}catch(t){throw t instanceof o?t:new d(`SQLite prepare failed: ${H(t)}`,t)}}exec(e){this.assertInitialized();let t=this.requireBackend();try{t.exec(e),pe(e)&&this.stmtCache?.clear();}catch(r){throw new d(`SQLite exec failed: ${H(r)}`,r)}}pragma(e,t){return this.assertInitialized(),this.requireBackend().pragma(e,t)}transaction(e){if(this.assertInitialized(),this.readOnlyFlag)throw new l("Cannot begin transaction on read-only SQLite database");return this.requireBackend().transaction(e)}execBatch(e){if(e.length!==0)for(let t of e)this.exec(t);}resolveOpenPath(e,t){if(e===":memory:"||t===void 0||t===""||this.sqliteBackend!=="better-sqlite3")return e;let r=h__namespace.dirname(e);if(h__namespace.resolve(r)===h__namespace.resolve(t))return e;let a=h__namespace.basename(e),s=h__namespace.join(t,a);try{try{let u=m__namespace.lstatSync(s);(u.isSymbolicLink()||u.isFile())&&m__namespace.unlinkSync(s);}catch{}return m__namespace.symlinkSync(h__namespace.resolve(e),s),this.walShmSymlink=s,this.effectiveDbPath=s,s}catch(u){throw new o(`Failed to create symlink for WAL at "${t}" \u2192 "${e}". Check directory existence & write permission.`,u)}}cleanupWalShmSymlink(){if(this.walShmSymlink){try{m__namespace.unlinkSync(this.walShmSymlink);}catch{}this.walShmSymlink=null,this.effectiveDbPath=null;}}createBackend(e,t){switch(this.sqliteBackend){case "better-sqlite3":return new w(e,{readonly:t.readonly});case "node:sqlite":return new b(e);case "@tursodatabase/database":return new k(e);default:throw new o(`Unsupported SQLite backend: ${String(this.sqliteBackend)}`)}}requireBackend(){if(!this.backend)throw new o("SQLite database not initialized. Call initialize() first.");return this.backend}requireCache(){if(!this.stmtCache)throw new o("SQLite database not initialized. Call initialize() first.");return this.stmtCache}assertInitialized(){if(!this.initialized)throw new o("SyncSqliteConnection not initialized. Call initialize() first.")}walModeDefault(){return true}foreignKeysDefault(){return true}readonlyDefault(){return false}busyTimeoutDefault(){return 5e3}walShmPathDefault(){}syncStmtCacheAccessor(){return this.requireCache()}},N=class{constructor(e){i(this,"backend",e);i(this,"cache",new Map);}get(e){let t=this.cache.get(e);if(t)return this.cache.delete(e),this.cache.set(e,t),t;let r=this.backend.prepare(e);if(this.cache.size>=he){let a=this.cache.keys().next().value;a!==void 0&&this.cache.delete(a);}return this.cache.set(e,r),r}clear(){this.cache.clear();}};function pe(n){let e=n.trimStart();if(e.length===0)return false;let a=((e.split(`
7
+ `,1)[0]??"").trimStart().split(/\s+/,1)[0]??"").toUpperCase();return a==="CREATE"||a==="DROP"||a==="ALTER"||a==="TRUNCATE"||a==="REPLACE"}function H(n){return n instanceof Error?n.message:String(n)}function me(n={}){let e=n.backend===void 0||n.backend==="auto"?f():n.backend,t=new A({...n,backend:e}),r=n.path??":memory:";if(r!==":memory:"){let a=h__namespace.dirname(r);try{m__namespace.mkdirSync(a,{recursive:!0});}catch(s){throw new o(`Failed to ensure directory for SQLite database at "${r}"`,s)}}return t}exports.ConnectionError=o;exports.DatabaseError=S;exports.DatabaseFactory=P;exports.PostgresDatabase=v;exports.QueryError=d;exports.SqliteDatabase=R;exports.SyncSqliteConnection=A;exports.TransactionError=l;exports.createSyncSqliteConnection=me;exports.default=P;exports.isSafeIdent=x;exports.resolveDefaultSqliteBackend=f;exports.translatePlaceholders=T;
@@ -0,0 +1,222 @@
1
+ import { PoolClient } from 'pg';
2
+
3
+ type DatabaseFlavor = 'sqlite' | 'postgresql';
4
+ type SqliteBackendKind = 'better-sqlite3' | 'node:sqlite' | '@tursodatabase/database';
5
+ interface SqliteDatabaseConfig {
6
+ backend?: SqliteBackendKind | 'auto';
7
+ path?: string;
8
+ walMode?: boolean;
9
+ walShmPath?: string;
10
+ foreignKeys?: boolean;
11
+ readonly?: boolean;
12
+ busyTimeoutMs?: number;
13
+ }
14
+ interface DatabaseConfig {
15
+ flavor: DatabaseFlavor;
16
+ sqlite?: SqliteDatabaseConfig;
17
+ connectionString?: string;
18
+ host?: string;
19
+ port?: number;
20
+ username?: string;
21
+ password?: string;
22
+ database?: string;
23
+ ssl?: boolean;
24
+ poolSize?: number;
25
+ }
26
+ type Row = Record<string, unknown>;
27
+ interface ExecuteResult {
28
+ changes: number;
29
+ lastInsertRowid?: number | bigint;
30
+ }
31
+ interface TransactionHandle {
32
+ readonly depth: number;
33
+ isFinished(): boolean;
34
+ query<Row_ extends Record<string, unknown> = Row>(sql: string, params?: ReadonlyArray<unknown>): Promise<Row_[]>;
35
+ queryOne<Row_ extends Record<string, unknown> = Row>(sql: string, params?: ReadonlyArray<unknown>): Promise<Row_ | null>;
36
+ execute(sql: string, params?: ReadonlyArray<unknown>): Promise<ExecuteResult>;
37
+ exec(sql: string): Promise<void>;
38
+ commit(): Promise<void>;
39
+ rollback(): Promise<void>;
40
+ savepoint(name: string): Promise<void>;
41
+ release(name: string): Promise<void>;
42
+ rollbackTo(name: string): Promise<void>;
43
+ }
44
+ interface DatabaseInterface {
45
+ initialize(): Promise<void>;
46
+ close(): Promise<void>;
47
+ isConnected(): boolean;
48
+ readonly flavor: DatabaseFlavor;
49
+ readonly sqliteBackend?: SqliteBackendKind;
50
+ query<Row_ extends Record<string, unknown> = Row>(sql: string, params?: ReadonlyArray<unknown>): Promise<Row_[]>;
51
+ queryOne<Row_ extends Record<string, unknown> = Row>(sql: string, params?: ReadonlyArray<unknown>): Promise<Row_ | null>;
52
+ execute(sql: string, params?: ReadonlyArray<unknown>): Promise<ExecuteResult>;
53
+ exec(sql: string): Promise<void>;
54
+ begin(): Promise<TransactionHandle>;
55
+ withTransaction<T>(fn: (tx: TransactionHandle) => Promise<T>): Promise<T>;
56
+ execBatch(sql: ReadonlyArray<string>): Promise<void>;
57
+ }
58
+ declare class DatabaseError extends Error {
59
+ readonly code: string;
60
+ readonly cause?: unknown;
61
+ constructor(code: string, message: string, cause?: unknown);
62
+ }
63
+ declare class ConnectionError extends DatabaseError {
64
+ constructor(message: string, cause?: unknown);
65
+ }
66
+ declare class QueryError extends DatabaseError {
67
+ constructor(message: string, cause?: unknown);
68
+ }
69
+ declare class TransactionError extends DatabaseError {
70
+ constructor(message: string, cause?: unknown);
71
+ }
72
+ declare const translatePlaceholders: (sql: string, style: "qmark" | "numbered") => string;
73
+ declare const isSafeIdent: (name: string) => boolean;
74
+
75
+ interface SqliteStatementAdapter {
76
+ run(...params: unknown[]): {
77
+ changes: number;
78
+ lastInsertRowid: number | bigint;
79
+ };
80
+ get(...params: unknown[]): unknown;
81
+ all(...params: unknown[]): unknown[];
82
+ iterate(...params: unknown[]): IterableIterator<unknown>;
83
+ }
84
+ interface SqliteBackendAdapter {
85
+ readonly backend: SqliteBackendKind;
86
+ exec(sql: string): void;
87
+ prepare(sql: string): SqliteStatementAdapter;
88
+ pragma(name: string, opts?: {
89
+ simple?: boolean;
90
+ }): unknown;
91
+ transaction<T extends (...args: never[]) => unknown>(fn: T): T;
92
+ close(): void;
93
+ readonly open: boolean;
94
+ }
95
+
96
+ declare const resolveDefaultSqliteBackend: () => SqliteBackendKind;
97
+
98
+ declare class SqliteDatabase implements DatabaseInterface {
99
+ readonly config: DatabaseConfig;
100
+ readonly flavor: "sqlite";
101
+ readonly sqliteBackend: SqliteBackendKind;
102
+ private backend;
103
+ private stmtCache;
104
+ private inTxDepth;
105
+ private readOnlyFlag;
106
+ private initialized;
107
+ private walShmSymlink;
108
+ private effectiveDbPath;
109
+ constructor(config: DatabaseConfig);
110
+ initialize(): Promise<void>;
111
+ private resolveOpenPath;
112
+ private cleanupWalShmSymlink;
113
+ private createBackend;
114
+ close(): Promise<void>;
115
+ isConnected(): boolean;
116
+ isReadOnly(): boolean;
117
+ private requireBackend;
118
+ private requireCache;
119
+ query<R extends Row = Row>(sql: string, params?: ReadonlyArray<unknown>): Promise<R[]>;
120
+ queryOne<R extends Row = Row>(sql: string, params?: ReadonlyArray<unknown>): Promise<R | null>;
121
+ execute(sql: string, params?: ReadonlyArray<unknown>): Promise<ExecuteResult>;
122
+ private prepareOrThrow;
123
+ exec(sql: string): Promise<void>;
124
+ execBatch(statements: ReadonlyArray<string>): Promise<void>;
125
+ begin(): Promise<TransactionHandle>;
126
+ withTransaction<T>(fn: (tx: TransactionHandle) => Promise<T>): Promise<T>;
127
+ getTransactionDepth(): number;
128
+ commitRoot(): void;
129
+ rollbackRoot(): void;
130
+ rollbackToSavepoint(name: string): void;
131
+ execSavepoint(name: string): void;
132
+ execRelease(name: string): void;
133
+ execRollbackTo(name: string): void;
134
+ pushTxDepth(): number;
135
+ popTxDepth(): void;
136
+ private errorMessage;
137
+ }
138
+
139
+ declare class PostgresDatabase implements DatabaseInterface {
140
+ readonly config: DatabaseConfig;
141
+ readonly flavor: "postgresql";
142
+ readonly sqliteBackend: undefined;
143
+ private pool;
144
+ private readonly translatedSqlCache;
145
+ private txCtx;
146
+ private lastTxDepth;
147
+ constructor(config: DatabaseConfig);
148
+ initialize(): Promise<void>;
149
+ close(): Promise<void>;
150
+ isConnected(): boolean;
151
+ private requirePool;
152
+ query<R extends Row = Row>(sql: string, params?: ReadonlyArray<unknown>): Promise<R[]>;
153
+ queryOne<R extends Row = Row>(sql: string, params?: ReadonlyArray<unknown>): Promise<R | null>;
154
+ execute(sql: string, params?: ReadonlyArray<unknown>): Promise<ExecuteResult>;
155
+ exec(sql: string): Promise<void>;
156
+ execBatch(statements: ReadonlyArray<string>): Promise<void>;
157
+ begin(): Promise<TransactionHandle>;
158
+ private requireCtx;
159
+ getClient(depth: number): PoolClient;
160
+ clearTxCtx(): void;
161
+ releaseTxClient(destroy: boolean): void;
162
+ withTransaction<T>(fn: (tx: TransactionHandle) => Promise<T>): Promise<T>;
163
+ pushTxDepth(): number;
164
+ popTxDepth(): void;
165
+ getTransactionDepth(): number;
166
+ }
167
+
168
+ declare class DatabaseFactory {
169
+ static create(config: DatabaseConfig): DatabaseInterface;
170
+ }
171
+
172
+ interface SyncSqliteConnectionConfig {
173
+ backend?: SqliteBackendKind | 'auto';
174
+ path?: string;
175
+ walMode?: boolean;
176
+ walShmPath?: string;
177
+ foreignKeys?: boolean;
178
+ readonly?: boolean;
179
+ busyTimeoutMs?: number;
180
+ }
181
+ declare class SyncSqliteConnection {
182
+ readonly flavor: "sqlite";
183
+ readonly sqliteBackend: SqliteBackendKind;
184
+ private backend;
185
+ private stmtCache;
186
+ private readOnlyFlag;
187
+ private initialized;
188
+ private walShmSymlink;
189
+ private effectiveDbPath;
190
+ private readonly dbPath;
191
+ constructor(config: SyncSqliteConnectionConfig);
192
+ private resolveBackendPath;
193
+ initialize(): void;
194
+ close(): void;
195
+ isOpen(): boolean;
196
+ isReadOnly(): boolean;
197
+ getDbPath(): string;
198
+ getBackend(): SqliteBackendKind;
199
+ getJournalMode(): string;
200
+ prepare(sql: string): SqliteStatementAdapter;
201
+ exec(sql: string): void;
202
+ pragma(name: string, opts?: {
203
+ simple?: boolean;
204
+ }): unknown;
205
+ transaction<T extends (...args: never[]) => unknown>(fn: T): T;
206
+ execBatch(statements: ReadonlyArray<string>): void;
207
+ private resolveOpenPath;
208
+ private cleanupWalShmSymlink;
209
+ private createBackend;
210
+ private requireBackend;
211
+ private requireCache;
212
+ private assertInitialized;
213
+ private walModeDefault;
214
+ private foreignKeysDefault;
215
+ private readonlyDefault;
216
+ private busyTimeoutDefault;
217
+ private walShmPathDefault;
218
+ private syncStmtCacheAccessor;
219
+ }
220
+ declare function createSyncSqliteConnection(config?: SyncSqliteConnectionConfig): SyncSqliteConnection;
221
+
222
+ export { ConnectionError, type DatabaseConfig, DatabaseError, DatabaseFactory, type DatabaseFlavor, type DatabaseInterface, type ExecuteResult, PostgresDatabase, QueryError, type Row, type SqliteBackendAdapter, type SqliteBackendKind, SqliteDatabase, type SqliteDatabaseConfig, type SqliteStatementAdapter, SyncSqliteConnection, type SyncSqliteConnectionConfig, TransactionError, type TransactionHandle, createSyncSqliteConnection, DatabaseFactory as default, isSafeIdent, resolveDefaultSqliteBackend, translatePlaceholders };
@@ -0,0 +1,222 @@
1
+ import { PoolClient } from 'pg';
2
+
3
+ type DatabaseFlavor = 'sqlite' | 'postgresql';
4
+ type SqliteBackendKind = 'better-sqlite3' | 'node:sqlite' | '@tursodatabase/database';
5
+ interface SqliteDatabaseConfig {
6
+ backend?: SqliteBackendKind | 'auto';
7
+ path?: string;
8
+ walMode?: boolean;
9
+ walShmPath?: string;
10
+ foreignKeys?: boolean;
11
+ readonly?: boolean;
12
+ busyTimeoutMs?: number;
13
+ }
14
+ interface DatabaseConfig {
15
+ flavor: DatabaseFlavor;
16
+ sqlite?: SqliteDatabaseConfig;
17
+ connectionString?: string;
18
+ host?: string;
19
+ port?: number;
20
+ username?: string;
21
+ password?: string;
22
+ database?: string;
23
+ ssl?: boolean;
24
+ poolSize?: number;
25
+ }
26
+ type Row = Record<string, unknown>;
27
+ interface ExecuteResult {
28
+ changes: number;
29
+ lastInsertRowid?: number | bigint;
30
+ }
31
+ interface TransactionHandle {
32
+ readonly depth: number;
33
+ isFinished(): boolean;
34
+ query<Row_ extends Record<string, unknown> = Row>(sql: string, params?: ReadonlyArray<unknown>): Promise<Row_[]>;
35
+ queryOne<Row_ extends Record<string, unknown> = Row>(sql: string, params?: ReadonlyArray<unknown>): Promise<Row_ | null>;
36
+ execute(sql: string, params?: ReadonlyArray<unknown>): Promise<ExecuteResult>;
37
+ exec(sql: string): Promise<void>;
38
+ commit(): Promise<void>;
39
+ rollback(): Promise<void>;
40
+ savepoint(name: string): Promise<void>;
41
+ release(name: string): Promise<void>;
42
+ rollbackTo(name: string): Promise<void>;
43
+ }
44
+ interface DatabaseInterface {
45
+ initialize(): Promise<void>;
46
+ close(): Promise<void>;
47
+ isConnected(): boolean;
48
+ readonly flavor: DatabaseFlavor;
49
+ readonly sqliteBackend?: SqliteBackendKind;
50
+ query<Row_ extends Record<string, unknown> = Row>(sql: string, params?: ReadonlyArray<unknown>): Promise<Row_[]>;
51
+ queryOne<Row_ extends Record<string, unknown> = Row>(sql: string, params?: ReadonlyArray<unknown>): Promise<Row_ | null>;
52
+ execute(sql: string, params?: ReadonlyArray<unknown>): Promise<ExecuteResult>;
53
+ exec(sql: string): Promise<void>;
54
+ begin(): Promise<TransactionHandle>;
55
+ withTransaction<T>(fn: (tx: TransactionHandle) => Promise<T>): Promise<T>;
56
+ execBatch(sql: ReadonlyArray<string>): Promise<void>;
57
+ }
58
+ declare class DatabaseError extends Error {
59
+ readonly code: string;
60
+ readonly cause?: unknown;
61
+ constructor(code: string, message: string, cause?: unknown);
62
+ }
63
+ declare class ConnectionError extends DatabaseError {
64
+ constructor(message: string, cause?: unknown);
65
+ }
66
+ declare class QueryError extends DatabaseError {
67
+ constructor(message: string, cause?: unknown);
68
+ }
69
+ declare class TransactionError extends DatabaseError {
70
+ constructor(message: string, cause?: unknown);
71
+ }
72
+ declare const translatePlaceholders: (sql: string, style: "qmark" | "numbered") => string;
73
+ declare const isSafeIdent: (name: string) => boolean;
74
+
75
+ interface SqliteStatementAdapter {
76
+ run(...params: unknown[]): {
77
+ changes: number;
78
+ lastInsertRowid: number | bigint;
79
+ };
80
+ get(...params: unknown[]): unknown;
81
+ all(...params: unknown[]): unknown[];
82
+ iterate(...params: unknown[]): IterableIterator<unknown>;
83
+ }
84
+ interface SqliteBackendAdapter {
85
+ readonly backend: SqliteBackendKind;
86
+ exec(sql: string): void;
87
+ prepare(sql: string): SqliteStatementAdapter;
88
+ pragma(name: string, opts?: {
89
+ simple?: boolean;
90
+ }): unknown;
91
+ transaction<T extends (...args: never[]) => unknown>(fn: T): T;
92
+ close(): void;
93
+ readonly open: boolean;
94
+ }
95
+
96
+ declare const resolveDefaultSqliteBackend: () => SqliteBackendKind;
97
+
98
+ declare class SqliteDatabase implements DatabaseInterface {
99
+ readonly config: DatabaseConfig;
100
+ readonly flavor: "sqlite";
101
+ readonly sqliteBackend: SqliteBackendKind;
102
+ private backend;
103
+ private stmtCache;
104
+ private inTxDepth;
105
+ private readOnlyFlag;
106
+ private initialized;
107
+ private walShmSymlink;
108
+ private effectiveDbPath;
109
+ constructor(config: DatabaseConfig);
110
+ initialize(): Promise<void>;
111
+ private resolveOpenPath;
112
+ private cleanupWalShmSymlink;
113
+ private createBackend;
114
+ close(): Promise<void>;
115
+ isConnected(): boolean;
116
+ isReadOnly(): boolean;
117
+ private requireBackend;
118
+ private requireCache;
119
+ query<R extends Row = Row>(sql: string, params?: ReadonlyArray<unknown>): Promise<R[]>;
120
+ queryOne<R extends Row = Row>(sql: string, params?: ReadonlyArray<unknown>): Promise<R | null>;
121
+ execute(sql: string, params?: ReadonlyArray<unknown>): Promise<ExecuteResult>;
122
+ private prepareOrThrow;
123
+ exec(sql: string): Promise<void>;
124
+ execBatch(statements: ReadonlyArray<string>): Promise<void>;
125
+ begin(): Promise<TransactionHandle>;
126
+ withTransaction<T>(fn: (tx: TransactionHandle) => Promise<T>): Promise<T>;
127
+ getTransactionDepth(): number;
128
+ commitRoot(): void;
129
+ rollbackRoot(): void;
130
+ rollbackToSavepoint(name: string): void;
131
+ execSavepoint(name: string): void;
132
+ execRelease(name: string): void;
133
+ execRollbackTo(name: string): void;
134
+ pushTxDepth(): number;
135
+ popTxDepth(): void;
136
+ private errorMessage;
137
+ }
138
+
139
+ declare class PostgresDatabase implements DatabaseInterface {
140
+ readonly config: DatabaseConfig;
141
+ readonly flavor: "postgresql";
142
+ readonly sqliteBackend: undefined;
143
+ private pool;
144
+ private readonly translatedSqlCache;
145
+ private txCtx;
146
+ private lastTxDepth;
147
+ constructor(config: DatabaseConfig);
148
+ initialize(): Promise<void>;
149
+ close(): Promise<void>;
150
+ isConnected(): boolean;
151
+ private requirePool;
152
+ query<R extends Row = Row>(sql: string, params?: ReadonlyArray<unknown>): Promise<R[]>;
153
+ queryOne<R extends Row = Row>(sql: string, params?: ReadonlyArray<unknown>): Promise<R | null>;
154
+ execute(sql: string, params?: ReadonlyArray<unknown>): Promise<ExecuteResult>;
155
+ exec(sql: string): Promise<void>;
156
+ execBatch(statements: ReadonlyArray<string>): Promise<void>;
157
+ begin(): Promise<TransactionHandle>;
158
+ private requireCtx;
159
+ getClient(depth: number): PoolClient;
160
+ clearTxCtx(): void;
161
+ releaseTxClient(destroy: boolean): void;
162
+ withTransaction<T>(fn: (tx: TransactionHandle) => Promise<T>): Promise<T>;
163
+ pushTxDepth(): number;
164
+ popTxDepth(): void;
165
+ getTransactionDepth(): number;
166
+ }
167
+
168
+ declare class DatabaseFactory {
169
+ static create(config: DatabaseConfig): DatabaseInterface;
170
+ }
171
+
172
+ interface SyncSqliteConnectionConfig {
173
+ backend?: SqliteBackendKind | 'auto';
174
+ path?: string;
175
+ walMode?: boolean;
176
+ walShmPath?: string;
177
+ foreignKeys?: boolean;
178
+ readonly?: boolean;
179
+ busyTimeoutMs?: number;
180
+ }
181
+ declare class SyncSqliteConnection {
182
+ readonly flavor: "sqlite";
183
+ readonly sqliteBackend: SqliteBackendKind;
184
+ private backend;
185
+ private stmtCache;
186
+ private readOnlyFlag;
187
+ private initialized;
188
+ private walShmSymlink;
189
+ private effectiveDbPath;
190
+ private readonly dbPath;
191
+ constructor(config: SyncSqliteConnectionConfig);
192
+ private resolveBackendPath;
193
+ initialize(): void;
194
+ close(): void;
195
+ isOpen(): boolean;
196
+ isReadOnly(): boolean;
197
+ getDbPath(): string;
198
+ getBackend(): SqliteBackendKind;
199
+ getJournalMode(): string;
200
+ prepare(sql: string): SqliteStatementAdapter;
201
+ exec(sql: string): void;
202
+ pragma(name: string, opts?: {
203
+ simple?: boolean;
204
+ }): unknown;
205
+ transaction<T extends (...args: never[]) => unknown>(fn: T): T;
206
+ execBatch(statements: ReadonlyArray<string>): void;
207
+ private resolveOpenPath;
208
+ private cleanupWalShmSymlink;
209
+ private createBackend;
210
+ private requireBackend;
211
+ private requireCache;
212
+ private assertInitialized;
213
+ private walModeDefault;
214
+ private foreignKeysDefault;
215
+ private readonlyDefault;
216
+ private busyTimeoutDefault;
217
+ private walShmPathDefault;
218
+ private syncStmtCacheAccessor;
219
+ }
220
+ declare function createSyncSqliteConnection(config?: SyncSqliteConnectionConfig): SyncSqliteConnection;
221
+
222
+ export { ConnectionError, type DatabaseConfig, DatabaseError, DatabaseFactory, type DatabaseFlavor, type DatabaseInterface, type ExecuteResult, PostgresDatabase, QueryError, type Row, type SqliteBackendAdapter, type SqliteBackendKind, SqliteDatabase, type SqliteDatabaseConfig, type SqliteStatementAdapter, SyncSqliteConnection, type SyncSqliteConnectionConfig, TransactionError, type TransactionHandle, createSyncSqliteConnection, DatabaseFactory as default, isSafeIdent, resolveDefaultSqliteBackend, translatePlaceholders };
package/dist/index.mjs ADDED
@@ -0,0 +1,7 @@
1
+ import {createRequire}from'module';import*as y from'fs';import*as p from'path';import {Pool}from'pg';var j=Object.defineProperty;var U=(n,e,t)=>e in n?j(n,e,{enumerable:true,configurable:true,writable:true,value:t}):n[e]=t;var i=(n,e,t)=>U(n,typeof e!="symbol"?e+"":e,t);var x=class extends Error{constructor(t,r,a){super(r);i(this,"code");i(this,"cause");this.name="DatabaseError",this.code=t,this.cause=a;}},o=class extends x{constructor(e,t){super("CONNECTION",e,t),this.name="ConnectionError";}},c=class extends x{constructor(e,t){super("QUERY",e,t),this.name="QueryError";}},l=class extends x{constructor(e,t){super("TX",e,t),this.name="TransactionError";}},C=(n,e)=>{if(e==="qmark")return n;let t=0;return n.replace(/\?/g,()=>`$${++t}`)},q=n=>/^[A-Za-z_][A-Za-z0-9_]*$/.test(n);var J=createRequire(import.meta.url),E=(n,e)=>{try{let t=J(n);return e(t)}catch{return false}},w=()=>{if(E("better-sqlite3",n=>typeof n.default=="function"))return "better-sqlite3";if(E("node:sqlite",n=>typeof n.DatabaseSync=="function"))return "node:sqlite";if(E("@tursodatabase/database",n=>typeof(n.default??n)=="function"))return "@tursodatabase/database";throw new o(`No SQLite backend available. Please install one of:
2
+ - better-sqlite3 (recommended; highest perf)
3
+ - node:sqlite (built-in since Node.js 22.5+)
4
+ - @tursodatabase/database`)};var f=class{constructor(e){i(this,"stmt",e);}run(...e){return this.stmt.run(...e)}get(...e){return this.stmt.get(...e)}all(...e){return this.stmt.all(...e)}iterate(...e){return this.stmt.iterate(...e)}};var X=createRequire(import.meta.url),_=n=>typeof n=="function"&&n.prototype!==void 0,b=class{constructor(e,t={}){i(this,"backend","better-sqlite3");i(this,"db");let r=X("better-sqlite3"),a=_(r)?r:r.default;if(!_(a))throw new TypeError(`better-sqlite3 import did not expose a constructor (got ${typeof r})`);this.db=new a(e,t);}exec(e){this.db.exec(e);}prepare(e){return new f(this.db.prepare(e))}pragma(e,t){return this.db.pragma(e,t)}transaction(e){return this.db.transaction(e)}close(){this.db.open&&this.db.close();}get open(){return this.db.open}};var te=createRequire(import.meta.url),k=class{constructor(e){i(this,"backend","node:sqlite");i(this,"db");let t=te("node:sqlite");this.db=new t.DatabaseSync(e);}exec(e){this.db.exec(e);}prepare(e){return new f(this.db.prepare(e))}pragma(e,t){return this.db.exec(`PRAGMA ${e}`),t?.simple!==true?void 0:this.db.prepare("SELECT pragma_value AS v FROM pragma_value(?)").get(e)}transaction(e){return((...r)=>{this.db.exec("BEGIN");let a;try{a=e(...r);}catch(s){try{this.db.exec("ROLLBACK");}catch{}throw s}return this.db.exec("COMMIT"),a})}close(){this.db.isOpen&&this.db.close();}get open(){return this.db.isOpen}};var re=createRequire(import.meta.url),Q=n=>typeof n=="function"&&n.prototype!==void 0,g=class{constructor(e){i(this,"backend","@tursodatabase/database");i(this,"db",null);let t=re("@tursodatabase/database"),r=Q(t)?t:t.default;if(!Q(r))throw new TypeError(`@tursodatabase/database import did not expose a constructor (got ${typeof t})`);this.db=new r(e);}requireDb(){if(!this.db)throw new Error(`Turso backend not initialized (db=${String(this.db)})`);return this.db}exec(e){this.requireDb().exec(e);}prepare(e){return new f(this.requireDb().prepare(e))}pragma(e){this.requireDb().exec(`PRAGMA ${e}`);}transaction(e){return((...r)=>{let a=this.requireDb();a.exec("BEGIN");let s;try{s=e(...r);}catch(d){try{a.exec("ROLLBACK");}catch{}throw d}return a.exec("COMMIT"),s})}close(){if(this.db)try{this.db.close();}finally{this.db=null;}}get open(){return this.db!==null}};var ae=256,L=class{constructor(e){i(this,"backend",e);i(this,"cache",new Map);}get(e){let t=this.cache.get(e);if(t)return this.cache.delete(e),this.cache.set(e,t),t;let r=this.backend.prepare(e);if(this.cache.size>=ae){let a=this.cache.keys().next().value;a!==void 0&&this.cache.delete(a);}return this.cache.set(e,r),r}clear(){this.cache.clear();}},K=n=>n==null?{}:typeof n!="object"?{}:{...n},ie=n=>n.map(K);function O(n){if(!q(n))throw new l(`Unsafe SQL identifier: ${JSON.stringify(n)}`);return `"${n}"`}function se(n){let e=n.trimStart();if(e.length===0)return false;let a=((e.split(`
5
+ `,1)[0]??"").trimStart().split(/\s+/,1)[0]??"").toUpperCase();return a==="CREATE"||a==="DROP"||a==="ALTER"||a==="TRUNCATE"||a==="REPLACE"}var I=class{constructor(e,t){i(this,"db",e);i(this,"depth");i(this,"finished",false);this.depth=t;}isFinished(){return this.finished}async query(e,t){return this.assertOpen(),this.db.query(e,t)}async queryOne(e,t){return this.assertOpen(),this.db.queryOne(e,t)}async execute(e,t){return this.assertOpen(),this.db.execute(e,t)}async exec(e){this.assertOpen(),await this.db.exec(e);}async commit(){this.finished||(this.finished=true,this.depth===1&&this.db.commitRoot(),this.db.popTxDepth());}async rollback(){if(!this.finished){this.finished=true;try{this.depth===1?this.db.rollbackRoot():this.db.rollbackToSavepoint(`sp_${this.depth}`);}catch{}this.db.popTxDepth();}}async savepoint(e){this.assertOpen(),this.db.execSavepoint(O(e));}async release(e){this.assertOpen(),this.db.execRelease(O(e));}async rollbackTo(e){this.assertOpen(),this.db.execRollbackTo(O(e));}assertOpen(){if(this.finished)throw new l(`Transaction already finalized (depth=${this.depth})`)}},v=class{constructor(e){i(this,"config",e);i(this,"flavor","sqlite");i(this,"sqliteBackend");i(this,"backend",null);i(this,"stmtCache",null);i(this,"inTxDepth",0);i(this,"readOnlyFlag",false);i(this,"initialized",false);i(this,"walShmSymlink",null);i(this,"effectiveDbPath",null);let t=e.sqlite??{};this.sqliteBackend=t.backend===void 0||t.backend==="auto"?w():t.backend;}async initialize(){if(this.initialized)return;let e=this.config.sqlite??{},t=e.path??":memory:",r=e.walMode!==false,a=e.foreignKeys!==false,s=e.readonly===true,d=e.busyTimeoutMs??5e3,R=e.walShmPath,F=this.resolveOpenPath(t,R);try{this.backend=this.createBackend(F,{readonly:s});}catch(B){throw this.cleanupWalShmSymlink(),new o(`Failed to create SQLite backend (${this.sqliteBackend}) at "${F}"`,B)}this.readOnlyFlag=s,this.stmtCache=new L(this.backend);try{!s&&t!==":memory:"&&r&&this.backend.exec("PRAGMA journal_mode = WAL"),!s&&a&&this.backend.exec("PRAGMA foreign_keys = ON"),this.backend.exec(`PRAGMA busy_timeout = ${d}`);}catch(B){throw this.backend=null,this.stmtCache=null,this.cleanupWalShmSymlink(),new o("Failed to apply SQLite pragmas",B)}this.initialized=true;}resolveOpenPath(e,t){if(e===":memory:"||t===void 0||t===""||this.sqliteBackend!=="better-sqlite3")return e;let r=p.dirname(e);if(p.resolve(r)===p.resolve(t))return e;let a=p.basename(e),s=p.join(t,a);try{try{let d=y.lstatSync(s);(d.isSymbolicLink()||d.isFile())&&y.unlinkSync(s);}catch{}return y.symlinkSync(p.resolve(e),s),this.walShmSymlink=s,this.effectiveDbPath=s,s}catch(d){throw new o(`Failed to create symlink for WAL at "${t}" \u2192 "${e}". Check directory existence & write permission.`,d)}}cleanupWalShmSymlink(){if(this.walShmSymlink){try{y.unlinkSync(this.walShmSymlink);}catch{}this.walShmSymlink=null,this.effectiveDbPath=null;}}createBackend(e,t){switch(this.sqliteBackend){case "better-sqlite3":return new b(e,{readonly:t.readonly});case "node:sqlite":return new k(e);case "@tursodatabase/database":return new g(e);default:throw new o(`Unsupported SQLite backend: ${String(this.sqliteBackend)}`)}}async close(){if(!this.backend){this.stmtCache?.clear(),this.stmtCache=null,this.initialized=false,this.cleanupWalShmSymlink();return}try{this.backend.close();}finally{this.backend=null,this.stmtCache?.clear(),this.stmtCache=null,this.inTxDepth=0,this.initialized=false,this.cleanupWalShmSymlink();}}isConnected(){return this.backend?.open===true}isReadOnly(){return this.readOnlyFlag}requireBackend(){if(!this.backend)throw new o("SQLite database not initialized. Call initialize() first.");return this.backend}requireCache(){if(!this.stmtCache)throw new o("SQLite database not initialized. Call initialize() first.");return this.stmtCache}async query(e,t){let r=this.prepareOrThrow(e);try{return ie(r.all(...t??[]))}catch(a){throw new c(`SQLite query failed: ${this.errorMessage(a)}`,a)}}async queryOne(e,t){let r=this.prepareOrThrow(e);try{let a=r.get(...t??[]);return a==null?null:K(a)}catch(a){throw new c(`SQLite queryOne failed: ${this.errorMessage(a)}`,a)}}async execute(e,t){let r=this.prepareOrThrow(e);try{return r.run(...t??[])}catch(a){throw new c(`SQLite execute failed: ${this.errorMessage(a)}`,a)}}prepareOrThrow(e){try{return this.requireCache().get(e)}catch(t){throw t instanceof o?t:new c(`SQLite prepare failed: ${this.errorMessage(t)}`,t)}}async exec(e){let t=this.requireBackend();try{t.exec(e),se(e)&&this.stmtCache?.clear();}catch(r){throw new c(`SQLite exec failed: ${this.errorMessage(r)}`,r)}}async execBatch(e){for(let t of e)await this.exec(t);}async begin(){let e=this.requireBackend();if(this.readOnlyFlag)throw new l("Cannot begin transaction on read-only SQLite database");let t=this.pushTxDepth();try{t===1?e.exec("BEGIN"):e.exec(`SAVEPOINT sp_${t}`);}catch(r){this.popTxDepth();let a=t===1?"BEGIN":`SAVEPOINT sp_${t}`;throw new l(`${a} failed`,r)}return new I(this,t)}async withTransaction(e){let t=await this.begin();try{let r=await e(t);return await t.commit(),r}catch(r){try{await t.rollback();}catch{}throw r}}getTransactionDepth(){return this.inTxDepth}commitRoot(){this.requireBackend().exec("COMMIT");}rollbackRoot(){this.requireBackend().exec("ROLLBACK");}rollbackToSavepoint(e){this.requireBackend().exec(`ROLLBACK TO SAVEPOINT ${e}`);}execSavepoint(e){this.requireBackend().exec(`SAVEPOINT ${e}`);}execRelease(e){this.requireBackend().exec(`RELEASE SAVEPOINT ${e}`);}execRollbackTo(e){this.requireBackend().exec(`ROLLBACK TO SAVEPOINT ${e}`);}pushTxDepth(){return this.inTxDepth+=1}popTxDepth(){this.inTxDepth>0&&(this.inTxDepth-=1);}errorMessage(e){return e instanceof Error?e.message:String(e)}};var le=256,z=class{constructor(){i(this,"cache",new Map);}get(e){let t=this.cache.get(e);if(t)return this.cache.delete(e),this.cache.set(e,t),t;let r=C(e,"numbered");if(this.cache.size>=le){let a=this.cache.keys().next().value;a!==void 0&&this.cache.delete(a);}return this.cache.set(e,r),r}clear(){this.cache.clear();}},ce=n=>n==null?{}:{...n},de=n=>n.map(e=>ce(e)),$=n=>{if(!q(n))throw new l(`Unsafe SQL identifier: ${JSON.stringify(n)}`);return `"${n}"`};function W(n,e){return n.includes("?")?{sql:C(n,"numbered"),params:e?[...e]:[]}:{sql:n,params:void 0}}var P=class{constructor(e,t){i(this,"db",e);i(this,"depth");i(this,"finished",false);this.depth=t;}get client(){return this.db.getClient(this.depth)}isFinished(){return this.finished}assertOpen(){if(this.finished)throw new l("Transaction already finalized")}async query(e,t){return this.assertOpen(),await G(this.client,e,t)}async queryOne(e,t){let r=await this.query(e,t);return r.length>0?r[0]??null:null}async execute(e,t){return this.assertOpen(),await H(this.client,e,t)}async exec(e){this.assertOpen(),await M(this.client,e);}async commit(){if(!this.finished)if(this.finished=true,this.depth===1)try{await this.client.query("COMMIT");}finally{this.db.releaseTxClient(false),this.db.clearTxCtx();}else await this.client.query(`RELEASE SAVEPOINT sp_${this.depth}`);}async rollback(){if(!this.finished)if(this.finished=true,this.depth===1)try{await this.client.query("ROLLBACK");}catch{}finally{this.db.releaseTxClient(false),this.db.clearTxCtx();}else try{await this.client.query(`ROLLBACK TO SAVEPOINT sp_${this.depth}`);}catch{}}async savepoint(e){this.assertOpen(),await this.client.query(`SAVEPOINT ${$(e)}`);}async release(e){this.assertOpen(),await this.client.query(`RELEASE SAVEPOINT ${$(e)}`);}async rollbackTo(e){this.assertOpen(),await this.client.query(`ROLLBACK TO SAVEPOINT ${$(e)}`);}};async function G(n,e,t){let{sql:r,params:a}=W(e,t);try{let s=await n.query(r,a);return de(s.rows)}catch(s){throw new c(`PostgreSQL query failed: ${s instanceof Error?s.message:String(s)}`,s)}}async function H(n,e,t){let{sql:r,params:a}=W(e,t);try{return {changes:(await n.query(r,a)).rowCount??0}}catch(s){throw new c(`PostgreSQL execute failed: ${s instanceof Error?s.message:String(s)}`,s)}}async function M(n,e){try{await n.query(e);}catch(t){throw new c(`PostgreSQL exec failed: ${t instanceof Error?t.message:String(t)}`,t)}}async function ue(n,e){if(e.length===0)return;let t=e.join(`;
6
+ `)+";";await M(n,t);}var T=class{constructor(e){i(this,"config",e);i(this,"flavor","postgresql");i(this,"sqliteBackend");i(this,"pool",null);i(this,"translatedSqlCache",new z);i(this,"txCtx",null);i(this,"lastTxDepth",0);}async initialize(){if(!this.pool)try{this.pool=new Pool({host:this.config.host??"localhost",port:this.config.port??5432,user:this.config.username??"postgres",password:this.config.password??"",database:this.config.database??"eas_database",ssl:this.config.ssl??!1,max:this.config.poolSize??10}),(await this.pool.connect()).release();}catch(e){throw this.pool=null,new o("Failed to connect to PostgreSQL",e)}}async close(){if(!this.pool)return;let e=this.pool;this.pool=null;try{await e.end();}catch(t){throw new o("Failed to close PostgreSQL pool",t)}finally{this.translatedSqlCache.clear(),this.txCtx=null,this.lastTxDepth=0;}}isConnected(){return this.pool===null?false:!this.pool.ended&&!this.pool.closing}requirePool(){if(!this.pool)throw new o("PostgreSQL database not initialized. Call initialize() first.");return this.pool}async query(e,t){let r=await this.requirePool().connect();try{return await G(r,e,t)}finally{r.release();}}async queryOne(e,t){let r=await this.query(e,t);return r.length>0?r[0]??null:null}async execute(e,t){let r=await this.requirePool().connect();try{return await H(r,e,t)}finally{r.release();}}async exec(e){let t=await this.requirePool().connect();try{await M(t,e);}finally{t.release();}}async execBatch(e){if(e.length===0)return;let t=await this.requirePool().connect();try{await ue(t,e);}finally{t.release();}}async begin(){let e=this.requirePool(),t=this.pushTxDepth();if(t>1){let a=this.requireCtx(t);try{await a.client.query(`SAVEPOINT sp_${t}`);}catch(s){throw this.popTxDepth(),new l(`SAVEPOINT sp_${t} failed`,s)}return new P(this,t)}let r;try{r=await e.connect();}catch(a){throw this.popTxDepth(),new l("BEGIN failed: cannot acquire client",a)}try{await r.query("BEGIN");}catch(a){throw r.release(true),this.popTxDepth(),new l("BEGIN failed",a)}return this.txCtx={client:r,depth:t},new P(this,t)}requireCtx(e){if(!this.txCtx)throw new l(`No active transaction (depth=${e})`);return this.txCtx}getClient(e){return this.requireCtx(e).client}clearTxCtx(){this.txCtx=null,this.lastTxDepth=0,this.translatedSqlCache.clear();}releaseTxClient(e){let t=this.txCtx;if(t)try{t.client.release(e);}catch{}}async withTransaction(e){let t=await this.begin();try{let r=await e(t);return await t.commit(),r}catch(r){try{await t.rollback();}catch{}throw r}}pushTxDepth(){return this.lastTxDepth+=1}popTxDepth(){this.lastTxDepth>0&&(this.lastTxDepth-=1);}getTransactionDepth(){return this.lastTxDepth}};var A=class{static create(e){switch(e.flavor){case "sqlite":return new v(e);case "postgresql":return new T(e);default:{let t=e.flavor;throw new o(`Unsupported database flavor: ${String(t)}`)}}}};var he=256,D=class{constructor(e){i(this,"flavor","sqlite");i(this,"sqliteBackend");i(this,"backend",null);i(this,"stmtCache",null);i(this,"readOnlyFlag",false);i(this,"initialized",false);i(this,"walShmSymlink",null);i(this,"effectiveDbPath",null);i(this,"dbPath");this.sqliteBackend=e.backend===void 0||e.backend==="auto"?w():e.backend,this.dbPath=e.path??":memory:";}resolveBackendPath(e){return e===":memory:"?"":e}initialize(){if(this.initialized)return;let e=this.walModeDefault(),t=this.foreignKeysDefault(),r=this.readonlyDefault(),a=this.busyTimeoutDefault(),s=this.resolveOpenPath(this.dbPath,e?this.walShmPathDefault():void 0),d=s===":memory:"?"":s;try{this.backend=this.createBackend(d,{readonly:r});}catch(R){throw this.cleanupWalShmSymlink(),new o(`Failed to create SQLite backend (${this.sqliteBackend}) at "${s}"`,R)}this.readOnlyFlag=r,this.stmtCache=new N(this.backend);try{!r&&this.dbPath!==":memory:"&&e&&this.backend.exec("PRAGMA journal_mode = WAL"),!r&&t&&this.backend.exec("PRAGMA foreign_keys = ON"),this.backend.exec(`PRAGMA busy_timeout = ${a}`);}catch(R){throw this.backend=null,this.stmtCache=null,this.cleanupWalShmSymlink(),new o("Failed to apply SQLite pragmas",R)}this.initialized=true;}close(){if(!this.backend){this.stmtCache?.clear(),this.stmtCache=null,this.initialized=false,this.cleanupWalShmSymlink();return}try{this.backend.close();}finally{this.backend=null,this.stmtCache?.clear(),this.stmtCache=null,this.readOnlyFlag=false,this.initialized=false,this.cleanupWalShmSymlink();}}isOpen(){return this.backend?.open===true}isReadOnly(){return this.readOnlyFlag}getDbPath(){return this.dbPath}getBackend(){return this.sqliteBackend}getJournalMode(){this.assertInitialized();let e=this.requireBackend().pragma("journal_mode"),t=Array.isArray(e)?e[0]:e;if(t==null)return "";if(typeof t=="object"){let r=t.journal_mode;return typeof r=="string"?r.toLowerCase():String(r??"").toLowerCase()}return String(t).toLowerCase()}prepare(e){this.assertInitialized();try{return this.requireCache().get(e)}catch(t){throw t instanceof o?t:new c(`SQLite prepare failed: ${V(t)}`,t)}}exec(e){this.assertInitialized();let t=this.requireBackend();try{t.exec(e),pe(e)&&this.stmtCache?.clear();}catch(r){throw new c(`SQLite exec failed: ${V(r)}`,r)}}pragma(e,t){return this.assertInitialized(),this.requireBackend().pragma(e,t)}transaction(e){if(this.assertInitialized(),this.readOnlyFlag)throw new l("Cannot begin transaction on read-only SQLite database");return this.requireBackend().transaction(e)}execBatch(e){if(e.length!==0)for(let t of e)this.exec(t);}resolveOpenPath(e,t){if(e===":memory:"||t===void 0||t===""||this.sqliteBackend!=="better-sqlite3")return e;let r=p.dirname(e);if(p.resolve(r)===p.resolve(t))return e;let a=p.basename(e),s=p.join(t,a);try{try{let d=y.lstatSync(s);(d.isSymbolicLink()||d.isFile())&&y.unlinkSync(s);}catch{}return y.symlinkSync(p.resolve(e),s),this.walShmSymlink=s,this.effectiveDbPath=s,s}catch(d){throw new o(`Failed to create symlink for WAL at "${t}" \u2192 "${e}". Check directory existence & write permission.`,d)}}cleanupWalShmSymlink(){if(this.walShmSymlink){try{y.unlinkSync(this.walShmSymlink);}catch{}this.walShmSymlink=null,this.effectiveDbPath=null;}}createBackend(e,t){switch(this.sqliteBackend){case "better-sqlite3":return new b(e,{readonly:t.readonly});case "node:sqlite":return new k(e);case "@tursodatabase/database":return new g(e);default:throw new o(`Unsupported SQLite backend: ${String(this.sqliteBackend)}`)}}requireBackend(){if(!this.backend)throw new o("SQLite database not initialized. Call initialize() first.");return this.backend}requireCache(){if(!this.stmtCache)throw new o("SQLite database not initialized. Call initialize() first.");return this.stmtCache}assertInitialized(){if(!this.initialized)throw new o("SyncSqliteConnection not initialized. Call initialize() first.")}walModeDefault(){return true}foreignKeysDefault(){return true}readonlyDefault(){return false}busyTimeoutDefault(){return 5e3}walShmPathDefault(){}syncStmtCacheAccessor(){return this.requireCache()}},N=class{constructor(e){i(this,"backend",e);i(this,"cache",new Map);}get(e){let t=this.cache.get(e);if(t)return this.cache.delete(e),this.cache.set(e,t),t;let r=this.backend.prepare(e);if(this.cache.size>=he){let a=this.cache.keys().next().value;a!==void 0&&this.cache.delete(a);}return this.cache.set(e,r),r}clear(){this.cache.clear();}};function pe(n){let e=n.trimStart();if(e.length===0)return false;let a=((e.split(`
7
+ `,1)[0]??"").trimStart().split(/\s+/,1)[0]??"").toUpperCase();return a==="CREATE"||a==="DROP"||a==="ALTER"||a==="TRUNCATE"||a==="REPLACE"}function V(n){return n instanceof Error?n.message:String(n)}function me(n={}){let e=n.backend===void 0||n.backend==="auto"?w():n.backend,t=new D({...n,backend:e}),r=n.path??":memory:";if(r!==":memory:"){let a=p.dirname(r);try{y.mkdirSync(a,{recursive:!0});}catch(s){throw new o(`Failed to ensure directory for SQLite database at "${r}"`,s)}}return t}export{o as ConnectionError,x as DatabaseError,A as DatabaseFactory,T as PostgresDatabase,c as QueryError,v as SqliteDatabase,D as SyncSqliteConnection,l as TransactionError,me as createSyncSqliteConnection,A as default,q as isSafeIdent,w as resolveDefaultSqliteBackend,C as translatePlaceholders};
package/package.json ADDED
@@ -0,0 +1,95 @@
1
+ {
2
+ "name": "@easbot/database",
3
+ "version": "0.3.3",
4
+ "description": "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.",
5
+ "type": "module",
6
+ "main": "dist/index.cjs",
7
+ "module": "dist/index.mjs",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.mjs",
13
+ "require": "./dist/index.cjs"
14
+ },
15
+ "./package.json": "./package.json"
16
+ },
17
+ "keywords": [
18
+ "easbot",
19
+ "database",
20
+ "sqlite",
21
+ "postgresql",
22
+ "turso",
23
+ "mcp",
24
+ "ai",
25
+ "agent",
26
+ "storage",
27
+ "transaction",
28
+ "savepoint"
29
+ ],
30
+ "author": "houjallen",
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/houjallen/easbot.git",
35
+ "directory": "packages/database"
36
+ },
37
+ "homepage": "https://github.com/houjallen/easbot/tree/main/packages/database",
38
+ "bugs": {
39
+ "url": "https://github.com/houjallen/easbot/issues"
40
+ },
41
+ "files": [
42
+ "dist",
43
+ "README.md",
44
+ "README.en.md",
45
+ "LICENSE"
46
+ ],
47
+ "dependencies": {
48
+ "pg": "^8.20.0",
49
+ "zod": "^4.4.3"
50
+ },
51
+ "peerDependencies": {
52
+ "better-sqlite3": "^12.9.0"
53
+ },
54
+ "peerDependenciesMeta": {
55
+ "better-sqlite3": {
56
+ "optional": true
57
+ }
58
+ },
59
+ "optionalDependencies": {
60
+ "@tursodatabase/database": "^0.5.3"
61
+ },
62
+ "devDependencies": {
63
+ "@biomejs/biome": "^2.4.14",
64
+ "@types/better-sqlite3": "^7.6.13",
65
+ "@types/node": "^25.6.2",
66
+ "@types/pg": "^8.20.0",
67
+ "@vitest/coverage-v8": "^4.1.5",
68
+ "better-sqlite3": "^12.9.0",
69
+ "dotenv": "^17.4.2",
70
+ "tsup": "^8.5.1",
71
+ "typescript": "^6.0.3",
72
+ "vitest": "^4.1.5"
73
+ },
74
+ "engines": {
75
+ "node": ">=22.22.3"
76
+ },
77
+ "publishConfig": {
78
+ "access": "public"
79
+ },
80
+ "scripts": {
81
+ "dev": "tsup --watch --env.NODE_ENV development",
82
+ "build": "tsup --env.NODE_ENV production",
83
+ "test": "vitest",
84
+ "test:run": "vitest run",
85
+ "type-check": "tsc --noEmit",
86
+ "lint": "biome check .",
87
+ "lint:fix": "biome check --write .",
88
+ "lint:report": "biome check --reporter=summary .",
89
+ "format": "biome format .",
90
+ "format:fix": "biome format --write .",
91
+ "clean": "npx rimraf dist node_modules",
92
+ "publish:npm": "bash scripts/publish.sh",
93
+ "publish:npm:win": "powershell -ExecutionPolicy Bypass -File scripts/publish.ps1"
94
+ }
95
+ }