@h-ai/reldb 0.1.0-alpha.55 → 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 +520 -0
- package/README.ja.md +520 -0
- package/README.md +4 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|