@yunzai-ng/core 0.1.0 → 0.2.0

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/src/store/sql.ts CHANGED
@@ -1,324 +1,324 @@
1
- /**
2
- * 模块职责:SQLite 关系存储(语句缓存、异步事务、迁移)
3
- * 依赖方向:动态依赖可选原生模块 better-sqlite3;依赖 util/{lru,queue}、类型包
4
- * 生命周期:`openSql()` 打开,`close()` 关闭
5
- * 注意事项:**不引 ORM**,只暴露 run/all/get/transaction/migrate。
6
- *
7
- * **事务手写 BEGIN/COMMIT 并用互斥锁串行化**:better-sqlite3 的 `db.transaction()`
8
- * 只收同步函数,而本框架的接口是异步的(要容得下别的驱动)。一条连接上开不了两个
9
- * 事务,故事务之间必须排队;嵌套事务改用 SAVEPOINT,不重复抢锁。
10
- *
11
- * **语句缓存有界。** `prepare()` 有真实开销,但无上限的缓存在动态拼 SQL 的插件手里
12
- * 就是内存泄漏。
13
- */
14
- import type { SqlHandle, SqlMigration, SqlParam } from "@yunzai-ng/types"
15
- import { dirname } from "node:path"
16
- import { LruCache } from "../util/lru.js"
17
- import { Semaphore } from "../util/queue.js"
18
- import { ensureDir } from "../util/fs.js"
19
-
20
- /** 语句缓存条目上限 */
21
- const STMT_CACHE_MAX = 200
22
-
23
- /** 迁移记录表名 */
24
- const MIGRATION_TABLE = "_yzng_migrations"
25
-
26
- /** better-sqlite3 的语句最小面 */
27
- interface SqliteStatement {
28
- /** 执行不取行 */
29
- run(...params: SqlParam[]): { changes: number; lastInsertRowid: number | bigint }
30
- /** 取全部行 */
31
- all(...params: SqlParam[]): unknown[]
32
- /** 取首行 */
33
- get(...params: SqlParam[]): unknown
34
- }
35
-
36
- /** better-sqlite3 的连接最小面 */
37
- interface SqliteDb {
38
- /** 预编译语句 */
39
- prepare(sql: string): SqliteStatement
40
- /** 直接执行脚本(可含多条语句,不支持绑定参数) */
41
- exec(sql: string): void
42
- /** 读写 pragma */
43
- pragma(source: string): unknown
44
- /** 关闭连接 */
45
- close(): void
46
- }
47
-
48
- /** better-sqlite3 的构造签名 */
49
- type SqliteCtor = new (file: string, opts?: { readonly?: boolean }) => SqliteDb
50
-
51
- /** 一条连接上的共享状态 */
52
- interface SqlState {
53
- /** 连接 */
54
- db: SqliteDb
55
- /** 语句缓存 */
56
- stmts: LruCache<SqliteStatement>
57
- /** 事务互斥锁 */
58
- mutex: Semaphore
59
- /** SAVEPOINT 命名计数器 */
60
- savepoint: number
61
- }
62
-
63
- /**
64
- * 尝试加载 better-sqlite3
65
- * @returns 构造函数;模块不可用时 undefined
66
- */
67
- export async function loadSqlite(): Promise<SqliteCtor | undefined> {
68
- try {
69
- const mod = (await import("better-sqlite3")) as unknown as { default?: SqliteCtor }
70
- return mod.default
71
- } catch {
72
- return undefined
73
- }
74
- }
75
-
76
- /**
77
- * SQLite 句柄
78
- *
79
- * 根句柄由 `openSql()` 创建;事务内传给回调的是 `depth > 0` 的子句柄,
80
- * 它们共享同一条连接,因此**不能**在事务回调里持有子句柄留到事务外使用。
81
- */
82
- export class SqliteHandle implements SqlHandle {
83
- /** 共享状态 */
84
- readonly #state: SqlState
85
- /** 事务嵌套深度,0 表示不在事务中 */
86
- readonly #depth: number
87
-
88
- /**
89
- * @param state 共享状态
90
- * @param depth 事务嵌套深度
91
- */
92
- constructor(state: SqlState, depth = 0) {
93
- this.#state = state
94
- this.#depth = depth
95
- }
96
-
97
- /**
98
- * 执行不返回行的语句
99
- *
100
- * 不带参数且含多条语句时走 `exec()`,方便迁移里一次建表 + 建索引;
101
- * 这种情况下无法报告受影响行数,返回 0。
102
- * @param sql SQL 文本
103
- * @param params 绑定参数
104
- * @returns 受影响行数与最后插入 id
105
- */
106
- async run(sql: string, params: SqlParam[] = []): Promise<{ changes: number; lastInsertRowid: number | bigint }> {
107
- if (params.length === 0 && isMultiStatement(sql)) {
108
- this.#state.db.exec(sql)
109
- return { changes: 0, lastInsertRowid: 0 }
110
- }
111
- return this.#prepare(sql).run(...params)
112
- }
113
-
114
- /**
115
- * 查询多行
116
- * @param sql SQL 文本
117
- * @param params 绑定参数
118
- * @returns 结果行数组
119
- */
120
- async all<T = Record<string, unknown>>(sql: string, params: SqlParam[] = []): Promise<T[]> {
121
- return this.#prepare(sql).all(...params) as T[]
122
- }
123
-
124
- /**
125
- * 查询首行
126
- * @param sql SQL 文本
127
- * @param params 绑定参数
128
- * @returns 首行;无结果时 undefined
129
- */
130
- async get<T = Record<string, unknown>>(sql: string, params: SqlParam[] = []): Promise<T | undefined> {
131
- return (this.#prepare(sql).get(...params) as T | undefined) ?? undefined
132
- }
133
-
134
- /**
135
- * 在事务中执行
136
- *
137
- * 最外层用 `BEGIN IMMEDIATE`(立刻拿写锁,避免升级锁时才发现冲突而整段重来);
138
- * 嵌套层用 SAVEPOINT,实现"内层回滚不牵连外层"。
139
- * @param fn 事务体,抛错即回滚
140
- * @returns 事务体的返回值
141
- */
142
- async transaction<T>(fn: (tx: SqlHandle) => Promise<T>): Promise<T> {
143
- if (this.#depth > 0) return this.#savepoint(fn)
144
- // 同一条连接上不能并发开事务,排队等前一个事务结束
145
- return this.#state.mutex.use(async () => {
146
- const tx = new SqliteHandle(this.#state, 1)
147
- this.#state.db.exec("BEGIN IMMEDIATE")
148
- try {
149
- const result = await fn(tx)
150
- this.#state.db.exec("COMMIT")
151
- return result
152
- } catch (err) {
153
- // 回滚本身也可能失败(连接已断),此时保留原始错误更有诊断价值
154
- try {
155
- this.#state.db.exec("ROLLBACK")
156
- } catch {
157
- /* 忽略 */
158
- }
159
- throw err
160
- }
161
- })
162
- }
163
-
164
- /**
165
- * 应用迁移
166
- *
167
- * 每条迁移单独一个事务:失败只回滚这一条,已成功的保持生效,修好后重跑从断点继续。
168
- * 版本号落在迁移表里,故"这个库到哪一版了"有确定答案,不必靠"存在则忽略"的 try/catch。
169
- * @param migrations 迁移列表
170
- * @throws 版本号未严格递增,或某条迁移执行失败
171
- */
172
- async migrate(migrations: SqlMigration[]): Promise<void> {
173
- await this.run(
174
- `CREATE TABLE IF NOT EXISTS ${MIGRATION_TABLE} (
175
- version INTEGER PRIMARY KEY,
176
- name TEXT NOT NULL,
177
- applied_at INTEGER NOT NULL
178
- )`
179
- )
180
-
181
- const sorted = [...migrations].sort((a, b) => a.version - b.version)
182
- for (let i = 1; i < sorted.length; i++) {
183
- if (sorted[i]!.version === sorted[i - 1]!.version) {
184
- throw new Error(`迁移版本号重复:${sorted[i]!.version}`)
185
- }
186
- }
187
-
188
- const rows = await this.all<{ version: number }>(`SELECT version FROM ${MIGRATION_TABLE}`)
189
- const applied = new Set(rows.map(r => r.version))
190
-
191
- for (const migration of sorted) {
192
- if (applied.has(migration.version)) continue
193
- await this.transaction(async tx => {
194
- await migration.up(tx)
195
- await tx.run(`INSERT INTO ${MIGRATION_TABLE} (version, name, applied_at) VALUES (?, ?, ?)`, [
196
- migration.version,
197
- migration.name,
198
- Date.now()
199
- ])
200
- })
201
- }
202
- }
203
-
204
- /**
205
- * 嵌套事务:SAVEPOINT
206
- * @param fn 事务体
207
- * @returns 事务体的返回值
208
- */
209
- async #savepoint<T>(fn: (tx: SqlHandle) => Promise<T>): Promise<T> {
210
- const name = `yzng_sp_${++this.#state.savepoint}`
211
- const tx = new SqliteHandle(this.#state, this.#depth + 1)
212
- this.#state.db.exec(`SAVEPOINT ${name}`)
213
- try {
214
- const result = await fn(tx)
215
- this.#state.db.exec(`RELEASE ${name}`)
216
- return result
217
- } catch (err) {
218
- try {
219
- this.#state.db.exec(`ROLLBACK TO ${name}`)
220
- this.#state.db.exec(`RELEASE ${name}`)
221
- } catch {
222
- /* 忽略 */
223
- }
224
- throw err
225
- }
226
- }
227
-
228
- /**
229
- * 取(并缓存)预编译语句
230
- * @param sql SQL 文本
231
- * @returns 预编译语句
232
- */
233
- #prepare(sql: string): SqliteStatement {
234
- const cached = this.#state.stmts.get(sql)
235
- if (cached) return cached
236
- const stmt = this.#state.db.prepare(sql)
237
- this.#state.stmts.set(sql, stmt)
238
- return stmt
239
- }
240
- }
241
-
242
- /** 打开 SQL 存储的参数 */
243
- export interface OpenSqlOptions {
244
- /** 数据库文件绝对路径 */
245
- file: string
246
- /** 构造函数,缺省自动加载 */
247
- ctor?: SqliteCtor
248
- }
249
-
250
- /** 已打开的 SQL 存储 */
251
- export interface SqlStore {
252
- /** 句柄 */
253
- readonly handle: SqlHandle
254
- /** 数据库文件路径 */
255
- readonly file: string
256
- /** 关闭连接 */
257
- close(): void
258
- }
259
-
260
- /**
261
- * 打开 SQLite 数据库
262
- * @param opts 参数
263
- * @returns 已打开的 SQL 存储;原生模块不可用时 undefined(调用方应降级为纯 KV)
264
- */
265
- export async function openSql(opts: OpenSqlOptions): Promise<SqlStore | undefined> {
266
- const ctor = opts.ctor ?? (await loadSqlite())
267
- if (!ctor) return undefined
268
-
269
- await ensureDir(dirname(opts.file))
270
- const db = new ctor(opts.file)
271
-
272
- // WAL:读不阻塞写,且断电时不会像 rollback journal 那样留下半个事务
273
- db.pragma("journal_mode = WAL")
274
- // NORMAL 在 WAL 下依然崩溃安全(只可能丢最后一个未 checkpoint 的事务),
275
- // 却比 FULL 快一个数量级;机器人数据不是账本,这个取舍是合适的
276
- db.pragma("synchronous = NORMAL")
277
- db.pragma("foreign_keys = ON")
278
- // 被别的连接短暂锁住时等一会儿再报错,而不是立刻抛 SQLITE_BUSY
279
- db.pragma("busy_timeout = 5000")
280
-
281
- const state: SqlState = {
282
- db,
283
- stmts: new LruCache<SqliteStatement>({ max: STMT_CACHE_MAX }),
284
- mutex: new Semaphore(1),
285
- savepoint: 0
286
- }
287
-
288
- return {
289
- handle: new SqliteHandle(state),
290
- file: opts.file,
291
- close: () => {
292
- // 缓存里的语句必须先丢掉:连接关了之后再用它们会直接崩进程
293
- state.stmts.clear()
294
- db.close()
295
- }
296
- }
297
- }
298
-
299
- /**
300
- * 判断 SQL 是否包含多条语句
301
- *
302
- * 只做粗判:跳过字符串字面量后看是否还有 `;` 跟着非空白内容。
303
- * 粗判失败的后果仅仅是走 `prepare()` 然后由 SQLite 自己报错,可接受。
304
- * @param sql SQL 文本
305
- * @returns 是否多语句
306
- */
307
- function isMultiStatement(sql: string): boolean {
308
- let inString = false
309
- let quote = ""
310
- for (let i = 0; i < sql.length; i++) {
311
- const ch = sql[i]!
312
- if (inString) {
313
- if (ch === quote) inString = false
314
- continue
315
- }
316
- if (ch === "'" || ch === '"') {
317
- inString = true
318
- quote = ch
319
- continue
320
- }
321
- if (ch === ";" && sql.slice(i + 1).trim() !== "") return true
322
- }
323
- return false
324
- }
1
+ /**
2
+ * 模块职责:SQLite 关系存储(语句缓存、异步事务、迁移)
3
+ * 依赖方向:动态依赖可选原生模块 better-sqlite3;依赖 util/{lru,queue}、类型包
4
+ * 生命周期:`openSql()` 打开,`close()` 关闭
5
+ * 注意事项:**不引 ORM**,只暴露 run/all/get/transaction/migrate。
6
+ *
7
+ * **事务手写 BEGIN/COMMIT 并用互斥锁串行化**:better-sqlite3 的 `db.transaction()`
8
+ * 只收同步函数,而本框架的接口是异步的(要容得下别的驱动)。一条连接上开不了两个
9
+ * 事务,故事务之间必须排队;嵌套事务改用 SAVEPOINT,不重复抢锁。
10
+ *
11
+ * **语句缓存有界。** `prepare()` 有真实开销,但无上限的缓存在动态拼 SQL 的插件手里
12
+ * 就是内存泄漏。
13
+ */
14
+ import type { SqlHandle, SqlMigration, SqlParam } from "@yunzai-ng/types"
15
+ import { dirname } from "node:path"
16
+ import { LruCache } from "../util/lru.js"
17
+ import { Semaphore } from "../util/queue.js"
18
+ import { ensureDir } from "../util/fs.js"
19
+
20
+ /** 语句缓存条目上限 */
21
+ const STMT_CACHE_MAX = 200
22
+
23
+ /** 迁移记录表名 */
24
+ const MIGRATION_TABLE = "_yzng_migrations"
25
+
26
+ /** better-sqlite3 的语句最小面 */
27
+ interface SqliteStatement {
28
+ /** 执行不取行 */
29
+ run(...params: SqlParam[]): { changes: number; lastInsertRowid: number | bigint }
30
+ /** 取全部行 */
31
+ all(...params: SqlParam[]): unknown[]
32
+ /** 取首行 */
33
+ get(...params: SqlParam[]): unknown
34
+ }
35
+
36
+ /** better-sqlite3 的连接最小面 */
37
+ interface SqliteDb {
38
+ /** 预编译语句 */
39
+ prepare(sql: string): SqliteStatement
40
+ /** 直接执行脚本(可含多条语句,不支持绑定参数) */
41
+ exec(sql: string): void
42
+ /** 读写 pragma */
43
+ pragma(source: string): unknown
44
+ /** 关闭连接 */
45
+ close(): void
46
+ }
47
+
48
+ /** better-sqlite3 的构造签名 */
49
+ type SqliteCtor = new (file: string, opts?: { readonly?: boolean }) => SqliteDb
50
+
51
+ /** 一条连接上的共享状态 */
52
+ interface SqlState {
53
+ /** 连接 */
54
+ db: SqliteDb
55
+ /** 语句缓存 */
56
+ stmts: LruCache<SqliteStatement>
57
+ /** 事务互斥锁 */
58
+ mutex: Semaphore
59
+ /** SAVEPOINT 命名计数器 */
60
+ savepoint: number
61
+ }
62
+
63
+ /**
64
+ * 尝试加载 better-sqlite3
65
+ * @returns 构造函数;模块不可用时 undefined
66
+ */
67
+ export async function loadSqlite(): Promise<SqliteCtor | undefined> {
68
+ try {
69
+ const mod = (await import("better-sqlite3")) as unknown as { default?: SqliteCtor }
70
+ return mod.default
71
+ } catch {
72
+ return undefined
73
+ }
74
+ }
75
+
76
+ /**
77
+ * SQLite 句柄
78
+ *
79
+ * 根句柄由 `openSql()` 创建;事务内传给回调的是 `depth > 0` 的子句柄,
80
+ * 它们共享同一条连接,因此**不能**在事务回调里持有子句柄留到事务外使用。
81
+ */
82
+ export class SqliteHandle implements SqlHandle {
83
+ /** 共享状态 */
84
+ readonly #state: SqlState
85
+ /** 事务嵌套深度,0 表示不在事务中 */
86
+ readonly #depth: number
87
+
88
+ /**
89
+ * @param state 共享状态
90
+ * @param depth 事务嵌套深度
91
+ */
92
+ constructor(state: SqlState, depth = 0) {
93
+ this.#state = state
94
+ this.#depth = depth
95
+ }
96
+
97
+ /**
98
+ * 执行不返回行的语句
99
+ *
100
+ * 不带参数且含多条语句时走 `exec()`,方便迁移里一次建表 + 建索引;
101
+ * 这种情况下无法报告受影响行数,返回 0。
102
+ * @param sql SQL 文本
103
+ * @param params 绑定参数
104
+ * @returns 受影响行数与最后插入 id
105
+ */
106
+ async run(sql: string, params: SqlParam[] = []): Promise<{ changes: number; lastInsertRowid: number | bigint }> {
107
+ if (params.length === 0 && isMultiStatement(sql)) {
108
+ this.#state.db.exec(sql)
109
+ return { changes: 0, lastInsertRowid: 0 }
110
+ }
111
+ return this.#prepare(sql).run(...params)
112
+ }
113
+
114
+ /**
115
+ * 查询多行
116
+ * @param sql SQL 文本
117
+ * @param params 绑定参数
118
+ * @returns 结果行数组
119
+ */
120
+ async all<T = Record<string, unknown>>(sql: string, params: SqlParam[] = []): Promise<T[]> {
121
+ return this.#prepare(sql).all(...params) as T[]
122
+ }
123
+
124
+ /**
125
+ * 查询首行
126
+ * @param sql SQL 文本
127
+ * @param params 绑定参数
128
+ * @returns 首行;无结果时 undefined
129
+ */
130
+ async get<T = Record<string, unknown>>(sql: string, params: SqlParam[] = []): Promise<T | undefined> {
131
+ return (this.#prepare(sql).get(...params) as T | undefined) ?? undefined
132
+ }
133
+
134
+ /**
135
+ * 在事务中执行
136
+ *
137
+ * 最外层用 `BEGIN IMMEDIATE`(立刻拿写锁,避免升级锁时才发现冲突而整段重来);
138
+ * 嵌套层用 SAVEPOINT,实现"内层回滚不牵连外层"。
139
+ * @param fn 事务体,抛错即回滚
140
+ * @returns 事务体的返回值
141
+ */
142
+ async transaction<T>(fn: (tx: SqlHandle) => Promise<T>): Promise<T> {
143
+ if (this.#depth > 0) return this.#savepoint(fn)
144
+ // 同一条连接上不能并发开事务,排队等前一个事务结束
145
+ return this.#state.mutex.use(async () => {
146
+ const tx = new SqliteHandle(this.#state, 1)
147
+ this.#state.db.exec("BEGIN IMMEDIATE")
148
+ try {
149
+ const result = await fn(tx)
150
+ this.#state.db.exec("COMMIT")
151
+ return result
152
+ } catch (err) {
153
+ // 回滚本身也可能失败(连接已断),此时保留原始错误更有诊断价值
154
+ try {
155
+ this.#state.db.exec("ROLLBACK")
156
+ } catch {
157
+ /* 忽略 */
158
+ }
159
+ throw err
160
+ }
161
+ })
162
+ }
163
+
164
+ /**
165
+ * 应用迁移
166
+ *
167
+ * 每条迁移单独一个事务:失败只回滚这一条,已成功的保持生效,修好后重跑从断点继续。
168
+ * 版本号落在迁移表里,故"这个库到哪一版了"有确定答案,不必靠"存在则忽略"的 try/catch。
169
+ * @param migrations 迁移列表
170
+ * @throws 版本号未严格递增,或某条迁移执行失败
171
+ */
172
+ async migrate(migrations: SqlMigration[]): Promise<void> {
173
+ await this.run(
174
+ `CREATE TABLE IF NOT EXISTS ${MIGRATION_TABLE} (
175
+ version INTEGER PRIMARY KEY,
176
+ name TEXT NOT NULL,
177
+ applied_at INTEGER NOT NULL
178
+ )`
179
+ )
180
+
181
+ const sorted = [...migrations].sort((a, b) => a.version - b.version)
182
+ for (let i = 1; i < sorted.length; i++) {
183
+ if (sorted[i]!.version === sorted[i - 1]!.version) {
184
+ throw new Error(`迁移版本号重复:${sorted[i]!.version}`)
185
+ }
186
+ }
187
+
188
+ const rows = await this.all<{ version: number }>(`SELECT version FROM ${MIGRATION_TABLE}`)
189
+ const applied = new Set(rows.map(r => r.version))
190
+
191
+ for (const migration of sorted) {
192
+ if (applied.has(migration.version)) continue
193
+ await this.transaction(async tx => {
194
+ await migration.up(tx)
195
+ await tx.run(`INSERT INTO ${MIGRATION_TABLE} (version, name, applied_at) VALUES (?, ?, ?)`, [
196
+ migration.version,
197
+ migration.name,
198
+ Date.now()
199
+ ])
200
+ })
201
+ }
202
+ }
203
+
204
+ /**
205
+ * 嵌套事务:SAVEPOINT
206
+ * @param fn 事务体
207
+ * @returns 事务体的返回值
208
+ */
209
+ async #savepoint<T>(fn: (tx: SqlHandle) => Promise<T>): Promise<T> {
210
+ const name = `yzng_sp_${++this.#state.savepoint}`
211
+ const tx = new SqliteHandle(this.#state, this.#depth + 1)
212
+ this.#state.db.exec(`SAVEPOINT ${name}`)
213
+ try {
214
+ const result = await fn(tx)
215
+ this.#state.db.exec(`RELEASE ${name}`)
216
+ return result
217
+ } catch (err) {
218
+ try {
219
+ this.#state.db.exec(`ROLLBACK TO ${name}`)
220
+ this.#state.db.exec(`RELEASE ${name}`)
221
+ } catch {
222
+ /* 忽略 */
223
+ }
224
+ throw err
225
+ }
226
+ }
227
+
228
+ /**
229
+ * 取(并缓存)预编译语句
230
+ * @param sql SQL 文本
231
+ * @returns 预编译语句
232
+ */
233
+ #prepare(sql: string): SqliteStatement {
234
+ const cached = this.#state.stmts.get(sql)
235
+ if (cached) return cached
236
+ const stmt = this.#state.db.prepare(sql)
237
+ this.#state.stmts.set(sql, stmt)
238
+ return stmt
239
+ }
240
+ }
241
+
242
+ /** 打开 SQL 存储的参数 */
243
+ export interface OpenSqlOptions {
244
+ /** 数据库文件绝对路径 */
245
+ file: string
246
+ /** 构造函数,缺省自动加载 */
247
+ ctor?: SqliteCtor
248
+ }
249
+
250
+ /** 已打开的 SQL 存储 */
251
+ export interface SqlStore {
252
+ /** 句柄 */
253
+ readonly handle: SqlHandle
254
+ /** 数据库文件路径 */
255
+ readonly file: string
256
+ /** 关闭连接 */
257
+ close(): void
258
+ }
259
+
260
+ /**
261
+ * 打开 SQLite 数据库
262
+ * @param opts 参数
263
+ * @returns 已打开的 SQL 存储;原生模块不可用时 undefined(调用方应降级为纯 KV)
264
+ */
265
+ export async function openSql(opts: OpenSqlOptions): Promise<SqlStore | undefined> {
266
+ const ctor = opts.ctor ?? (await loadSqlite())
267
+ if (!ctor) return undefined
268
+
269
+ await ensureDir(dirname(opts.file))
270
+ const db = new ctor(opts.file)
271
+
272
+ // WAL:读不阻塞写,且断电时不会像 rollback journal 那样留下半个事务
273
+ db.pragma("journal_mode = WAL")
274
+ // NORMAL 在 WAL 下依然崩溃安全(只可能丢最后一个未 checkpoint 的事务),
275
+ // 却比 FULL 快一个数量级;机器人数据不是账本,这个取舍是合适的
276
+ db.pragma("synchronous = NORMAL")
277
+ db.pragma("foreign_keys = ON")
278
+ // 被别的连接短暂锁住时等一会儿再报错,而不是立刻抛 SQLITE_BUSY
279
+ db.pragma("busy_timeout = 5000")
280
+
281
+ const state: SqlState = {
282
+ db,
283
+ stmts: new LruCache<SqliteStatement>({ max: STMT_CACHE_MAX }),
284
+ mutex: new Semaphore(1),
285
+ savepoint: 0
286
+ }
287
+
288
+ return {
289
+ handle: new SqliteHandle(state),
290
+ file: opts.file,
291
+ close: () => {
292
+ // 缓存里的语句必须先丢掉:连接关了之后再用它们会直接崩进程
293
+ state.stmts.clear()
294
+ db.close()
295
+ }
296
+ }
297
+ }
298
+
299
+ /**
300
+ * 判断 SQL 是否包含多条语句
301
+ *
302
+ * 只做粗判:跳过字符串字面量后看是否还有 `;` 跟着非空白内容。
303
+ * 粗判失败的后果仅仅是走 `prepare()` 然后由 SQLite 自己报错,可接受。
304
+ * @param sql SQL 文本
305
+ * @returns 是否多语句
306
+ */
307
+ function isMultiStatement(sql: string): boolean {
308
+ let inString = false
309
+ let quote = ""
310
+ for (let i = 0; i < sql.length; i++) {
311
+ const ch = sql[i]!
312
+ if (inString) {
313
+ if (ch === quote) inString = false
314
+ continue
315
+ }
316
+ if (ch === "'" || ch === '"') {
317
+ inString = true
318
+ quote = ch
319
+ continue
320
+ }
321
+ if (ch === ";" && sql.slice(i + 1).trim() !== "") return true
322
+ }
323
+ return false
324
+ }