@h-ai/reldb 1.0.0-beta.2 → 1.0.0-beta.21
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 +295 -143
- package/README.ja.md +295 -143
- package/README.md +295 -143
- package/dist/{chunk-SDS2ZT3V.js → chunk-7OE27ZH3.js} +18 -6
- package/dist/chunk-7OE27ZH3.js.map +1 -0
- package/dist/index.d.ts +56 -54
- package/dist/index.js +392 -159
- package/dist/index.js.map +1 -1
- package/dist/providers/sqlite/reldb-sqlite-worker.mjs +23 -6
- package/dist/schemas.d.ts +2 -1
- package/dist/schemas.js +1 -1
- package/package.json +10 -10
- package/dist/chunk-SDS2ZT3V.js.map +0 -1
package/README.en.md
CHANGED
|
@@ -4,33 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
[Framework home](../../README.en.md) · Related modules: [@h-ai/core](../core/README.en.md)
|
|
6
6
|
|
|
7
|
-
## Introduction
|
|
8
|
-
|
|
9
7
|
A relational database module with a unified `reldb` object for SQLite, PostgreSQL, and MySQL.
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
```ts
|
|
14
|
-
const closed = await reldb.close()
|
|
15
|
-
if (!closed.success) {
|
|
16
|
-
// Retry once after addressing the cleanup failure; report another failure.
|
|
17
|
-
const retried = await reldb.close()
|
|
18
|
-
if (!retried.success)
|
|
19
|
-
throw new Error(retried.error.message)
|
|
20
|
-
}
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
Close waits for borrowed transactions to commit/roll back and release their handles; applications must finish transactions for shutdown to complete. SQLite WAL setup and worker handshakes are part of the owned initialization chain. PostgreSQL removes disconnected idle clients and replaces them on later queries without terminating the process; logs contain safe error identifiers rather than raw driver errors.
|
|
24
|
-
|
|
25
|
-
## Core features
|
|
26
|
-
|
|
27
|
-
- Connect to SQLite, PostgreSQL and MySQL through one API.
|
|
28
|
-
- Run parameterized SQL, manage tables/indexes and read paginated results.
|
|
29
|
-
- Organize application reads and writes with transactions, CRUD and repositories.
|
|
30
|
-
|
|
31
|
-
## Getting started
|
|
32
|
-
|
|
33
|
-
### Quick start
|
|
9
|
+
## Quick Start
|
|
34
10
|
|
|
35
11
|
```ts
|
|
36
12
|
import { HaiReldbError, reldb } from '@h-ai/reldb'
|
|
@@ -75,135 +51,110 @@ await reldb.close()
|
|
|
75
51
|
|
|
76
52
|
`reldb.config` returns a **redacted configuration snapshot**. Database `password`, URL credentials and the TLS client private key `ssl.key` are hidden. Use it to inspect state, not to replay connection details.
|
|
77
53
|
|
|
78
|
-
|
|
54
|
+
## Features
|
|
79
55
|
|
|
80
|
-
|
|
56
|
+
Connections become available only after a successful health check. Failed initialization disposes its candidate; `error.ext.cleanupPending === true` identifies cleanup that still needs retry. `reldb.close()` returns `Promise<HaiResult<void>>`, waits for initialization, and coalesces concurrent close calls. Once cleanup starts, `isInitialized` is false and configuration/operations are unavailable. A failed cleanup retains the resource: the next `close()` or `init()` retries disposal before allocating a replacement. Always inspect the close result.
|
|
81
57
|
|
|
82
58
|
```ts
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
59
|
+
const closed = await reldb.close()
|
|
60
|
+
if (!closed.success) {
|
|
61
|
+
// Retry once after addressing the cleanup failure; report another failure.
|
|
62
|
+
const retried = await reldb.close()
|
|
63
|
+
if (!retried.success)
|
|
64
|
+
throw new Error(retried.error.message)
|
|
65
|
+
}
|
|
89
66
|
```
|
|
90
67
|
|
|
91
|
-
|
|
68
|
+
Close waits for borrowed transactions to commit/roll back and release their handles; applications must finish transactions for shutdown to complete. SQLite WAL setup and worker handshakes are part of the owned initialization chain. PostgreSQL removes disconnected idle clients and replaces them on later queries without terminating the process; logs contain safe error identifiers rather than raw driver errors.
|
|
92
69
|
|
|
93
|
-
|
|
94
|
-
// SQLite
|
|
95
|
-
await reldb.init({ type: 'sqlite', database: './data.db' })
|
|
70
|
+
### Operations and results
|
|
96
71
|
|
|
97
|
-
|
|
98
|
-
await reldb.init({ type: 'postgresql', url: 'postgres://user:pass@localhost:5432/mydb', database: 'mydb' })
|
|
72
|
+
Use the complete initialization flow in this section first. The host validates and authorizes example business values; check success before reading data from asynchronous domain results. Here void means completion without data, null means absent; synchronous factories/tools return the listed type directly.
|
|
99
73
|
|
|
100
|
-
|
|
101
|
-
await reldb.init({
|
|
102
|
-
type: 'mysql',
|
|
103
|
-
host: 'localhost',
|
|
104
|
-
port: 3306,
|
|
105
|
-
database: 'mydb',
|
|
106
|
-
user: 'admin',
|
|
107
|
-
password: 'secret',
|
|
108
|
-
mysql: { charset: 'utf8mb4' },
|
|
109
|
-
pool: { max: 10, acquireTimeout: 5000, queueLimit: 100 },
|
|
110
|
-
})
|
|
111
|
-
```
|
|
74
|
+
Table/column/index names and SQL come from trusted code; params bind business values, steps define module migrations and options control preview/pagination.
|
|
112
75
|
|
|
113
|
-
####
|
|
76
|
+
#### reldb.ddl
|
|
114
77
|
|
|
115
|
-
|
|
78
|
+
Maintain tables, columns and indexes. Supply trusted identifiers, column/index definitions or DDL; success contains void, invalid changes fail.
|
|
116
79
|
|
|
117
|
-
|
|
|
118
|
-
|
|
|
119
|
-
| `
|
|
120
|
-
| `
|
|
121
|
-
| `
|
|
122
|
-
| `
|
|
123
|
-
| `
|
|
80
|
+
| Entry | Input parameters | Successful result | Call example |
|
|
81
|
+
| ----------------------- | -------------------------------- | ----------------- | ------------------------------------------------------------------------------------ |
|
|
82
|
+
| `reldb.ddl.createTable` | tableName, columns, ifNotExists | `void` | `await reldb.ddl.createTable('orders', { id: { type: 'TEXT', primaryKey: true } })` |
|
|
83
|
+
| `reldb.ddl.dropTable` | tableName, ifExists | `void` | `await reldb.ddl.dropTable('orders_archive', true)` |
|
|
84
|
+
| `reldb.ddl.addColumn` | tableName, columnName, columnDef | `void` | `await reldb.ddl.addColumn('orders', 'note', { type: 'TEXT' })` |
|
|
85
|
+
| `reldb.ddl.dropColumn` | tableName, columnName | `void` | `await reldb.ddl.dropColumn('orders', 'note')` |
|
|
86
|
+
| `reldb.ddl.renameTable` | oldName, newName | `void` | `await reldb.ddl.renameTable('orders_draft', 'orders')` |
|
|
87
|
+
| `reldb.ddl.createIndex` | tableName, indexName, indexDef | `void` | `await reldb.ddl.createIndex('orders', 'orders_status', { columns: ['status'] })` |
|
|
88
|
+
| `reldb.ddl.dropIndex` | indexName, ifExists | `void` | `await reldb.ddl.dropIndex('orders_status', true)` |
|
|
89
|
+
| `reldb.ddl.raw` | sql | `void` | `await reldb.ddl.raw('CREATE TABLE IF NOT EXISTS app_marker (id TEXT PRIMARY KEY)')` |
|
|
124
90
|
|
|
125
|
-
|
|
91
|
+
#### reldb.sql
|
|
126
92
|
|
|
127
|
-
|
|
93
|
+
Read/write business data with bound params: rows, first row/null, write result, void or a page respectively; batch takes statement objects.
|
|
128
94
|
|
|
129
|
-
|
|
95
|
+
| Entry | Input parameters | Successful result | Call example |
|
|
96
|
+
| --------------------- | ---------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
97
|
+
| `reldb.sql.query` | sql, params | `T[]` | `await reldb.sql.query('SELECT id FROM orders WHERE status = ?', ['pending'])` |
|
|
98
|
+
| `reldb.sql.get` | sql, params | `T \| null` | `await reldb.sql.get('SELECT id FROM orders WHERE id = ?', ['o1'])` |
|
|
99
|
+
| `reldb.sql.execute` | sql, params | `ExecuteResult` | `await reldb.sql.execute('UPDATE orders SET status = ? WHERE id = ?', ['paid', 'o1'])` |
|
|
100
|
+
| `reldb.sql.batch` | statements | `void` | `await reldb.sql.batch([{ sql: 'DELETE FROM orders WHERE id = ?', params: ['o1'] }])` |
|
|
101
|
+
| `reldb.sql.queryPage` | options | `PaginatedResult<T>` | `await reldb.sql.queryPage({ sql: 'SELECT id FROM orders ORDER BY id', pagination: { page: 1, pageSize: 20 } })` |
|
|
130
102
|
|
|
131
|
-
|
|
132
|
-
const initialized = await reldb.init({ type: 'mysql', url: process.env.HAI_DB_URL, pool: { max: 10, acquireTimeout: 5000, queueLimit: 100 } })
|
|
133
|
-
if (!initialized.success)
|
|
134
|
-
throw new Error(initialized.error.message)
|
|
135
|
-
```
|
|
103
|
+
#### reldb.pagination
|
|
136
104
|
|
|
137
|
-
|
|
105
|
+
Pagination: normalize accepts page options and limits and returns normalized options; build accepts items, total and paging and returns the page object directly.
|
|
138
106
|
|
|
139
|
-
|
|
107
|
+
| Entry | Input parameters | Successful result | Call example |
|
|
108
|
+
| ---------------------------- | ------------------------ | ---------------------- | ---------------------------------------------------------- |
|
|
109
|
+
| `reldb.pagination.normalize` | options, overrides | `NormalizedPagination` | `reldb.pagination.normalize({ page: 1, pageSize: 20 })` |
|
|
110
|
+
| `reldb.pagination.build` | items, total, pagination | `PaginatedResult<T>` | `reldb.pagination.build([], 0, { page: 1, pageSize: 20 })` |
|
|
140
111
|
|
|
141
|
-
|
|
112
|
+
#### reldb
|
|
142
113
|
|
|
143
|
-
|
|
114
|
+
Prepare module persistence: table name returns columns; module name, steps and dryRun return a migration plan. Back up and stop conflicting writers before applying.
|
|
144
115
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
throw new Error(initialized.error.message)
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
TLS tests require OpenSSL (Git for Windows includes one) and local container port forwarding. Each run generates and removes its own CA/private keys and checks trusted DNS, wrong CA/hostname rejection, `REQUIRE X509` client authentication and actual session `Ssl_cipher`. Disabling verification is not accepted as proof of authenticated TLS.
|
|
116
|
+
| Entry | Input parameters | Successful result | Call example |
|
|
117
|
+
| --------------------- | ---------------------- | --------------------- | ----------------------------------------------------------------------- |
|
|
118
|
+
| `reldb.schemaColumns` | table, sql | `string[]` | `await reldb.schemaColumns('orders')` |
|
|
119
|
+
| `reldb.migrateSchema` | module, steps, options | `SchemaMigrationPlan` | `await reldb.migrateSchema('orders', migrationSteps, { dryRun: true })` |
|
|
153
120
|
|
|
154
|
-
####
|
|
121
|
+
#### reldb.tx
|
|
155
122
|
|
|
156
|
-
|
|
123
|
+
Atomic writes: begin yields a transaction with query/get/execute/batch/queryPage/commit/rollback; wrap runs a callback and returns its value. A failed HaiResult must explicitly trigger rollback.
|
|
157
124
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
sqlite: {
|
|
163
|
-
executor: 'worker', // 'sync'(默认)| 'worker'
|
|
164
|
-
walMode: true, // 默认 true
|
|
165
|
-
workerConnectTimeoutMs: 10000, // worker 打开连接超时,默认 10000
|
|
166
|
-
},
|
|
167
|
-
})
|
|
168
|
-
```
|
|
125
|
+
| Entry | Input parameters | Successful result | Call example |
|
|
126
|
+
| ---------------- | ---------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
127
|
+
| `reldb.tx.begin` | No arguments | `DmlWithTxOperations` | `await reldb.tx.begin()` |
|
|
128
|
+
| `reldb.tx.wrap` | fn | `T` | `await reldb.tx.wrap(async tx => { const found = await tx.get('SELECT 1 AS value'); if (!found.success) throw new Error(found.error.message); return found.data })` |
|
|
169
129
|
|
|
170
|
-
|
|
130
|
+
#### reldb.crud
|
|
171
131
|
|
|
172
|
-
|
|
132
|
+
Supply table/field mappings for a business entity; obtain a repository directly for CRUD and pagination without exposing a database connection.
|
|
173
133
|
|
|
174
|
-
|
|
134
|
+
| Entry | Input parameters | Successful result | Call example |
|
|
135
|
+
| ------------------ | ---------------- | ---------------------------- | ----------------------------------------- |
|
|
136
|
+
| `reldb.crud.table` | config | `ReldbCrudRepository<TItem>` | `reldb.crud.table(orderRepositoryConfig)` |
|
|
175
137
|
|
|
176
|
-
|
|
177
|
-
# config/_db.yml
|
|
178
|
-
type: sqlite
|
|
179
|
-
database: ./data/app.db
|
|
180
|
-
operationLog:
|
|
181
|
-
read: false # query / get / queryPage
|
|
182
|
-
write: false # DDL, execute / batch, tx.begin / tx.commit / tx.rollback
|
|
183
|
-
maxLength: 1000 # maximum numeric summary length; 0 omits the summary, SQL text and values are never logged
|
|
184
|
-
level: debug # info | debug | trace,默认 debug
|
|
185
|
-
```
|
|
138
|
+
#### reldb.json
|
|
186
139
|
|
|
187
|
-
|
|
188
|
-
`HAI_DB_DATABASE`, and `HAI_DB_OPERATIONLOG_LEVEL` override YAML values.
|
|
140
|
+
Cross-dialect JSON expressions: trusted column, JSON path and value/patch return SQL plus bound params directly, without executing a query.
|
|
189
141
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
maxLength: 500,
|
|
198
|
-
level: 'debug',
|
|
199
|
-
},
|
|
200
|
-
})
|
|
201
|
-
```
|
|
142
|
+
| Entry | Input parameters | Successful result | Call example |
|
|
143
|
+
| -------------------- | ------------------- | ----------------- | --------------------------------------------------- |
|
|
144
|
+
| `reldb.json.extract` | column, path | `JsonSqlExpr` | `reldb.json.extract('payload', '$.status')` |
|
|
145
|
+
| `reldb.json.set` | column, path, value | `JsonSqlExpr` | `reldb.json.set('payload', '$.status', 'paid')` |
|
|
146
|
+
| `reldb.json.insert` | column, path, value | `JsonSqlExpr` | `reldb.json.insert('payload', '$.note', 'checked')` |
|
|
147
|
+
| `reldb.json.remove` | column, path | `JsonSqlExpr` | `reldb.json.remove('payload', '$.note')` |
|
|
148
|
+
| `reldb.json.merge` | column, patch | `JsonSqlExpr` | `reldb.json.merge('payload', { status: 'paid' })` |
|
|
202
149
|
|
|
203
|
-
|
|
150
|
+
- Connect to SQLite, PostgreSQL and MySQL through one API.
|
|
151
|
+
- Run parameterized SQL, manage tables/indexes and read paginated results.
|
|
152
|
+
- Organize application reads and writes with transactions, CRUD and repositories.
|
|
204
153
|
|
|
205
154
|
### Transactions
|
|
206
155
|
|
|
156
|
+
A transaction has one finishing owner. Once commit or rollback starts, new queries, duplicate commits, and concurrent rollbacks immediately return TRANSACTION_FAILED; the connection is released once. Await the first finishing result rather than using concurrent completion as recovery.
|
|
157
|
+
|
|
207
158
|
Three approaches:
|
|
208
159
|
|
|
209
160
|
#### wrap (automatic commit/rollback)
|
|
@@ -272,8 +223,14 @@ if (!rolledBack.success)
|
|
|
272
223
|
```ts
|
|
273
224
|
await reldb.tx.wrap(async (tx) => {
|
|
274
225
|
const inserted = await tx.batch([
|
|
275
|
-
{
|
|
276
|
-
|
|
226
|
+
{
|
|
227
|
+
sql: 'INSERT INTO users (name) VALUES (?)',
|
|
228
|
+
params: ['Alice']
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
sql: 'INSERT INTO users (name) VALUES (?)',
|
|
232
|
+
params: ['Bob']
|
|
233
|
+
},
|
|
277
234
|
])
|
|
278
235
|
if (!inserted.success)
|
|
279
236
|
throw new Error(inserted.error.message, { cause: inserted.error })
|
|
@@ -376,10 +333,16 @@ await userCrud.deleteById(1)
|
|
|
376
333
|
#### count / exists / existsById
|
|
377
334
|
|
|
378
335
|
```ts
|
|
379
|
-
const total = await userCrud.count({
|
|
336
|
+
const total = await userCrud.count({
|
|
337
|
+
where: 'name LIKE ?',
|
|
338
|
+
params: ['%张%']
|
|
339
|
+
})
|
|
380
340
|
// total.data → 5
|
|
381
341
|
|
|
382
|
-
const has = await userCrud.exists({
|
|
342
|
+
const has = await userCrud.exists({
|
|
343
|
+
where: 'email = ?',
|
|
344
|
+
params: ['test@example.com']
|
|
345
|
+
})
|
|
383
346
|
// has.data → true
|
|
384
347
|
|
|
385
348
|
const found = await userCrud.existsById(1)
|
|
@@ -426,11 +389,46 @@ interface UserRow {
|
|
|
426
389
|
}
|
|
427
390
|
|
|
428
391
|
const USER_FIELDS: ReldbCrudFieldDefinition[] = [
|
|
429
|
-
{
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
392
|
+
{
|
|
393
|
+
fieldName: 'id',
|
|
394
|
+
columnName: 'id',
|
|
395
|
+
def: { type: 'INTEGER', primaryKey: true, autoIncrement: true },
|
|
396
|
+
select: true,
|
|
397
|
+
create: false,
|
|
398
|
+
update: false
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
fieldName: 'name',
|
|
402
|
+
columnName: 'name',
|
|
403
|
+
def: { type: 'TEXT', notNull: true },
|
|
404
|
+
select: true,
|
|
405
|
+
create: true,
|
|
406
|
+
update: true
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
fieldName: 'email',
|
|
410
|
+
columnName: 'email',
|
|
411
|
+
def: { type: 'TEXT', notNull: true },
|
|
412
|
+
select: true,
|
|
413
|
+
create: true,
|
|
414
|
+
update: true
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
fieldName: 'createdAt',
|
|
418
|
+
columnName: 'created_at',
|
|
419
|
+
def: { type: 'TIMESTAMP', notNull: true },
|
|
420
|
+
select: true,
|
|
421
|
+
create: true,
|
|
422
|
+
update: false
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
fieldName: 'updatedAt',
|
|
426
|
+
columnName: 'updated_at',
|
|
427
|
+
def: { type: 'TIMESTAMP', notNull: true },
|
|
428
|
+
select: true,
|
|
429
|
+
create: true,
|
|
430
|
+
update: false
|
|
431
|
+
},
|
|
434
432
|
]
|
|
435
433
|
|
|
436
434
|
class UserRepository extends BaseReldbCrudRepository<UserRow> {
|
|
@@ -440,7 +438,11 @@ class UserRepository extends BaseReldbCrudRepository<UserRow> {
|
|
|
440
438
|
|
|
441
439
|
/** 自定义查询方法 */
|
|
442
440
|
async findByEmail(email: string) {
|
|
443
|
-
return this.findAll({
|
|
441
|
+
return this.findAll({
|
|
442
|
+
where: 'email = ?',
|
|
443
|
+
params: [email],
|
|
444
|
+
limit: 1
|
|
445
|
+
})
|
|
444
446
|
}
|
|
445
447
|
|
|
446
448
|
/** 自定义方法中使用 this.sql(tx) 自动路由到事务 */
|
|
@@ -568,7 +570,13 @@ await reldb.ddl.createTable('products', {
|
|
|
568
570
|
// 插入初始 JSON
|
|
569
571
|
await reldb.sql.execute(
|
|
570
572
|
`INSERT INTO products (meta) VALUES (?)`,
|
|
571
|
-
[
|
|
573
|
+
[
|
|
574
|
+
JSON.stringify({
|
|
575
|
+
status: 'draft',
|
|
576
|
+
tags: ['new'],
|
|
577
|
+
version: 1
|
|
578
|
+
})
|
|
579
|
+
],
|
|
572
580
|
)
|
|
573
581
|
|
|
574
582
|
// 设置字段
|
|
@@ -591,7 +599,159 @@ const { sql: rmSql, params: rmParams } = reldb.json.remove('meta', '$.reviewer')
|
|
|
591
599
|
await reldb.sql.execute(`UPDATE products SET meta = ${rmSql} WHERE id = ?`, [...rmParams, 1])
|
|
592
600
|
```
|
|
593
601
|
|
|
594
|
-
###
|
|
602
|
+
### Production shared state
|
|
603
|
+
|
|
604
|
+
Production requires PostgreSQL/MySQL. SQLite, including disk files, is for development/explicit tests and does not provide cross-host shared state. Initialization checks apply when NODE_ENV or HAI_CORE_ENV is production; only explicit HAI_E2E=1/HAI_E2E_MOCK=1 tests are exempt. [State, concurrency and recovery (Chinese)](../../.agents/instructions/distributed-state.md).
|
|
605
|
+
|
|
606
|
+
### Module schema migrations
|
|
607
|
+
|
|
608
|
+
`reldb.schemaColumns(table, sql?)` inspects real columns. `reldb.migrateSchema(module, steps, { dryRun: true })` creates no ledger. Modules own their steps. Execution journals versions in `hai_schema_migrations`; SQLite/PostgreSQL are transactional, while MySQL implicit DDL commits require reentrant steps. Failures do not advance versions; retries inspect real state. Older code rejects newer ledgers. Stop old writers and back up DB/session cache first. Rollback restores compatible database and binaries together. See the [upgrade guide](../../docs/usage/README.en.md).
|
|
609
|
+
|
|
610
|
+
## Configuration
|
|
611
|
+
|
|
612
|
+
Lifecycle: init takes backend/business configuration; use the module only after success. Await close before releasing borrowed dependencies. Follow the quick-start example and public signatures for exact return values.
|
|
613
|
+
|
|
614
|
+
`@h-ai/reldb/schemas` exports the same configuration schemas and types with only Zod, without loading the database singleton or drivers. CLI and browser code can validate configuration through this entry. MySQL TLS accepts `false` or a strict object; `{}` verifies the certificate chain and DNS identity by default. PostgreSQL retains its boolean, string mode and object contract.
|
|
615
|
+
|
|
616
|
+
```ts
|
|
617
|
+
import { ReldbConfigSchema } from '@h-ai/reldb/schemas'
|
|
618
|
+
|
|
619
|
+
const checked = ReldbConfigSchema.safeParse({ type: 'mysql', database: 'shop', ssl: {} })
|
|
620
|
+
if (!checked.success)
|
|
621
|
+
throw new Error('Invalid database configuration')
|
|
622
|
+
// checked.data.ssl: { rejectUnauthorized: true, verifyIdentity: true }
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
Use either a connection string (`url`) or separate fields (`host/port/database/user/password`).
|
|
626
|
+
|
|
627
|
+
```ts
|
|
628
|
+
// SQLite
|
|
629
|
+
await reldb.init({ type: 'sqlite', database: './data.db' })
|
|
630
|
+
|
|
631
|
+
// PostgreSQL
|
|
632
|
+
await reldb.init({ type: 'postgresql', url: 'postgres://user:pass@localhost:5432/mydb', database: 'mydb' })
|
|
633
|
+
|
|
634
|
+
// MySQL
|
|
635
|
+
await reldb.init({
|
|
636
|
+
type: 'mysql',
|
|
637
|
+
host: 'localhost',
|
|
638
|
+
port: 3306,
|
|
639
|
+
database: 'mydb',
|
|
640
|
+
user: 'admin',
|
|
641
|
+
password: 'secret',
|
|
642
|
+
mysql: { charset: 'utf8mb4' },
|
|
643
|
+
pool: { max: 10, acquireTimeout: 5000, queueLimit: 100 },
|
|
644
|
+
})
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
### Connection pools and MySQL URLs
|
|
648
|
+
|
|
649
|
+
MySQL `pool` accepts only `max`, `acquireTimeout` and `queueLimit`, defaulting to 10, 10000 milliseconds and 1000 even when omitted. Unsupported pool fields such as `min` and `idleTimeout`, and the unimplemented `mysql.timezone`, fail configuration validation. Idle connections remain subject to mysql2/server disconnection and `close()`; the module promises no MySQL minimum retention or configurable idle expiry. [mysql2 3.24.5 options](https://github.com/sidorares/node-mysql2/blob/v3.24.5/typings/mysql/lib/Pool.d.ts) differ from PostgreSQL.
|
|
650
|
+
|
|
651
|
+
| Field | PostgreSQL (defaults when `pool` is provided) | MySQL |
|
|
652
|
+
| ---------------- | ------------------------------------------------------------- | --------------------------------------------------- |
|
|
653
|
+
| `max` | 10 | 10, counting borrowed and connecting slots together |
|
|
654
|
+
| `min` | 1, retention floor for established connections; no prewarming | Unsupported; rejected |
|
|
655
|
+
| `idleTimeout` | 30000 ms; 0 disables | Unsupported; rejected |
|
|
656
|
+
| `acquireTimeout` | 10000 ms; 0 disables | 10000 ms; integer from 1 to 2147483647 |
|
|
657
|
+
| `queueLimit` | Unsupported | 1000; positive integer limiting queued requests |
|
|
658
|
+
|
|
659
|
+
PostgreSQL uses pg defaults when `pool` is omitted; see [pg.Pool](https://node-postgres.com/apis/pool) for retention semantics. All MySQL SQL, DDL, CRUD, batch and transaction entry points use a bounded FIFO acquisition queue. Waiting and establishment share one deadline: expiry returns `TIMEOUT`, and a full queue returns `POOL_EXHAUSTED`. Expired waiters are removed; late connections are released without executing SQL and retain their capacity slot until settled. The deadline is checked on delivery and before starting work. It does not limit SQL or transactions that have already started.
|
|
660
|
+
|
|
661
|
+
MySQL `close()` rejects new and queued requests immediately, then waits for borrowed connections, transactions and pending establishment before ending the pool. The driver uses the same value for `connectTimeout`; shutdown may wait for that timeout or existing work to finish. Applications must finish transactions. Environment variables use `HAI_DB_POOL_*`: `MIN` and `IDLETIMEOUT` apply only to PostgreSQL; MySQL `QUEUELIMIT` bounds queued requests.
|
|
662
|
+
|
|
663
|
+
MySQL accepts a `mysql://` URL without split connection fields; `database` is required without a URL. URL host/port/database/user/password take precedence over conflicting split fields. `pool`, `mysql.charset`, `ssl` and `operationLog` remain independent options. Query strings and fragments are rejected, so hidden URL parameters cannot change TLS or pool policy. Connection logs sanitize URLs. This example assumes `HAI_DB_URL` is set:
|
|
664
|
+
|
|
665
|
+
```ts
|
|
666
|
+
const initialized = await reldb.init({
|
|
667
|
+
type: 'mysql',
|
|
668
|
+
url: process.env.HAI_DB_URL,
|
|
669
|
+
pool: { max: 10, acquireTimeout: 5000, queueLimit: 100 }
|
|
670
|
+
})
|
|
671
|
+
if (!initialized.success)
|
|
672
|
+
throw new Error(initialized.error.message)
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
An empty MySQL `url` (including an empty environment placeholder) selects host/port/database/user/password; nonempty URLs remain strictly validated.
|
|
676
|
+
|
|
677
|
+
#### MySQL TLS
|
|
678
|
+
|
|
679
|
+
Omitting `ssl` or using `false` disables TLS. Enabling it requires a strict object; an empty object defaults to `{ rejectUnauthorized: true, verifyIdentity: true }`. `ca` contains trusted CA PEM text, or Node's trust store is used when omitted. Optional client `cert` and `key` must contain PEM text and be provided together. Unknown TLS fields, `true` and PostgreSQL string modes are rejected; arbitrary options and callbacks are not forwarded.
|
|
680
|
+
|
|
681
|
+
[mysql2 3.24.5 identity checks](https://github.com/sidorares/node-mysql2/blob/v3.24.5/lib/base/connection.js) depend on a DNS servername. With both certificate and identity checks enabled, IP targets return `CONFIG_ERROR`; use a hostname matching the certificate DNS SAN. Explicit `verifyIdentity: false` retains only chain verification. `rejectUnauthorized: false` abandons server authentication; encryption alone does not authenticate the server. Disabling either check emits a fixed risk warning and is not reported as a secure configuration.
|
|
682
|
+
|
|
683
|
+
```ts
|
|
684
|
+
// Supply trustedCaPem from deployment trust configuration, not an arbitrary peer certificate.
|
|
685
|
+
const initialized = await reldb.init({
|
|
686
|
+
type: 'mysql',
|
|
687
|
+
host: 'db.example.com',
|
|
688
|
+
database: 'app',
|
|
689
|
+
ssl: { ca: trustedCaPem }
|
|
690
|
+
})
|
|
691
|
+
if (!initialized.success)
|
|
692
|
+
throw new Error(initialized.error.message)
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
TLS tests require OpenSSL (Git for Windows includes one) and local container port forwarding. Each run generates and removes its own CA/private keys and checks trusted DNS, wrong CA/hostname rejection, `REQUIRE X509` client authentication and actual session `Ssl_cipher`. Disabling verification is not accepted as proof of authenticated TLS.
|
|
696
|
+
|
|
697
|
+
#### SQLite execution (asynchronous worker)
|
|
698
|
+
|
|
699
|
+
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`):
|
|
700
|
+
|
|
701
|
+
```ts
|
|
702
|
+
await reldb.init({
|
|
703
|
+
type: 'sqlite',
|
|
704
|
+
database: './data.db',
|
|
705
|
+
sqlite: {
|
|
706
|
+
executor: 'worker', // 'sync'(默认)| 'worker'
|
|
707
|
+
walMode: true, // 默认 true
|
|
708
|
+
workerConnectTimeoutMs: 10000, // worker 打开连接超时,默认 10000
|
|
709
|
+
},
|
|
710
|
+
})
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
In worker mode, queries, writes, and transactions run serially on the worker thread. Public APIs and return values remain identical to synchronous mode.
|
|
714
|
+
|
|
715
|
+
#### Operation logging
|
|
716
|
+
|
|
717
|
+
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.
|
|
718
|
+
|
|
719
|
+
```yaml
|
|
720
|
+
# config/_db.yml
|
|
721
|
+
type: sqlite
|
|
722
|
+
database: ./data/app.db
|
|
723
|
+
operationLog:
|
|
724
|
+
read: false # query / get / queryPage
|
|
725
|
+
write: false # DDL, execute / batch, tx.begin / tx.commit / tx.rollback
|
|
726
|
+
maxLength: 1000 # maximum numeric summary length; 0 omits the summary, SQL text and values are never logged
|
|
727
|
+
level: debug # info | debug | trace,默认 debug
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
When loading through `core.config.load('db', ...)`, convention-based variables such as `HAI_DB_TYPE`,
|
|
731
|
+
`HAI_DB_DATABASE`, and `HAI_DB_OPERATIONLOG_LEVEL` override YAML values.
|
|
732
|
+
|
|
733
|
+
```ts
|
|
734
|
+
await reldb.init({
|
|
735
|
+
type: 'sqlite',
|
|
736
|
+
database: './data.db',
|
|
737
|
+
operationLog: {
|
|
738
|
+
read: true,
|
|
739
|
+
write: true,
|
|
740
|
+
maxLength: 500,
|
|
741
|
+
level: 'debug',
|
|
742
|
+
},
|
|
743
|
+
})
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
Operation logs contain fixed operation names and SQL length, parameter count, and batch count. Failure logs also retain error codes, traceId, and allowlisted, sanitized driver messages and stacks for diagnosis. Full SQL, bound parameters, and connection configuration are not logged directly.
|
|
747
|
+
|
|
748
|
+
The host loads and validates configuration before passing it to this module. Keep secrets in private server environment files. Select the backend, capacity, deadlines and logging for the actual use case; reinitialize and check the result after changes.
|
|
749
|
+
|
|
750
|
+
[Per-option purpose, use cases and examples](../../docs/usage/configuration/data.md).
|
|
751
|
+
|
|
752
|
+
## Error Handling
|
|
753
|
+
|
|
754
|
+
Check result.success for domain operations. Handle failures with the module error definitions and safe messages; retain codes, correlation IDs and sanitized causes in server logs. Synchronous tools, factories, stream iteration and lifecycle calls follow their own signatures. A completed Promise alone is not business success. See each feature for recovery limits.
|
|
595
755
|
|
|
596
756
|
All operations return `HaiResult<T>`; check `result.success` for success or failure.
|
|
597
757
|
|
|
@@ -630,15 +790,7 @@ Common error codes:
|
|
|
630
790
|
|
|
631
791
|
Custom SQL methods in `BaseReldbCrudRepository` subclasses must check `await this.ensureReady()`. Construction starts table creation; service init must await candidate readiness before publication. PostgreSQL retries a single autocommit `CREATE TABLE/INDEX IF NOT EXISTS` once only for `23505/pg_class_relname_nsp_index` catalog contention (also `pg_type_typname_nsp_index` for tables), or a table row-type race reported as `42710` with server routine `heap_create_with_catalog` / `TypeCreate`, consistently through DDL and SQL entry points. A failed retry remains an error. Transactions, business uniqueness errors and other failures are not retried. Initial connection probing attempts temporary DNS `EAI_AGAIN` failures at most three times, with 250/500 ms delays; business SQL is never replayed.
|
|
632
792
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
Production requires PostgreSQL/MySQL. SQLite, including disk files, is for development/explicit tests and does not provide cross-host shared state. Initialization checks apply when NODE_ENV or HAI_CORE_ENV is production; only explicit HAI_E2E=1/HAI_E2E_MOCK=1 tests are exempt. [State, concurrency and recovery (Chinese)](../../.agents/instructions/distributed-state.md)。
|
|
636
|
-
|
|
637
|
-
## Module schema migrations
|
|
638
|
-
|
|
639
|
-
`schemaColumns(reldb, table, sql?)` inspects real columns. `migrateSchema(reldb, module, steps, { dryRun: true })` creates no ledger. Modules own their steps. Execution journals versions in `hai_schema_migrations`; SQLite/PostgreSQL are transactional, while MySQL implicit DDL commits require reentrant steps. Failures do not advance versions; retries inspect real state. Older code rejects newer ledgers. Stop old writers and back up DB/session cache first. Rollback restores compatible database and binaries together. See the [upgrade guide](../../docs/usage/README.en.md).
|
|
640
|
-
|
|
641
|
-
## Error messages and correlated logs
|
|
793
|
+
### Error messages and correlated logs
|
|
642
794
|
|
|
643
795
|
Unique constraint violations return `HaiReldbError.DUPLICATE_ENTRY` (409); timeout, pool exhaustion and other existing error codes remain intact. Branch on error codes rather than localized messages or driver text. Logs preserve driver code/schema/routine/constraint and stack line numbers; numeric bound values do not obscure these diagnostic identifiers.
|
|
644
796
|
|
|
@@ -646,7 +798,7 @@ HTTP errors carry safe localized messages and a reference ID. ToastContainer hid
|
|
|
646
798
|
|
|
647
799
|
Use `core.error.toPublic(error, traceId?)` at HTTP/UI boundaries and `core.error.formatMessage(error)` after translating messages; formatting is idempotent. Pass complete Error objects to logs instead of discarding their context. Database failures log driver codes, sanitized messages/stacks and table/column context; SQL text, bound values and connection credentials are excluded.
|
|
648
800
|
|
|
649
|
-
## Development
|
|
801
|
+
## Development
|
|
650
802
|
|
|
651
803
|
```bash
|
|
652
804
|
pnpm --filter @h-ai/reldb test
|