@h-ai/reldb 1.0.0-beta.19 → 1.0.0-beta.20

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.
Files changed (4) hide show
  1. package/README.en.md +225 -141
  2. package/README.ja.md +225 -141
  3. package/README.md +225 -141
  4. package/package.json +2 -2
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
- 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.
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,141 +51,105 @@ 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
- ### Configuration
54
+ ## Features
79
55
 
80
- `@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.
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
- import { ReldbConfigSchema } from '@h-ai/reldb/schemas'
84
-
85
- const checked = ReldbConfigSchema.safeParse({ type: 'mysql', database: 'shop', ssl: {} })
86
- if (!checked.success)
87
- throw new Error('Invalid database configuration')
88
- // checked.data.ssl: { rejectUnauthorized: true, verifyIdentity: true }
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
- Use either a connection string (`url`) or separate fields (`host/port/database/user/password`).
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
- ```ts
94
- // SQLite
95
- await reldb.init({ type: 'sqlite', database: './data.db' })
70
+ ### Operations and results
96
71
 
97
- // PostgreSQL
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
- // MySQL
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
- #### Connection pools and MySQL URLs
76
+ #### reldb.ddl
114
77
 
115
- 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.
78
+ Maintain tables, columns and indexes. Supply trusted identifiers, column/index definitions or DDL; success contains void, invalid changes fail.
116
79
 
117
- | Field | PostgreSQL (defaults when `pool` is provided) | MySQL |
118
- | ---------------- | ------------------------------------------------------------- | --------------------------------------------------- |
119
- | `max` | 10 | 10, counting borrowed and connecting slots together |
120
- | `min` | 1, retention floor for established connections; no prewarming | Unsupported; rejected |
121
- | `idleTimeout` | 30000 ms; 0 disables | Unsupported; rejected |
122
- | `acquireTimeout` | 10000 ms; 0 disables | 10000 ms; integer from 1 to 2147483647 |
123
- | `queueLimit` | Unsupported | 1000; positive integer limiting queued requests |
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
- 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.
126
-
127
- 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.
91
+ #### reldb.sql
128
92
 
129
- 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:
93
+ Read/write business data with bound params: rows, first row/null, write result, void or a page respectively; batch takes statement objects.
130
94
 
131
- ```ts
132
- const initialized = await reldb.init({
133
- type: 'mysql',
134
- url: process.env.HAI_DB_URL,
135
- pool: { max: 10, acquireTimeout: 5000, queueLimit: 100 }
136
- })
137
- if (!initialized.success)
138
- throw new Error(initialized.error.message)
139
- ```
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 } })` |
140
102
 
141
- An empty MySQL `url` (including an empty environment placeholder) selects host/port/database/user/password; nonempty URLs remain strictly validated.
103
+ #### reldb.pagination
142
104
 
143
- #### MySQL TLS
105
+ Pagination: normalize accepts page options and limits and returns normalized options; build accepts items, total and paging and returns the page object directly.
144
106
 
145
- 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.
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 })` |
146
111
 
147
- [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.
112
+ #### reldb
148
113
 
149
- ```ts
150
- // Supply trustedCaPem from deployment trust configuration, not an arbitrary peer certificate.
151
- const initialized = await reldb.init({
152
- type: 'mysql',
153
- host: 'db.example.com',
154
- database: 'app',
155
- ssl: { ca: trustedCaPem }
156
- })
157
- if (!initialized.success)
158
- throw new Error(initialized.error.message)
159
- ```
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.
160
115
 
161
- 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 })` |
162
120
 
163
- #### SQLite execution (asynchronous worker)
121
+ #### reldb.tx
164
122
 
165
- 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`):
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.
166
124
 
167
- ```ts
168
- await reldb.init({
169
- type: 'sqlite',
170
- database: './data.db',
171
- sqlite: {
172
- executor: 'worker', // 'sync'(默认)| 'worker'
173
- walMode: true, // 默认 true
174
- workerConnectTimeoutMs: 10000, // worker 打开连接超时,默认 10000
175
- },
176
- })
177
- ```
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 })` |
178
129
 
179
- In worker mode, queries, writes, and transactions run serially on the worker thread. Public APIs and return values remain identical to synchronous mode.
130
+ #### reldb.crud
180
131
 
181
- #### Operation logging
132
+ Supply table/field mappings for a business entity; obtain a repository directly for CRUD and pagination without exposing a database connection.
182
133
 
183
- 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.
134
+ | Entry | Input parameters | Successful result | Call example |
135
+ | ------------------ | ---------------- | ---------------------------- | ----------------------------------------- |
136
+ | `reldb.crud.table` | config | `ReldbCrudRepository<TItem>` | `reldb.crud.table(orderRepositoryConfig)` |
184
137
 
185
- ```yaml
186
- # config/_db.yml
187
- type: sqlite
188
- database: ./data/app.db
189
- operationLog:
190
- read: false # query / get / queryPage
191
- write: false # DDL, execute / batch, tx.begin / tx.commit / tx.rollback
192
- maxLength: 1000 # maximum numeric summary length; 0 omits the summary, SQL text and values are never logged
193
- level: debug # info | debug | trace,默认 debug
194
- ```
138
+ #### reldb.json
195
139
 
196
- When loading through `core.config.load('db', ...)`, convention-based variables such as `HAI_DB_TYPE`,
197
- `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.
198
141
 
199
- ```ts
200
- await reldb.init({
201
- type: 'sqlite',
202
- database: './data.db',
203
- operationLog: {
204
- read: true,
205
- write: true,
206
- maxLength: 500,
207
- level: 'debug',
208
- },
209
- })
210
- ```
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' })` |
211
149
 
212
- 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.
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.
213
153
 
214
154
  ### Transactions
215
155
 
@@ -659,7 +599,159 @@ const { sql: rmSql, params: rmParams } = reldb.json.remove('meta', '$.reviewer')
659
599
  await reldb.sql.execute(`UPDATE products SET meta = ${rmSql} WHERE id = ?`, [...rmParams, 1])
660
600
  ```
661
601
 
662
- ### Error handling
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.
663
755
 
664
756
  All operations return `HaiResult<T>`; check `result.success` for success or failure.
665
757
 
@@ -698,15 +790,7 @@ Common error codes:
698
790
 
699
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.
700
792
 
701
- ## Production shared state
702
-
703
- 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)。
704
-
705
- ## Module schema migrations
706
-
707
- `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).
708
-
709
- ## Error messages and correlated logs
793
+ ### Error messages and correlated logs
710
794
 
711
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.
712
796
 
@@ -714,7 +798,7 @@ HTTP errors carry safe localized messages and a reference ID. ToastContainer hid
714
798
 
715
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.
716
800
 
717
- ## Development and testing
801
+ ## Development
718
802
 
719
803
  ```bash
720
804
  pnpm --filter @h-ai/reldb test