@h-ai/reldb 0.1.0-alpha.56 → 0.1.0-alpha.57

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/README.en.md ADDED
@@ -0,0 +1,520 @@
1
+ # @h-ai/reldb
2
+
3
+ [简体中文](./README.md) | [English](./README.en.md) | [日本語](./README.ja.md)
4
+
5
+ [Framework home](../../README.en.md) · Related modules: [@h-ai/core](../core/README.en.md)
6
+
7
+ A relational database module with a unified `reldb` object for SQLite, PostgreSQL, and MySQL.
8
+
9
+ ## Supported databases
10
+
11
+ - SQLite
12
+ - PostgreSQL
13
+ - MySQL
14
+
15
+ ## Quick start
16
+
17
+ ```ts
18
+ import { HaiReldbError, reldb } from '@h-ai/reldb'
19
+
20
+ // 初始化
21
+ await reldb.init({ type: 'sqlite', database: ':memory:' })
22
+
23
+ // DDL
24
+ await reldb.ddl.createTable('users', {
25
+ id: { type: 'INTEGER', primaryKey: true, autoIncrement: true },
26
+ name: { type: 'TEXT', notNull: true },
27
+ email: { type: 'TEXT', unique: true },
28
+ })
29
+
30
+ // SQL
31
+ await reldb.sql.execute('INSERT INTO users (name, email) VALUES (?, ?)', ['张三', 'test@example.com'])
32
+ const users = await reldb.sql.query<{ id: number, name: string }>('SELECT * FROM users')
33
+
34
+ // 分页查询
35
+ const page = await reldb.sql.queryPage<{ id: number, name: string }>({
36
+ sql: 'SELECT id, name FROM users ORDER BY id',
37
+ pagination: { page: 1, pageSize: 20 },
38
+ })
39
+
40
+ // CRUD
41
+ const userCrud = reldb.crud.table({
42
+ table: 'users',
43
+ idColumn: 'id',
44
+ select: ['id', 'name', 'email'],
45
+ createColumns: ['name', 'email'],
46
+ updateColumns: ['name', 'email'],
47
+ dbType: 'sqlite',
48
+ })
49
+ await userCrud.create({ name: '李四', email: 'li@test.com' })
50
+ const user = await userCrud.findById(1)
51
+
52
+ // 关闭
53
+ await reldb.close()
54
+ ```
55
+
56
+ `reldb.config` returns a **redacted configuration snapshot**. Database `password` and credentials embedded in URLs are hidden automatically. Use it to inspect state, not to replay original connection details.
57
+
58
+ ## Configuration
59
+
60
+ Use either a connection string (`url`) or separate fields (`host/port/database/user/password`).
61
+
62
+ ```ts
63
+ // SQLite
64
+ await reldb.init({ type: 'sqlite', database: './data.db' })
65
+
66
+ // PostgreSQL
67
+ await reldb.init({ type: 'postgresql', url: 'postgres://user:pass@localhost:5432/mydb' })
68
+
69
+ // MySQL
70
+ await reldb.init({
71
+ type: 'mysql',
72
+ host: 'localhost',
73
+ port: 3306,
74
+ database: 'mydb',
75
+ user: 'admin',
76
+ password: 'secret',
77
+ mysql: { charset: 'utf8mb4' },
78
+ })
79
+ ```
80
+
81
+ ### SQLite execution (asynchronous worker)
82
+
83
+ better-sqlite3 is synchronous, so queries block Node's main event loop. WAL improves database lock concurrency but cannot remove this blocking. To offload it, opt in to a dedicated SQLite worker thread (single connection; defaults to `sync`):
84
+
85
+ ```ts
86
+ await reldb.init({
87
+ type: 'sqlite',
88
+ database: './data.db',
89
+ sqlite: {
90
+ executor: 'worker', // 'sync'(默认)| 'worker'
91
+ walMode: true, // 默认 true
92
+ workerConnectTimeoutMs: 10000, // worker 打开连接超时,默认 10000
93
+ },
94
+ })
95
+ ```
96
+
97
+ In worker mode, queries, writes, and transactions run serially on the worker thread. Public APIs and return values remain identical to synchronous mode.
98
+
99
+ ### Operation logging
100
+
101
+ Enable SQL operation logs with `operationLog` in the database configuration. Logs are emitted at the DML layer of reldb provider-base, without duplicate wrappers in `reldb-main.ts` or raw providers.
102
+
103
+ ```yaml
104
+ # config/_db.yml
105
+ type: sqlite
106
+ database: ./data/app.db
107
+ operationLog:
108
+ read: false # query / get / queryPage
109
+ write: false # DDL, execute / batch, tx.begin / tx.commit / tx.rollback
110
+ maxLength: 1000 # 参数序列化后的最大输出长度,超出会截断
111
+ level: debug # info | debug | trace,默认 debug
112
+ ```
113
+
114
+ When loading through `core.config.load('db', ...)`, convention-based variables such as `HAI_DB_TYPE`,
115
+ `HAI_DB_DATABASE`, and `HAI_DB_OPERATIONLOG_LEVEL` override YAML values.
116
+
117
+ ```ts
118
+ await reldb.init({
119
+ type: 'sqlite',
120
+ database: './data.db',
121
+ operationLog: {
122
+ read: true,
123
+ write: true,
124
+ maxLength: 500,
125
+ level: 'debug',
126
+ },
127
+ })
128
+ ```
129
+
130
+ ## Transactions
131
+
132
+ Three approaches:
133
+
134
+ ### wrap (automatic commit/rollback)
135
+
136
+ ```ts
137
+ const result = await reldb.tx.wrap(async (tx) => {
138
+ await tx.execute('INSERT INTO users (name) VALUES (?)', ['用户1'])
139
+ await tx.execute('INSERT INTO users (name) VALUES (?)', ['用户2'])
140
+ return 'ok'
141
+ })
142
+ // 回调正常返回 → 自动提交;抛异常 → 自动回滚
143
+ ```
144
+
145
+ ### begin + commit (explicit commit)
146
+
147
+ ```ts
148
+ const txResult = await reldb.tx.begin()
149
+ if (!txResult.success) { /* 处理错误 */ }
150
+ const tx = txResult.data
151
+
152
+ await tx.execute('INSERT INTO users (name) VALUES (?)', ['用户1'])
153
+ await tx.execute('INSERT INTO users (name) VALUES (?)', ['用户2'])
154
+ await tx.commit()
155
+ ```
156
+
157
+ ### begin + rollback (explicit rollback)
158
+
159
+ ```ts
160
+ const txResult = await reldb.tx.begin()
161
+ if (!txResult.success) { /* 处理错误 */ }
162
+ const tx = txResult.data
163
+
164
+ await tx.execute('INSERT INTO users (name) VALUES (?)', ['用户1'])
165
+ // 业务检查失败,手动回滚
166
+ await tx.rollback()
167
+ ```
168
+
169
+ ### batch / queryPage inside transactions
170
+
171
+ ```ts
172
+ await reldb.tx.wrap(async (tx) => {
173
+ await tx.batch([
174
+ { sql: 'INSERT INTO users (name) VALUES (?)', params: ['用户1'] },
175
+ { sql: 'INSERT INTO users (name) VALUES (?)', params: ['用户2'] },
176
+ ])
177
+
178
+ const page = await tx.queryPage<{ id: number, name: string }>({
179
+ sql: 'SELECT id, name FROM users ORDER BY id',
180
+ pagination: { page: 1, pageSize: 10 },
181
+ })
182
+ })
183
+ ```
184
+
185
+ ## Crud
186
+
187
+ Create a single-table CRUD repository with `reldb.crud.table()`, without writing SQL:
188
+
189
+ ```ts
190
+ const userCrud = reldb.crud.table<{ id: number, name: string, email: string }>({
191
+ table: 'users',
192
+ idColumn: 'id', // 主键列,默认 'id'
193
+ select: ['id', 'name', 'email'], // 查询列,默认 '*'
194
+ createColumns: ['name', 'email'], // 允许插入的列
195
+ updateColumns: ['name', 'email'], // 允许更新的列
196
+ dbType: 'sqlite', // 数据库类型
197
+ })
198
+ ```
199
+
200
+ ### create / createMany
201
+
202
+ ```ts
203
+ // 单条插入
204
+ const result = await userCrud.create({ name: '张三', email: 'test@example.com' })
205
+ // result.data → { changes: 1, lastInsertRowid: 1 }
206
+
207
+ // 批量插入
208
+ await userCrud.createMany([
209
+ { name: '用户A', email: 'a@test.com' },
210
+ { name: '用户B', email: 'b@test.com' },
211
+ ])
212
+ ```
213
+
214
+ ### createOrUpdate
215
+
216
+ Update when the primary key exists; otherwise create (upsert):
217
+
218
+ ```ts
219
+ const userCrud = reldb.crud.table<{ id: number, name: string, email: string }>({
220
+ table: 'users',
221
+ idColumn: 'id',
222
+ createColumns: ['id', 'name', 'email'],
223
+ updateColumns: ['name', 'email'],
224
+ dbType: 'sqlite',
225
+ })
226
+
227
+ await userCrud.createOrUpdate({ id: 1, name: '张三', email: 'test@example.com' })
228
+ // 不存在 → 插入;已存在 → 更新 name、email
229
+ ```
230
+
231
+ `BaseReldbCrudRepository` obtains `dbType` automatically; no manual argument is needed.
232
+
233
+ ### findById / getById / findAll / findPage
234
+
235
+ ```ts
236
+ // 主键查找(不存在时返回 null)
237
+ const user = await userCrud.findById(1)
238
+ // user.data → { id: 1, name: '张三', email: 'test@example.com' } | null
239
+
240
+ // 主键获取(不存在时返回 RECORD_NOT_FOUND 错误)
241
+ const user2 = await userCrud.getById(1)
242
+ // user2.data → { id: 1, name: '张三', email: 'test@example.com' }
243
+
244
+ // 条件查询(where + params 占位符)
245
+ const actives = await userCrud.findAll({
246
+ where: 'name LIKE ?',
247
+ params: ['%张%'],
248
+ orderBy: 'id DESC',
249
+ limit: 10,
250
+ offset: 0,
251
+ })
252
+
253
+ // 分页查询
254
+ const page = await userCrud.findPage({
255
+ where: 'name LIKE ?',
256
+ params: ['%张%'],
257
+ orderBy: 'id DESC',
258
+ pagination: { page: 1, pageSize: 20 },
259
+ })
260
+ // page.data → { items: [...], total: 100, page: 1, pageSize: 20 }
261
+ ```
262
+
263
+ ### updateById / deleteById
264
+
265
+ ```ts
266
+ await userCrud.updateById(1, { name: '李四' })
267
+ await userCrud.deleteById(1)
268
+ ```
269
+
270
+ ### count / exists / existsById
271
+
272
+ ```ts
273
+ const total = await userCrud.count({ where: 'name LIKE ?', params: ['%张%'] })
274
+ // total.data → 5
275
+
276
+ const has = await userCrud.exists({ where: 'email = ?', params: ['test@example.com'] })
277
+ // has.data → true
278
+
279
+ const found = await userCrud.existsById(1)
280
+ // found.data → true
281
+ ```
282
+
283
+ ### Crud inside transactions
284
+
285
+ All methods accept `tx` and automatically route operations to that transaction context:
286
+
287
+ ```ts
288
+ await reldb.tx.wrap(async (tx) => {
289
+ await userCrud.create({ name: '用户A', email: 'a@test.com' }, tx)
290
+ await userCrud.updateById(1, { name: '新名字' }, tx)
291
+ const user = await userCrud.findById(1, tx)
292
+ })
293
+ ```
294
+
295
+ ## BaseReldbCrudRepository
296
+
297
+ Business repositories extend `BaseReldbCrudRepository` and define field mappings with `fields` for automatic table creation and type conversion:
298
+
299
+ ```ts
300
+ import type { DmlWithTxOperations, ReldbCrudFieldDefinition } from '@h-ai/reldb'
301
+ import { BaseReldbCrudRepository, reldb } from '@h-ai/reldb'
302
+
303
+ interface UserRow {
304
+ id: number
305
+ name: string
306
+ email: string
307
+ createdAt: Date
308
+ updatedAt: Date
309
+ }
310
+
311
+ const USER_FIELDS: ReldbCrudFieldDefinition[] = [
312
+ { fieldName: 'id', columnName: 'id', def: { type: 'INTEGER', primaryKey: true, autoIncrement: true }, select: true, create: false, update: false },
313
+ { fieldName: 'name', columnName: 'name', def: { type: 'TEXT', notNull: true }, select: true, create: true, update: true },
314
+ { fieldName: 'email', columnName: 'email', def: { type: 'TEXT', notNull: true }, select: true, create: true, update: true },
315
+ { fieldName: 'createdAt', columnName: 'created_at', def: { type: 'TIMESTAMP', notNull: true }, select: true, create: true, update: false },
316
+ { fieldName: 'updatedAt', columnName: 'updated_at', def: { type: 'TIMESTAMP', notNull: true }, select: true, create: true, update: false },
317
+ ]
318
+
319
+ class UserRepository extends BaseReldbCrudRepository<UserRow> {
320
+ constructor() {
321
+ super(reldb, { table: 'users', idColumn: 'id', fields: USER_FIELDS })
322
+ }
323
+
324
+ /** 自定义查询方法 */
325
+ async findByEmail(email: string) {
326
+ return this.findAll({ where: 'email = ?', params: [email], limit: 1 })
327
+ }
328
+
329
+ /** 自定义方法中使用 this.sql(tx) 自动路由到事务 */
330
+ async insertRaw(data: { name: string, email: string }, tx?: DmlWithTxOperations) {
331
+ // 自定义原始写操作应先确保表已就绪(避免刚建库/建表较慢时的竞态)
332
+ const ready = await this.ensureReady()
333
+ if (!ready.success)
334
+ return ready
335
+ const now = Date.now()
336
+ return this.sql(tx).execute(
337
+ 'INSERT INTO users (name, email, created_at, updated_at) VALUES (?, ?, ?, ?)',
338
+ [data.name, data.email, now, now],
339
+ )
340
+ }
341
+ }
342
+
343
+ // 使用
344
+ const repo = new UserRepository()
345
+ await repo.create({ name: '张三', email: 'test@example.com' })
346
+ const user = await repo.findById(1)
347
+
348
+ // 创建或更新(upsert)
349
+ await repo.createOrUpdate({ id: 1, name: '李四', email: 'test@example.com' })
350
+ // id=1 已存在 → 更新 name;不存在 → 插入新记录
351
+
352
+ // 跨仓库事务
353
+ const txResult = await reldb.tx.begin()
354
+ if (txResult.success) {
355
+ const tx = txResult.data
356
+ await repo.create({ name: '用户A', email: 'a@test.com' }, tx)
357
+ await repo.create({ name: '用户B', email: 'b@test.com' }, tx)
358
+ await tx.commit()
359
+ }
360
+ ```
361
+
362
+ ## JSON path operations
363
+
364
+ Use `reldb.json` to build SQL expressions for consistent JSON path operations across databases.
365
+
366
+ Paths follow SQL/JSON Path (`$.key` / `$.key.subkey` / `$[0]`); each backend maps them to its own functions or operators.
367
+
368
+ | Operation | SQLite | PostgreSQL | MySQL |
369
+ | --------- | -------------- | -------------- | ------------------ |
370
+ | `extract` | `json_extract` | `#>` (text[]) | `JSON_EXTRACT` |
371
+ | `set` | `json_set` | `jsonb_set` | `JSON_SET` |
372
+ | `insert` | `json_insert` | `jsonb_insert` | `JSON_INSERT` |
373
+ | `remove` | `json_remove` | `#-` (text[]) | `JSON_REMOVE` |
374
+ | `merge` | `json_patch` | `\|\| ::jsonb` | `JSON_MERGE_PATCH` |
375
+
376
+ All methods return `{ sql, params }` fragments that can be embedded in `reldb.sql.query` / `reldb.sql.execute`.
377
+
378
+ ### extract — read a JSON field value
379
+
380
+ ```ts
381
+ // 用于 WHERE 条件
382
+ const { sql, params } = reldb.json.extract('settings', '$.theme')
383
+ const rows = await reldb.sql.query(
384
+ `SELECT * FROM users WHERE ${sql} = ?`,
385
+ [...params, '"dark"'],
386
+ )
387
+
388
+ // 用于 SELECT 列
389
+ const { sql, params } = reldb.json.extract('profile', '$.name')
390
+ const result = await reldb.sql.query(
391
+ `SELECT id, ${sql} as display_name FROM users`,
392
+ params,
393
+ )
394
+ ```
395
+
396
+ ### set — set a JSON path (create or replace)
397
+
398
+ ```ts
399
+ const { sql, params } = reldb.json.set('settings', '$.theme', 'dark')
400
+ await reldb.sql.execute(
401
+ `UPDATE users SET settings = ${sql} WHERE id = ?`,
402
+ [...params, userId],
403
+ )
404
+ ```
405
+
406
+ ### insert — insert at a JSON path (only when absent)
407
+
408
+ ```ts
409
+ const { sql, params } = reldb.json.insert('data', '$.firstSeen', new Date().toISOString())
410
+ await reldb.sql.execute(
411
+ `UPDATE events SET data = ${sql} WHERE id = ?`,
412
+ [...params, eventId],
413
+ )
414
+ ```
415
+
416
+ ### remove — remove a JSON path
417
+
418
+ ```ts
419
+ const { sql, params } = reldb.json.remove('settings', '$.deprecated')
420
+ await reldb.sql.execute(
421
+ `UPDATE users SET settings = ${sql} WHERE id = ?`,
422
+ [...params, userId],
423
+ )
424
+ ```
425
+
426
+ ### merge — merge JSON objects (RFC 7396 Merge Patch)
427
+
428
+ ```ts
429
+ // 覆盖已有键、保留未提及键、将 null 键删除
430
+ const { sql, params } = reldb.json.merge('profile', {
431
+ bio: '新简介',
432
+ avatar: null, // 传入 null 表示删除该键
433
+ })
434
+ await reldb.sql.execute(
435
+ `UPDATE users SET profile = ${sql} WHERE id = ?`,
436
+ [...params, userId],
437
+ )
438
+ ```
439
+
440
+ ### Complete example
441
+
442
+ ```ts
443
+ // 建表(含 JSON 列)
444
+ await reldb.ddl.createTable('products', {
445
+ id: { type: 'INTEGER', primaryKey: true, autoIncrement: true },
446
+ meta: { type: 'JSON' },
447
+ })
448
+
449
+ // 插入初始 JSON
450
+ await reldb.sql.execute(
451
+ `INSERT INTO products (meta) VALUES (?)`,
452
+ [JSON.stringify({ status: 'draft', tags: ['new'], version: 1 })],
453
+ )
454
+
455
+ // 设置字段
456
+ const { sql: setSql, params: setParams } = reldb.json.set('meta', '$.status', 'published')
457
+ await reldb.sql.execute(`UPDATE products SET meta = ${setSql} WHERE id = ?`, [...setParams, 1])
458
+
459
+ // 提取字段(用于过滤)
460
+ const { sql: exSql, params: exParams } = reldb.json.extract('meta', '$.status')
461
+ const published = await reldb.sql.query(
462
+ `SELECT * FROM products WHERE ${exSql} = ?`,
463
+ [...exParams, '"published"'],
464
+ )
465
+
466
+ // 合并更新
467
+ const { sql: mSql, params: mParams } = reldb.json.merge('meta', { version: 2, reviewer: 'admin' })
468
+ await reldb.sql.execute(`UPDATE products SET meta = ${mSql} WHERE id = ?`, [...mParams, 1])
469
+
470
+ // 删除字段
471
+ const { sql: rmSql, params: rmParams } = reldb.json.remove('meta', '$.reviewer')
472
+ await reldb.sql.execute(`UPDATE products SET meta = ${rmSql} WHERE id = ?`, [...rmParams, 1])
473
+ ```
474
+
475
+ ## Error handling
476
+
477
+ All operations return `HaiResult<T>`; check `result.success` for success or failure.
478
+
479
+ ```ts
480
+ import { HaiReldbError, reldb } from '@h-ai/reldb'
481
+
482
+ const result = await reldb.sql.query('SELECT * FROM users')
483
+ if (!result.success) {
484
+ switch (result.error.code) {
485
+ case HaiReldbError.NOT_INITIALIZED.code:
486
+ // 请先调用 reldb.init()
487
+ break
488
+ case HaiReldbError.QUERY_FAILED.code:
489
+ // SQL 执行错误
490
+ break
491
+ case HaiReldbError.CONSTRAINT_VIOLATION.code:
492
+ // 约束违反(如唯一约束、外键等)
493
+ break
494
+ }
495
+ }
496
+ ```
497
+
498
+ Common error codes:
499
+
500
+ | Error code | code | Description |
501
+ | ------------------------------------ | --------------- | --------------------- |
502
+ | `HaiReldbError.NOT_INITIALIZED` | `hai:reldb:010` | Not initialized |
503
+ | `HaiReldbError.CONNECTION_FAILED` | `hai:reldb:001` | Connection failed |
504
+ | `HaiReldbError.QUERY_FAILED` | `hai:reldb:002` | Query failed |
505
+ | `HaiReldbError.CONSTRAINT_VIOLATION` | `hai:reldb:003` | Constraint violation |
506
+ | `HaiReldbError.TRANSACTION_FAILED` | `hai:reldb:004` | Transaction failed |
507
+ | `HaiReldbError.DDL_FAILED` | `hai:reldb:020` | DDL operation failed |
508
+ | `HaiReldbError.CONFIG_ERROR` | `hai:reldb:013` | Invalid configuration |
509
+
510
+ ## Tests
511
+
512
+ ```bash
513
+ pnpm --filter @h-ai/reldb test
514
+ ```
515
+
516
+ > MySQL/PostgreSQL tests require Docker.
517
+
518
+ ## License
519
+
520
+ Apache-2.0
package/README.ja.md ADDED
@@ -0,0 +1,520 @@
1
+ # @h-ai/reldb
2
+
3
+ [简体中文](./README.md) | [English](./README.en.md) | [日本語](./README.ja.md)
4
+
5
+ [フレームワークのホーム](../../README.ja.md) · 関連モジュール:[@h-ai/core](../core/README.ja.md)
6
+
7
+ 共通の `reldb` オブジェクトで SQLite、PostgreSQL、MySQL を操作するリレーショナルデータベースモジュールです。
8
+
9
+ ## 対応データベース
10
+
11
+ - SQLite
12
+ - PostgreSQL
13
+ - MySQL
14
+
15
+ ## クイックスタート
16
+
17
+ ```ts
18
+ import { HaiReldbError, reldb } from '@h-ai/reldb'
19
+
20
+ // 初始化
21
+ await reldb.init({ type: 'sqlite', database: ':memory:' })
22
+
23
+ // DDL
24
+ await reldb.ddl.createTable('users', {
25
+ id: { type: 'INTEGER', primaryKey: true, autoIncrement: true },
26
+ name: { type: 'TEXT', notNull: true },
27
+ email: { type: 'TEXT', unique: true },
28
+ })
29
+
30
+ // SQL
31
+ await reldb.sql.execute('INSERT INTO users (name, email) VALUES (?, ?)', ['张三', 'test@example.com'])
32
+ const users = await reldb.sql.query<{ id: number, name: string }>('SELECT * FROM users')
33
+
34
+ // 分页查询
35
+ const page = await reldb.sql.queryPage<{ id: number, name: string }>({
36
+ sql: 'SELECT id, name FROM users ORDER BY id',
37
+ pagination: { page: 1, pageSize: 20 },
38
+ })
39
+
40
+ // CRUD
41
+ const userCrud = reldb.crud.table({
42
+ table: 'users',
43
+ idColumn: 'id',
44
+ select: ['id', 'name', 'email'],
45
+ createColumns: ['name', 'email'],
46
+ updateColumns: ['name', 'email'],
47
+ dbType: 'sqlite',
48
+ })
49
+ await userCrud.create({ name: '李四', email: 'li@test.com' })
50
+ const user = await userCrud.findById(1)
51
+
52
+ // 关闭
53
+ await reldb.close()
54
+ ```
55
+
56
+ `reldb.config` は**機密情報をマスクした設定スナップショット**を返します。DB の `password` と URL 内の認証情報は自動的に非表示になります。状態確認用であり、元の接続情報の再利用には使わないでください。
57
+
58
+ ## 設定
59
+
60
+ 接続文字列(`url`)または個別フィールド(`host/port/database/user/password`)を指定できます。
61
+
62
+ ```ts
63
+ // SQLite
64
+ await reldb.init({ type: 'sqlite', database: './data.db' })
65
+
66
+ // PostgreSQL
67
+ await reldb.init({ type: 'postgresql', url: 'postgres://user:pass@localhost:5432/mydb' })
68
+
69
+ // MySQL
70
+ await reldb.init({
71
+ type: 'mysql',
72
+ host: 'localhost',
73
+ port: 3306,
74
+ database: 'mydb',
75
+ user: 'admin',
76
+ password: 'secret',
77
+ mysql: { charset: 'utf8mb4' },
78
+ })
79
+ ```
80
+
81
+ ### SQLite の実行方式(非同期 worker)
82
+
83
+ better-sqlite3 は同期ドライバーなので、クエリは Node のメインイベントループをブロックします。WAL は DB ロックの並行性を改善しますが、同期呼び出しによるブロックは解消しません。専用 worker スレッドで SQLite を実行して処理を移せます(単一接続、任意設定、既定は `sync`):
84
+
85
+ ```ts
86
+ await reldb.init({
87
+ type: 'sqlite',
88
+ database: './data.db',
89
+ sqlite: {
90
+ executor: 'worker', // 'sync'(默认)| 'worker'
91
+ walMode: true, // 默认 true
92
+ workerConnectTimeoutMs: 10000, // worker 打开连接超时,默认 10000
93
+ },
94
+ })
95
+ ```
96
+
97
+ worker モードではクエリ、書き込み、トランザクションを worker スレッドで順番に実行します。公開 API と戻り値は同期モードと同じです。
98
+
99
+ ### 操作ログ
100
+
101
+ DB 設定の `operationLog` で SQL 操作ログを有効にできます。ログは reldb provider-base の DML 層で出力され、`reldb-main.ts` や各 raw provider で重複してラップしません。
102
+
103
+ ```yaml
104
+ # config/_db.yml
105
+ type: sqlite
106
+ database: ./data/app.db
107
+ operationLog:
108
+ read: false # query / get / queryPage
109
+ write: false # DDL, execute / batch, tx.begin / tx.commit / tx.rollback
110
+ maxLength: 1000 # 参数序列化后的最大输出长度,超出会截断
111
+ level: debug # info | debug | trace,默认 debug
112
+ ```
113
+
114
+ `core.config.load('db', ...)` で読み込む場合、`HAI_DB_TYPE`、
115
+ `HAI_DB_DATABASE`、`HAI_DB_OPERATIONLOG_LEVEL` などの規約に沿う変数が YAML より優先されます。
116
+
117
+ ```ts
118
+ await reldb.init({
119
+ type: 'sqlite',
120
+ database: './data.db',
121
+ operationLog: {
122
+ read: true,
123
+ write: true,
124
+ maxLength: 500,
125
+ level: 'debug',
126
+ },
127
+ })
128
+ ```
129
+
130
+ ## トランザクション
131
+
132
+ 3 種類の使用方法:
133
+
134
+ ### wrap(自動コミット/ロールバック)
135
+
136
+ ```ts
137
+ const result = await reldb.tx.wrap(async (tx) => {
138
+ await tx.execute('INSERT INTO users (name) VALUES (?)', ['用户1'])
139
+ await tx.execute('INSERT INTO users (name) VALUES (?)', ['用户2'])
140
+ return 'ok'
141
+ })
142
+ // 回调正常返回 → 自动提交;抛异常 → 自动回滚
143
+ ```
144
+
145
+ ### begin + commit(手動コミット)
146
+
147
+ ```ts
148
+ const txResult = await reldb.tx.begin()
149
+ if (!txResult.success) { /* 处理错误 */ }
150
+ const tx = txResult.data
151
+
152
+ await tx.execute('INSERT INTO users (name) VALUES (?)', ['用户1'])
153
+ await tx.execute('INSERT INTO users (name) VALUES (?)', ['用户2'])
154
+ await tx.commit()
155
+ ```
156
+
157
+ ### begin + rollback(手動ロールバック)
158
+
159
+ ```ts
160
+ const txResult = await reldb.tx.begin()
161
+ if (!txResult.success) { /* 处理错误 */ }
162
+ const tx = txResult.data
163
+
164
+ await tx.execute('INSERT INTO users (name) VALUES (?)', ['用户1'])
165
+ // 业务检查失败,手动回滚
166
+ await tx.rollback()
167
+ ```
168
+
169
+ ### トランザクション内での batch / queryPage
170
+
171
+ ```ts
172
+ await reldb.tx.wrap(async (tx) => {
173
+ await tx.batch([
174
+ { sql: 'INSERT INTO users (name) VALUES (?)', params: ['用户1'] },
175
+ { sql: 'INSERT INTO users (name) VALUES (?)', params: ['用户2'] },
176
+ ])
177
+
178
+ const page = await tx.queryPage<{ id: number, name: string }>({
179
+ sql: 'SELECT id, name FROM users ORDER BY id',
180
+ pagination: { page: 1, pageSize: 10 },
181
+ })
182
+ })
183
+ ```
184
+
185
+ ## Crud
186
+
187
+ `reldb.crud.table()` で単一テーブルの CRUD リポジトリーを作成できます。SQL の記述は不要です:
188
+
189
+ ```ts
190
+ const userCrud = reldb.crud.table<{ id: number, name: string, email: string }>({
191
+ table: 'users',
192
+ idColumn: 'id', // 主键列,默认 'id'
193
+ select: ['id', 'name', 'email'], // 查询列,默认 '*'
194
+ createColumns: ['name', 'email'], // 允许插入的列
195
+ updateColumns: ['name', 'email'], // 允许更新的列
196
+ dbType: 'sqlite', // 数据库类型
197
+ })
198
+ ```
199
+
200
+ ### create / createMany
201
+
202
+ ```ts
203
+ // 单条插入
204
+ const result = await userCrud.create({ name: '张三', email: 'test@example.com' })
205
+ // result.data → { changes: 1, lastInsertRowid: 1 }
206
+
207
+ // 批量插入
208
+ await userCrud.createMany([
209
+ { name: '用户A', email: 'a@test.com' },
210
+ { name: '用户B', email: 'b@test.com' },
211
+ ])
212
+ ```
213
+
214
+ ### createOrUpdate
215
+
216
+ 主キーが存在すれば更新し、存在しなければ作成します(upsert):
217
+
218
+ ```ts
219
+ const userCrud = reldb.crud.table<{ id: number, name: string, email: string }>({
220
+ table: 'users',
221
+ idColumn: 'id',
222
+ createColumns: ['id', 'name', 'email'],
223
+ updateColumns: ['name', 'email'],
224
+ dbType: 'sqlite',
225
+ })
226
+
227
+ await userCrud.createOrUpdate({ id: 1, name: '张三', email: 'test@example.com' })
228
+ // 不存在 → 插入;已存在 → 更新 name、email
229
+ ```
230
+
231
+ `BaseReldbCrudRepository` は `dbType` を自動取得するため、手動指定は不要です。
232
+
233
+ ### findById / getById / findAll / findPage
234
+
235
+ ```ts
236
+ // 主键查找(不存在时返回 null)
237
+ const user = await userCrud.findById(1)
238
+ // user.data → { id: 1, name: '张三', email: 'test@example.com' } | null
239
+
240
+ // 主键获取(不存在时返回 RECORD_NOT_FOUND 错误)
241
+ const user2 = await userCrud.getById(1)
242
+ // user2.data → { id: 1, name: '张三', email: 'test@example.com' }
243
+
244
+ // 条件查询(where + params 占位符)
245
+ const actives = await userCrud.findAll({
246
+ where: 'name LIKE ?',
247
+ params: ['%张%'],
248
+ orderBy: 'id DESC',
249
+ limit: 10,
250
+ offset: 0,
251
+ })
252
+
253
+ // 分页查询
254
+ const page = await userCrud.findPage({
255
+ where: 'name LIKE ?',
256
+ params: ['%张%'],
257
+ orderBy: 'id DESC',
258
+ pagination: { page: 1, pageSize: 20 },
259
+ })
260
+ // page.data → { items: [...], total: 100, page: 1, pageSize: 20 }
261
+ ```
262
+
263
+ ### updateById / deleteById
264
+
265
+ ```ts
266
+ await userCrud.updateById(1, { name: '李四' })
267
+ await userCrud.deleteById(1)
268
+ ```
269
+
270
+ ### count / exists / existsById
271
+
272
+ ```ts
273
+ const total = await userCrud.count({ where: 'name LIKE ?', params: ['%张%'] })
274
+ // total.data → 5
275
+
276
+ const has = await userCrud.exists({ where: 'email = ?', params: ['test@example.com'] })
277
+ // has.data → true
278
+
279
+ const found = await userCrud.existsById(1)
280
+ // found.data → true
281
+ ```
282
+
283
+ ### トランザクション内での Crud
284
+
285
+ すべてのメソッドに `tx` を渡すことができ、対応するトランザクションのコンテキストへ自動的に切り替わります:
286
+
287
+ ```ts
288
+ await reldb.tx.wrap(async (tx) => {
289
+ await userCrud.create({ name: '用户A', email: 'a@test.com' }, tx)
290
+ await userCrud.updateById(1, { name: '新名字' }, tx)
291
+ const user = await userCrud.findById(1, tx)
292
+ })
293
+ ```
294
+
295
+ ## BaseReldbCrudRepository
296
+
297
+ 業務リポジトリーは `BaseReldbCrudRepository` を継承し、`fields` でフィールドの対応を定義します。テーブル作成と型変換は自動です:
298
+
299
+ ```ts
300
+ import type { DmlWithTxOperations, ReldbCrudFieldDefinition } from '@h-ai/reldb'
301
+ import { BaseReldbCrudRepository, reldb } from '@h-ai/reldb'
302
+
303
+ interface UserRow {
304
+ id: number
305
+ name: string
306
+ email: string
307
+ createdAt: Date
308
+ updatedAt: Date
309
+ }
310
+
311
+ const USER_FIELDS: ReldbCrudFieldDefinition[] = [
312
+ { fieldName: 'id', columnName: 'id', def: { type: 'INTEGER', primaryKey: true, autoIncrement: true }, select: true, create: false, update: false },
313
+ { fieldName: 'name', columnName: 'name', def: { type: 'TEXT', notNull: true }, select: true, create: true, update: true },
314
+ { fieldName: 'email', columnName: 'email', def: { type: 'TEXT', notNull: true }, select: true, create: true, update: true },
315
+ { fieldName: 'createdAt', columnName: 'created_at', def: { type: 'TIMESTAMP', notNull: true }, select: true, create: true, update: false },
316
+ { fieldName: 'updatedAt', columnName: 'updated_at', def: { type: 'TIMESTAMP', notNull: true }, select: true, create: true, update: false },
317
+ ]
318
+
319
+ class UserRepository extends BaseReldbCrudRepository<UserRow> {
320
+ constructor() {
321
+ super(reldb, { table: 'users', idColumn: 'id', fields: USER_FIELDS })
322
+ }
323
+
324
+ /** 自定义查询方法 */
325
+ async findByEmail(email: string) {
326
+ return this.findAll({ where: 'email = ?', params: [email], limit: 1 })
327
+ }
328
+
329
+ /** 自定义方法中使用 this.sql(tx) 自动路由到事务 */
330
+ async insertRaw(data: { name: string, email: string }, tx?: DmlWithTxOperations) {
331
+ // 自定义原始写操作应先确保表已就绪(避免刚建库/建表较慢时的竞态)
332
+ const ready = await this.ensureReady()
333
+ if (!ready.success)
334
+ return ready
335
+ const now = Date.now()
336
+ return this.sql(tx).execute(
337
+ 'INSERT INTO users (name, email, created_at, updated_at) VALUES (?, ?, ?, ?)',
338
+ [data.name, data.email, now, now],
339
+ )
340
+ }
341
+ }
342
+
343
+ // 使用
344
+ const repo = new UserRepository()
345
+ await repo.create({ name: '张三', email: 'test@example.com' })
346
+ const user = await repo.findById(1)
347
+
348
+ // 创建或更新(upsert)
349
+ await repo.createOrUpdate({ id: 1, name: '李四', email: 'test@example.com' })
350
+ // id=1 已存在 → 更新 name;不存在 → 插入新记录
351
+
352
+ // 跨仓库事务
353
+ const txResult = await reldb.tx.begin()
354
+ if (txResult.success) {
355
+ const tx = txResult.data
356
+ await repo.create({ name: '用户A', email: 'a@test.com' }, tx)
357
+ await repo.create({ name: '用户B', email: 'b@test.com' }, tx)
358
+ await tx.commit()
359
+ }
360
+ ```
361
+
362
+ ## JSON パス操作
363
+
364
+ `reldb.json` で、複数のデータベースに共通する JSON パス操作の SQL 式を作成します。
365
+
366
+ パスは SQL/JSON Path(`$.key` / `$.key.subkey` / `$[0]`)に従い、各 DB の関数・演算子に自動変換されます。
367
+
368
+ | 操作 | SQLite | PostgreSQL | MySQL |
369
+ | --------- | -------------- | -------------- | ------------------ |
370
+ | `extract` | `json_extract` | `#>` (text[]) | `JSON_EXTRACT` |
371
+ | `set` | `json_set` | `jsonb_set` | `JSON_SET` |
372
+ | `insert` | `json_insert` | `jsonb_insert` | `JSON_INSERT` |
373
+ | `remove` | `json_remove` | `#-` (text[]) | `JSON_REMOVE` |
374
+ | `merge` | `json_patch` | `\|\| ::jsonb` | `JSON_MERGE_PATCH` |
375
+
376
+ すべてのメソッドは `{ sql, params }` の断片を返し、`reldb.sql.query` / `reldb.sql.execute` に直接組み込めます。
377
+
378
+ ### extract — JSON フィールド値の取得
379
+
380
+ ```ts
381
+ // 用于 WHERE 条件
382
+ const { sql, params } = reldb.json.extract('settings', '$.theme')
383
+ const rows = await reldb.sql.query(
384
+ `SELECT * FROM users WHERE ${sql} = ?`,
385
+ [...params, '"dark"'],
386
+ )
387
+
388
+ // 用于 SELECT 列
389
+ const { sql, params } = reldb.json.extract('profile', '$.name')
390
+ const result = await reldb.sql.query(
391
+ `SELECT id, ${sql} as display_name FROM users`,
392
+ params,
393
+ )
394
+ ```
395
+
396
+ ### set — JSON パスの設定(作成または置換)
397
+
398
+ ```ts
399
+ const { sql, params } = reldb.json.set('settings', '$.theme', 'dark')
400
+ await reldb.sql.execute(
401
+ `UPDATE users SET settings = ${sql} WHERE id = ?`,
402
+ [...params, userId],
403
+ )
404
+ ```
405
+
406
+ ### insert — JSON パスへの挿入(未存在の場合のみ)
407
+
408
+ ```ts
409
+ const { sql, params } = reldb.json.insert('data', '$.firstSeen', new Date().toISOString())
410
+ await reldb.sql.execute(
411
+ `UPDATE events SET data = ${sql} WHERE id = ?`,
412
+ [...params, eventId],
413
+ )
414
+ ```
415
+
416
+ ### remove — JSON パスの削除
417
+
418
+ ```ts
419
+ const { sql, params } = reldb.json.remove('settings', '$.deprecated')
420
+ await reldb.sql.execute(
421
+ `UPDATE users SET settings = ${sql} WHERE id = ?`,
422
+ [...params, userId],
423
+ )
424
+ ```
425
+
426
+ ### merge — JSON オブジェクトのマージ(RFC 7396 Merge Patch)
427
+
428
+ ```ts
429
+ // 覆盖已有键、保留未提及键、将 null 键删除
430
+ const { sql, params } = reldb.json.merge('profile', {
431
+ bio: '新简介',
432
+ avatar: null, // 传入 null 表示删除该键
433
+ })
434
+ await reldb.sql.execute(
435
+ `UPDATE users SET profile = ${sql} WHERE id = ?`,
436
+ [...params, userId],
437
+ )
438
+ ```
439
+
440
+ ### 完全な使用例
441
+
442
+ ```ts
443
+ // 建表(含 JSON 列)
444
+ await reldb.ddl.createTable('products', {
445
+ id: { type: 'INTEGER', primaryKey: true, autoIncrement: true },
446
+ meta: { type: 'JSON' },
447
+ })
448
+
449
+ // 插入初始 JSON
450
+ await reldb.sql.execute(
451
+ `INSERT INTO products (meta) VALUES (?)`,
452
+ [JSON.stringify({ status: 'draft', tags: ['new'], version: 1 })],
453
+ )
454
+
455
+ // 设置字段
456
+ const { sql: setSql, params: setParams } = reldb.json.set('meta', '$.status', 'published')
457
+ await reldb.sql.execute(`UPDATE products SET meta = ${setSql} WHERE id = ?`, [...setParams, 1])
458
+
459
+ // 提取字段(用于过滤)
460
+ const { sql: exSql, params: exParams } = reldb.json.extract('meta', '$.status')
461
+ const published = await reldb.sql.query(
462
+ `SELECT * FROM products WHERE ${exSql} = ?`,
463
+ [...exParams, '"published"'],
464
+ )
465
+
466
+ // 合并更新
467
+ const { sql: mSql, params: mParams } = reldb.json.merge('meta', { version: 2, reviewer: 'admin' })
468
+ await reldb.sql.execute(`UPDATE products SET meta = ${mSql} WHERE id = ?`, [...mParams, 1])
469
+
470
+ // 删除字段
471
+ const { sql: rmSql, params: rmParams } = reldb.json.remove('meta', '$.reviewer')
472
+ await reldb.sql.execute(`UPDATE products SET meta = ${rmSql} WHERE id = ?`, [...rmParams, 1])
473
+ ```
474
+
475
+ ## エラー処理
476
+
477
+ すべての操作は `HaiResult<T>` を返し、`result.success` で成功・失敗を判定します。
478
+
479
+ ```ts
480
+ import { HaiReldbError, reldb } from '@h-ai/reldb'
481
+
482
+ const result = await reldb.sql.query('SELECT * FROM users')
483
+ if (!result.success) {
484
+ switch (result.error.code) {
485
+ case HaiReldbError.NOT_INITIALIZED.code:
486
+ // 请先调用 reldb.init()
487
+ break
488
+ case HaiReldbError.QUERY_FAILED.code:
489
+ // SQL 执行错误
490
+ break
491
+ case HaiReldbError.CONSTRAINT_VIOLATION.code:
492
+ // 约束违反(如唯一约束、外键等)
493
+ break
494
+ }
495
+ }
496
+ ```
497
+
498
+ 主なエラーコード:
499
+
500
+ | エラーコード | code | 説明 |
501
+ | ------------------------------------ | --------------- | ---------------------- |
502
+ | `HaiReldbError.NOT_INITIALIZED` | `hai:reldb:010` | 未初期化 |
503
+ | `HaiReldbError.CONNECTION_FAILED` | `hai:reldb:001` | 接続に失敗 |
504
+ | `HaiReldbError.QUERY_FAILED` | `hai:reldb:002` | 検索に失敗 |
505
+ | `HaiReldbError.CONSTRAINT_VIOLATION` | `hai:reldb:003` | 制約違反 |
506
+ | `HaiReldbError.TRANSACTION_FAILED` | `hai:reldb:004` | トランザクションに失敗 |
507
+ | `HaiReldbError.DDL_FAILED` | `hai:reldb:020` | DDL 操作に失敗 |
508
+ | `HaiReldbError.CONFIG_ERROR` | `hai:reldb:013` | 設定エラー |
509
+
510
+ ## テスト
511
+
512
+ ```bash
513
+ pnpm --filter @h-ai/reldb test
514
+ ```
515
+
516
+ > MySQL/PostgreSQL のテストには Docker が必要です。
517
+
518
+ ## License
519
+
520
+ Apache-2.0
package/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @h-ai/reldb
2
2
 
3
+ [简体中文](./README.md) | [English](./README.en.md) | [日本語](./README.ja.md)
4
+
5
+ [框架首页](../../README.md) · 相关模块:[@h-ai/core](../core/README.md)
6
+
3
7
  关系型数据库访问模块,通过统一的 `reldb` 对象访问 SQLite、PostgreSQL、MySQL。
4
8
 
5
9
  ## 支持的数据库
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@h-ai/reldb",
3
3
  "type": "module",
4
- "version": "0.1.0-alpha.56",
4
+ "version": "0.1.0-alpha.57",
5
5
  "description": "Hai Framework relational database module for SQLite, PostgreSQL, and MySQL.",
6
6
  "license": "Apache-2.0",
7
7
  "exports": {
@@ -25,7 +25,7 @@
25
25
  "dependencies": {
26
26
  "better-sqlite3": "^12.6.2",
27
27
  "zod": "^4.4.3",
28
- "@h-ai/core": "0.1.0-alpha.56"
28
+ "@h-ai/core": "0.1.0-alpha.57"
29
29
  },
30
30
  "optionalDependencies": {
31
31
  "mysql2": "^3.11.5",