@h-ai/reldb 0.1.0-alpha.67 → 0.1.0-alpha.68
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 +95 -12
- package/README.ja.md +95 -12
- package/README.md +95 -12
- package/dist/chunk-SDS2ZT3V.js +132 -0
- package/dist/chunk-SDS2ZT3V.js.map +1 -0
- package/dist/index.d.ts +586 -632
- package/dist/index.js +762 -327
- package/dist/index.js.map +1 -1
- package/dist/providers/sqlite/reldb-sqlite-worker.mjs +101 -70
- package/dist/schemas.d.ts +490 -0
- package/dist/schemas.js +3 -0
- package/dist/schemas.js.map +1 -0
- package/package.json +7 -2
package/README.en.md
CHANGED
|
@@ -8,6 +8,20 @@
|
|
|
8
8
|
|
|
9
9
|
A relational database module with a unified `reldb` object for SQLite, PostgreSQL, and MySQL.
|
|
10
10
|
|
|
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
|
+
|
|
11
25
|
## Core features
|
|
12
26
|
|
|
13
27
|
- Connect to SQLite, PostgreSQL and MySQL through one API.
|
|
@@ -59,10 +73,21 @@ const user = await userCrud.findById(1)
|
|
|
59
73
|
await reldb.close()
|
|
60
74
|
```
|
|
61
75
|
|
|
62
|
-
`reldb.config` returns a **redacted configuration snapshot**. Database `password
|
|
76
|
+
`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.
|
|
63
77
|
|
|
64
78
|
### Configuration
|
|
65
79
|
|
|
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.
|
|
81
|
+
|
|
82
|
+
```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 }
|
|
89
|
+
```
|
|
90
|
+
|
|
66
91
|
Use either a connection string (`url`) or separate fields (`host/port/database/user/password`).
|
|
67
92
|
|
|
68
93
|
```ts
|
|
@@ -70,7 +95,7 @@ Use either a connection string (`url`) or separate fields (`host/port/database/u
|
|
|
70
95
|
await reldb.init({ type: 'sqlite', database: './data.db' })
|
|
71
96
|
|
|
72
97
|
// PostgreSQL
|
|
73
|
-
await reldb.init({ type: 'postgresql', url: 'postgres://user:pass@localhost:5432/mydb' })
|
|
98
|
+
await reldb.init({ type: 'postgresql', url: 'postgres://user:pass@localhost:5432/mydb', database: 'mydb' })
|
|
74
99
|
|
|
75
100
|
// MySQL
|
|
76
101
|
await reldb.init({
|
|
@@ -81,9 +106,51 @@ await reldb.init({
|
|
|
81
106
|
user: 'admin',
|
|
82
107
|
password: 'secret',
|
|
83
108
|
mysql: { charset: 'utf8mb4' },
|
|
109
|
+
pool: { max: 10, acquireTimeout: 5000, queueLimit: 100 },
|
|
84
110
|
})
|
|
85
111
|
```
|
|
86
112
|
|
|
113
|
+
#### Connection pools and MySQL URLs
|
|
114
|
+
|
|
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.
|
|
116
|
+
|
|
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 |
|
|
124
|
+
|
|
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.
|
|
128
|
+
|
|
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:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
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
|
+
```
|
|
136
|
+
|
|
137
|
+
An empty MySQL `url` (including an empty environment placeholder) selects host/port/database/user/password; nonempty URLs remain strictly validated.
|
|
138
|
+
|
|
139
|
+
#### MySQL TLS
|
|
140
|
+
|
|
141
|
+
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.
|
|
142
|
+
|
|
143
|
+
[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.
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
// Supply trustedCaPem from deployment trust configuration, not an arbitrary peer certificate.
|
|
147
|
+
const initialized = await reldb.init({ type: 'mysql', host: 'db.example.com', database: 'app', ssl: { ca: trustedCaPem } })
|
|
148
|
+
if (!initialized.success)
|
|
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.
|
|
153
|
+
|
|
87
154
|
#### SQLite execution (asynchronous worker)
|
|
88
155
|
|
|
89
156
|
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`):
|
|
@@ -113,7 +180,7 @@ database: ./data/app.db
|
|
|
113
180
|
operationLog:
|
|
114
181
|
read: false # query / get / queryPage
|
|
115
182
|
write: false # DDL, execute / batch, tx.begin / tx.commit / tx.rollback
|
|
116
|
-
maxLength: 1000 #
|
|
183
|
+
maxLength: 1000 # maximum numeric summary length; 0 omits the summary, SQL text and values are never logged
|
|
117
184
|
level: debug # info | debug | trace,默认 debug
|
|
118
185
|
```
|
|
119
186
|
|
|
@@ -133,6 +200,8 @@ await reldb.init({
|
|
|
133
200
|
})
|
|
134
201
|
```
|
|
135
202
|
|
|
203
|
+
Operation logs contain only fixed operation names and numeric SQL/parameter/batch counts. SQL text, values and driver error content are excluded, including failure paths.
|
|
204
|
+
|
|
136
205
|
### Transactions
|
|
137
206
|
|
|
138
207
|
Three approaches:
|
|
@@ -302,6 +371,8 @@ await reldb.tx.wrap(async (tx) => {
|
|
|
302
371
|
|
|
303
372
|
Business repositories extend `BaseReldbCrudRepository` and define field mappings with `fields` for automatic table creation and type conversion:
|
|
304
373
|
|
|
374
|
+
Use `BIGINT` for Unix milliseconds (64-bit INTEGER on SQLite, BIGINT on PostgreSQL/MySQL). A regular INTEGER on PostgreSQL/MySQL cannot hold current epoch milliseconds. Raw SQL/CRUD and base repository reads preserve driver BIGINT types: number/string/bigint. Business repositories requiring number must validate safe integers before conversion to avoid silent precision loss. The base repository reads TIMESTAMP fields as Date.
|
|
375
|
+
|
|
305
376
|
```ts
|
|
306
377
|
import type { DmlWithTxOperations, ReldbCrudFieldDefinition } from '@h-ai/reldb'
|
|
307
378
|
import { BaseReldbCrudRepository, reldb } from '@h-ai/reldb'
|
|
@@ -443,6 +514,8 @@ await reldb.sql.execute(
|
|
|
443
514
|
)
|
|
444
515
|
```
|
|
445
516
|
|
|
517
|
+
`json.merge(column, patch)` implements RFC 7396 object patches on all three backends: preserve untouched nested keys, delete keys targeted by null, and replace arrays as a whole. PostgreSQL applies parameterized steps in a recursive CTE within one SQL statement, with no installed function or application read-modify-write. The patch root must be a JSON-serializable object; arbitrary JSON patch roots are outside this contract. Column expressions are developer-owned SQL; paths and values remain parameters. See [RFC 7396](https://www.rfc-editor.org/rfc/rfc7396) and [PostgreSQL JSON operators](https://www.postgresql.org/docs/16/functions-json.html).
|
|
518
|
+
|
|
446
519
|
#### Complete example
|
|
447
520
|
|
|
448
521
|
```ts
|
|
@@ -503,15 +576,23 @@ if (!result.success) {
|
|
|
503
576
|
|
|
504
577
|
Common error codes:
|
|
505
578
|
|
|
506
|
-
| Error code | code | Description
|
|
507
|
-
| ------------------------------------ | --------------- |
|
|
508
|
-
| `HaiReldbError.NOT_INITIALIZED` | `hai:reldb:010` | Not initialized
|
|
509
|
-
| `HaiReldbError.CONNECTION_FAILED` | `hai:reldb:001` | Connection failed
|
|
510
|
-
| `HaiReldbError.QUERY_FAILED` | `hai:reldb:002` | Query failed
|
|
511
|
-
| `HaiReldbError.CONSTRAINT_VIOLATION` | `hai:reldb:003` | Constraint violation
|
|
512
|
-
| `HaiReldbError.TRANSACTION_FAILED` | `hai:reldb:004` | Transaction failed
|
|
513
|
-
| `HaiReldbError.DDL_FAILED` | `hai:reldb:020` | DDL operation failed
|
|
514
|
-
| `HaiReldbError.CONFIG_ERROR` | `hai:reldb:013` | Invalid configuration
|
|
579
|
+
| Error code | code | Description |
|
|
580
|
+
| ------------------------------------ | --------------- | ----------------------------------- |
|
|
581
|
+
| `HaiReldbError.NOT_INITIALIZED` | `hai:reldb:010` | Not initialized |
|
|
582
|
+
| `HaiReldbError.CONNECTION_FAILED` | `hai:reldb:001` | Connection failed |
|
|
583
|
+
| `HaiReldbError.QUERY_FAILED` | `hai:reldb:002` | Query failed |
|
|
584
|
+
| `HaiReldbError.CONSTRAINT_VIOLATION` | `hai:reldb:003` | Constraint violation |
|
|
585
|
+
| `HaiReldbError.TRANSACTION_FAILED` | `hai:reldb:004` | Transaction failed |
|
|
586
|
+
| `HaiReldbError.DDL_FAILED` | `hai:reldb:020` | DDL operation failed |
|
|
587
|
+
| `HaiReldbError.CONFIG_ERROR` | `hai:reldb:013` | Invalid configuration |
|
|
588
|
+
| `HaiReldbError.TIMEOUT` | `hai:reldb:009` | MySQL acquisition deadline exceeded |
|
|
589
|
+
| `HaiReldbError.POOL_EXHAUSTED` | `hai:reldb:011` | MySQL acquisition queue is full |
|
|
590
|
+
|
|
591
|
+
Custom SQL methods in `BaseReldbCrudRepository` subclasses must check `await this.ensureReady()`. Construction starts table creation; service init must await candidate readiness before publication. PostgreSQL concurrent first `CREATE TABLE IF NOT EXISTS` retries the same DDL once only for `23505/pg_type_typname_nsp_index` catalog contention; other errors remain failures.
|
|
592
|
+
|
|
593
|
+
## Production shared state
|
|
594
|
+
|
|
595
|
+
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)。
|
|
515
596
|
|
|
516
597
|
## Development and testing
|
|
517
598
|
|
|
@@ -521,6 +602,8 @@ pnpm --filter @h-ai/reldb test
|
|
|
521
602
|
|
|
522
603
|
> MySQL/PostgreSQL tests require Docker.
|
|
523
604
|
|
|
605
|
+
Source JSDoc follows the [shared conventions](../../.agents/instructions/module-conventions.md#jsdoc-与示例验证): public examples use published entry points, and internal examples are checked in their module scope. After building, run `pnpm check:jsdoc --package reldb`; compiling examples does not replace backend behavior tests.
|
|
606
|
+
|
|
524
607
|
## License
|
|
525
608
|
|
|
526
609
|
Apache-2.0
|
package/README.ja.md
CHANGED
|
@@ -8,6 +8,20 @@
|
|
|
8
8
|
|
|
9
9
|
共通の `reldb` オブジェクトで SQLite、PostgreSQL、MySQL を操作するリレーショナルデータベースモジュールです。
|
|
10
10
|
|
|
11
|
+
接続はヘルスチェック成功後に公開します。初期化失敗時は候補リソースを解放し、解放にも失敗した場合は初期化エラーの `error.ext.cleanupPending` が `true` になります。`reldb.close()` は `Promise<HaiResult<void>>` を返し、初期化を待機して並行する終了処理をまとめます。解放開始後は `isInitialized` が `false` となり、設定と操作は利用できません。解放に失敗したリソースは保持され、次の `close()` または `init()` で再試行します。解放が成功するまで代替接続は作成しません。終了結果を必ず確認してください。
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
const closed = await reldb.close()
|
|
15
|
+
if (!closed.success) {
|
|
16
|
+
// 障害への対処後に一度再試行し、再失敗はアプリへ報告します。
|
|
17
|
+
const retried = await reldb.close()
|
|
18
|
+
if (!retried.success)
|
|
19
|
+
throw new Error(retried.error.message)
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
終了処理は既存トランザクションのコミット/ロールバックと解放を待ちます。アプリはトランザクションを終了させてください。SQLite の WAL 設定と worker のハンドシェイクも初期化リソースとして管理します。PostgreSQL の切断済みアイドル接続はプールが除去し、次のクエリで置き換えます。プロセスは終了せず、ログには安全なエラー識別子のみを記録します。
|
|
24
|
+
|
|
11
25
|
## 主な機能
|
|
12
26
|
|
|
13
27
|
- 共通 API で SQLite・PostgreSQL・MySQL に接続。
|
|
@@ -59,10 +73,21 @@ const user = await userCrud.findById(1)
|
|
|
59
73
|
await reldb.close()
|
|
60
74
|
```
|
|
61
75
|
|
|
62
|
-
`reldb.config` は**機密情報をマスクした設定スナップショット**を返します。DB の `password
|
|
76
|
+
`reldb.config` は**機密情報をマスクした設定スナップショット**を返します。DB の `password`、URL 内の認証情報、TLS クライアント秘密鍵 `ssl.key` を非表示にします。状態確認用であり、元の接続情報の再利用には使わないでください。
|
|
63
77
|
|
|
64
78
|
### 設定
|
|
65
79
|
|
|
80
|
+
`@h-ai/reldb/schemas` は同じ設定 Schema と型を Zod のみで公開し、DB インスタンスやドライバーを読み込みません。CLI やブラウザーで設定検証に利用できます。MySQL TLS は `false` または厳密なオブジェクトのみで、`{}` は証明書チェーンと DNS ID を検証します。PostgreSQL の真偽値・文字列モード・オブジェクト契約は維持します。
|
|
81
|
+
|
|
82
|
+
```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 }
|
|
89
|
+
```
|
|
90
|
+
|
|
66
91
|
接続文字列(`url`)または個別フィールド(`host/port/database/user/password`)を指定できます。
|
|
67
92
|
|
|
68
93
|
```ts
|
|
@@ -70,7 +95,7 @@ await reldb.close()
|
|
|
70
95
|
await reldb.init({ type: 'sqlite', database: './data.db' })
|
|
71
96
|
|
|
72
97
|
// PostgreSQL
|
|
73
|
-
await reldb.init({ type: 'postgresql', url: 'postgres://user:pass@localhost:5432/mydb' })
|
|
98
|
+
await reldb.init({ type: 'postgresql', url: 'postgres://user:pass@localhost:5432/mydb', database: 'mydb' })
|
|
74
99
|
|
|
75
100
|
// MySQL
|
|
76
101
|
await reldb.init({
|
|
@@ -81,9 +106,51 @@ await reldb.init({
|
|
|
81
106
|
user: 'admin',
|
|
82
107
|
password: 'secret',
|
|
83
108
|
mysql: { charset: 'utf8mb4' },
|
|
109
|
+
pool: { max: 10, acquireTimeout: 5000, queueLimit: 100 },
|
|
84
110
|
})
|
|
85
111
|
```
|
|
86
112
|
|
|
113
|
+
#### 接続プールと MySQL URL
|
|
114
|
+
|
|
115
|
+
MySQL の `pool` は `max`、`acquireTimeout`、`queueLimit` のみを受け付け、省略時もそれぞれ 10、10000 ミリ秒、1000 を使用します。`min`、`idleTimeout` などの未対応フィールドと未実装の `mysql.timezone` は設定検証で拒否します。アイドル接続は mysql2/サーバーによる切断または `close()` で解放し、最小保持数や設定可能なアイドル回収期限は保証しません。[mysql2 3.24.5 の設定](https://github.com/sidorares/node-mysql2/blob/v3.24.5/typings/mysql/lib/Pool.d.ts)は PostgreSQL と異なります。
|
|
116
|
+
|
|
117
|
+
| フィールド | PostgreSQL(`pool` 指定時の既定値) | MySQL |
|
|
118
|
+
| ---------------- | ----------------------------------- | -------------------------------------- |
|
|
119
|
+
| `max` | 10 | 10、借用中と接続確立中の合計上限 |
|
|
120
|
+
| `min` | 1、既存接続の保持下限。事前作成なし | 未対応、拒否 |
|
|
121
|
+
| `idleTimeout` | 30000 ミリ秒、0 は無効化 | 未対応、拒否 |
|
|
122
|
+
| `acquireTimeout` | 10000 ミリ秒、0 は無効化 | 10000 ミリ秒、1–2147483647 の整数 |
|
|
123
|
+
| `queueLimit` | 未対応 | 1000、待機リクエスト数の上限、正の整数 |
|
|
124
|
+
|
|
125
|
+
PostgreSQL で `pool` を省略した場合は pg の既定値を使用します。保持と回収の仕様は [pg.Pool](https://node-postgres.com/apis/pool) を参照してください。MySQL の SQL、DDL、CRUD、バッチ、トランザクションは共通の有限 FIFO 待機キューを使います。待機と接続確立に一つの期限を適用し、超過時は `TIMEOUT`、満杯時は `POOL_EXHAUSTED` を返します。期限切れ項目は削除し、遅れて取得した接続は SQL を実行せず解放します。確立処理が完了するまで容量枠を保持します。受領時と処理開始前にも期限を確認しますが、開始済み SQL/トランザクションの実行時間は制限しません。
|
|
126
|
+
|
|
127
|
+
MySQL の `close()` は新規・待機リクエストを直ちに拒否し、借用接続、トランザクション、接続確立の完了後にプールを閉じます。`connectTimeout` にも同じ値を使うため、その期限や既存処理を待つ場合があります。アプリはトランザクションを終了してください。環境変数は `HAI_DB_POOL_*` です。`MIN` / `IDLETIMEOUT` は PostgreSQL 専用で、MySQL の `QUEUELIMIT` は待機総数を制限します。
|
|
128
|
+
|
|
129
|
+
MySQL は `mysql://` の `url` だけでも接続でき、URL がなければ `database` が必須です。URL の host/port/database/user/password を優先し、競合する個別フィールドは接続に使用しません。`pool`、`mysql.charset`、`ssl`、`operationLog` は独立して適用します。query/fragment は拒否し、URL 内の隠れた設定で TLS やプールを変更できません。接続ログの URL は機密情報を伏せます。以下は `HAI_DB_URL` の設定を前提とします:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
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
|
+
```
|
|
136
|
+
|
|
137
|
+
MySQL の空 `url`(空の環境変数を含む)は host/port/database/user/password を使用します。空でない URL は厳密に検証します。
|
|
138
|
+
|
|
139
|
+
#### MySQL TLS
|
|
140
|
+
|
|
141
|
+
`ssl` を省略するか `false` にすると TLS を使用しません。有効化には厳密なオブジェクトを指定し、空オブジェクトの既定値は `{ rejectUnauthorized: true, verifyIdentity: true }` です。`ca` は信頼する CA の PEM 内容で、省略時は Node の信頼ストアを使います。任意のクライアント `cert` / `key` も PEM 内容で、必ず対で指定します。未知の TLS フィールド、`true`、PostgreSQL の文字列モードは拒否し、任意の設定やコールバックは透過しません。
|
|
142
|
+
|
|
143
|
+
[mysql2 3.24.5 の主機名検証](https://github.com/sidorares/node-mysql2/blob/v3.24.5/lib/base/connection.js)は DNS servername に依存します。証明書と名前の両方を検証する場合、IP 接続先は `CONFIG_ERROR` とし、証明書の DNS SAN に一致するホスト名を使います。明示的な `verifyIdentity: false` は証明書チェーンのみを検証します。`rejectUnauthorized: false` はサーバー認証を放棄し、暗号化だけでは相手の身元を確認できません。どちらかの検証を無効にすると固定のリスク警告を記録し、安全な構成とは扱いません。
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
// trustedCaPem は配置時の信頼設定から供給し、任意の接続先証明書をそのまま信頼しません。
|
|
147
|
+
const initialized = await reldb.init({ type: 'mysql', host: 'db.example.com', database: 'app', ssl: { ca: trustedCaPem } })
|
|
148
|
+
if (!initialized.success)
|
|
149
|
+
throw new Error(initialized.error.message)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
TLS テストには OpenSSL(Windows は Git 同梱版も可)とローカルのコンテナー転送が必要です。各実行で CA/秘密鍵を生成・削除し、信頼済み DNS、誤った CA/主機名の拒否、`REQUIRE X509` 認証、実際の `Ssl_cipher` を検証します。検証無効の接続を認証済み TLS の証拠にはしません。
|
|
153
|
+
|
|
87
154
|
#### SQLite の実行方式(非同期 worker)
|
|
88
155
|
|
|
89
156
|
better-sqlite3 は同期ドライバーなので、クエリは Node のメインイベントループをブロックします。WAL は DB ロックの並行性を改善しますが、同期呼び出しによるブロックは解消しません。専用 worker スレッドで SQLite を実行して処理を移せます(単一接続、任意設定、既定は `sync`):
|
|
@@ -113,7 +180,7 @@ database: ./data/app.db
|
|
|
113
180
|
operationLog:
|
|
114
181
|
read: false # query / get / queryPage
|
|
115
182
|
write: false # DDL, execute / batch, tx.begin / tx.commit / tx.rollback
|
|
116
|
-
maxLength: 1000 #
|
|
183
|
+
maxLength: 1000 # 安全な件数サマリーの最大長。0は省略。SQL本文と値は記録しない
|
|
117
184
|
level: debug # info | debug | trace,默认 debug
|
|
118
185
|
```
|
|
119
186
|
|
|
@@ -133,6 +200,8 @@ await reldb.init({
|
|
|
133
200
|
})
|
|
134
201
|
```
|
|
135
202
|
|
|
203
|
+
操作ログには固定の操作名とSQL長・パラメータ数・バッチ数のみを記録します。SQL本文、値、ドライバーのエラー内容は失敗時も出力しません。
|
|
204
|
+
|
|
136
205
|
### トランザクション
|
|
137
206
|
|
|
138
207
|
3 種類の使用方法:
|
|
@@ -302,6 +371,8 @@ await reldb.tx.wrap(async (tx) => {
|
|
|
302
371
|
|
|
303
372
|
業務リポジトリーは `BaseReldbCrudRepository` を継承し、`fields` でフィールドの対応を定義します。テーブル作成と型変換は自動です:
|
|
304
373
|
|
|
374
|
+
Unix ミリ秒には `BIGINT`(SQLite では 64 ビット INTEGER、PostgreSQL/MySQL では BIGINT)を使用します。PostgreSQL/MySQL の通常の INTEGER では現在のミリ秒値を保存できません。生 SQL/CRUD と基底リポジトリーは BIGINT のドライバー型(number/string/bigint)を保持します。number が必要な業務リポジトリーは変換時に安全な整数であることを検証し、精度の損失を防いでください。基底リポジトリーは TIMESTAMP を Date として読み取ります。
|
|
375
|
+
|
|
305
376
|
```ts
|
|
306
377
|
import type { DmlWithTxOperations, ReldbCrudFieldDefinition } from '@h-ai/reldb'
|
|
307
378
|
import { BaseReldbCrudRepository, reldb } from '@h-ai/reldb'
|
|
@@ -443,6 +514,8 @@ await reldb.sql.execute(
|
|
|
443
514
|
)
|
|
444
515
|
```
|
|
445
516
|
|
|
517
|
+
`json.merge(column, patch)` は三つのバックエンドで RFC 7396 のオブジェクトパッチを実装します。未変更の入れ子キーを保持し、null が指定したキーを削除し、配列を全体置換します。PostgreSQL はパラメーター化した再帰 CTE を単一 SQL 内で適用し、関数の事前インストールやアプリ側の読み取り後の書き戻しを必要としません。パッチのルートは JSON シリアライズ可能なオブジェクトに限ります。列式は開発者の静的 SQL で、パスと値はパラメーターです。[RFC 7396](https://www.rfc-editor.org/rfc/rfc7396)・[PostgreSQL JSON 演算](https://www.postgresql.org/docs/16/functions-json.html)。
|
|
518
|
+
|
|
446
519
|
#### 完全な使用例
|
|
447
520
|
|
|
448
521
|
```ts
|
|
@@ -503,15 +576,23 @@ if (!result.success) {
|
|
|
503
576
|
|
|
504
577
|
主なエラーコード:
|
|
505
578
|
|
|
506
|
-
| エラーコード | code | 説明
|
|
507
|
-
| ------------------------------------ | --------------- |
|
|
508
|
-
| `HaiReldbError.NOT_INITIALIZED` | `hai:reldb:010` | 未初期化
|
|
509
|
-
| `HaiReldbError.CONNECTION_FAILED` | `hai:reldb:001` | 接続に失敗
|
|
510
|
-
| `HaiReldbError.QUERY_FAILED` | `hai:reldb:002` | 検索に失敗
|
|
511
|
-
| `HaiReldbError.CONSTRAINT_VIOLATION` | `hai:reldb:003` | 制約違反
|
|
512
|
-
| `HaiReldbError.TRANSACTION_FAILED` | `hai:reldb:004` | トランザクションに失敗
|
|
513
|
-
| `HaiReldbError.DDL_FAILED` | `hai:reldb:020` | DDL 操作に失敗
|
|
514
|
-
| `HaiReldbError.CONFIG_ERROR` | `hai:reldb:013` | 設定エラー
|
|
579
|
+
| エラーコード | code | 説明 |
|
|
580
|
+
| ------------------------------------ | --------------- | -------------------------- |
|
|
581
|
+
| `HaiReldbError.NOT_INITIALIZED` | `hai:reldb:010` | 未初期化 |
|
|
582
|
+
| `HaiReldbError.CONNECTION_FAILED` | `hai:reldb:001` | 接続に失敗 |
|
|
583
|
+
| `HaiReldbError.QUERY_FAILED` | `hai:reldb:002` | 検索に失敗 |
|
|
584
|
+
| `HaiReldbError.CONSTRAINT_VIOLATION` | `hai:reldb:003` | 制約違反 |
|
|
585
|
+
| `HaiReldbError.TRANSACTION_FAILED` | `hai:reldb:004` | トランザクションに失敗 |
|
|
586
|
+
| `HaiReldbError.DDL_FAILED` | `hai:reldb:020` | DDL 操作に失敗 |
|
|
587
|
+
| `HaiReldbError.CONFIG_ERROR` | `hai:reldb:013` | 設定エラー |
|
|
588
|
+
| `HaiReldbError.TIMEOUT` | `hai:reldb:009` | MySQL 接続取得の期限超過 |
|
|
589
|
+
| `HaiReldbError.POOL_EXHAUSTED` | `hai:reldb:011` | MySQL 接続待機キューが満杯 |
|
|
590
|
+
|
|
591
|
+
`BaseReldbCrudRepository` の派生クラスで独自 SQL を実行する前に `await this.ensureReady()` を確認します。構築時に建表を開始するだけなので、サービス init は候補の準備完了後に公開します。PostgreSQL の並行初回 `CREATE TABLE IF NOT EXISTS` は `23505/pg_type_typname_nsp_index` の競合だけ同じ DDL を一度再実行し、他のエラーは失敗のままです。
|
|
592
|
+
|
|
593
|
+
## 本番の共有状態
|
|
594
|
+
|
|
595
|
+
本番は PostgreSQL/MySQL が必須です。ファイルを含む SQLite は開発・明示的テスト用であり、ホスト間の共有状態を保証しません。 NODE_ENV または HAI_CORE_ENV が production の場合、初期化成功前に検証します。明示的な HAI_E2E=1/HAI_E2E_MOCK=1 テストのみ除外します。 [状態・並行制御・復旧範囲(中国語)](../../.agents/instructions/distributed-state.md)。
|
|
515
596
|
|
|
516
597
|
## 開発とテスト
|
|
517
598
|
|
|
@@ -521,6 +602,8 @@ pnpm --filter @h-ai/reldb test
|
|
|
521
602
|
|
|
522
603
|
> MySQL/PostgreSQL のテストには Docker が必要です。
|
|
523
604
|
|
|
605
|
+
ソースの JSDoc は[共通規約](../../.agents/instructions/module-conventions.md#jsdoc-与示例验证)に従います。公開例は実際の公開入口、内部例は元のモジュールスコープで型検査します。ビルド後に `pnpm check:jsdoc --package reldb` を実行してください。例の型検査は実バックエンドの動作検証の代わりにはなりません。
|
|
606
|
+
|
|
524
607
|
## ライセンス
|
|
525
608
|
|
|
526
609
|
Apache-2.0
|
package/README.md
CHANGED
|
@@ -8,6 +8,20 @@
|
|
|
8
8
|
|
|
9
9
|
关系型数据库访问模块,通过统一的 `reldb` 对象访问 SQLite、PostgreSQL、MySQL。
|
|
10
10
|
|
|
11
|
+
初始化只在健康检查成功后发布连接;失败时释放候选资源。若清理仍失败,初始化错误的 `error.ext.cleanupPending` 为 `true`。`reldb.close()` 返回 `Promise<HaiResult<void>>`,等待正在进行的初始化,并合并并发关闭。开始清理后 `isInitialized` 为 `false`,配置与业务操作不再可用;清理失败仍保留资源,后续 `close()` 或 `init()` 先重试释放,成功前不会创建替代连接。调用方必须检查关闭结果。
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
const closed = await reldb.close()
|
|
15
|
+
if (!closed.success) {
|
|
16
|
+
// 处理清理故障后有限重试;再次失败应向应用报告。
|
|
17
|
+
const retried = await reldb.close()
|
|
18
|
+
if (!retried.success)
|
|
19
|
+
throw new Error(retried.error.message)
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
关闭会等待已借出的事务提交/回滚并释放句柄;应用必须结束事务才能完成关闭。SQLite 的 WAL 配置与 worker 握手也属于初始化资源链。PostgreSQL 空闲连接断开由连接池移除并在后续查询时替换,不使进程退出;日志仅记录安全错误标识,不输出驱动错误原文。
|
|
24
|
+
|
|
11
25
|
## 核心功能清单
|
|
12
26
|
|
|
13
27
|
- 用同一套 API 连接 SQLite、PostgreSQL 和 MySQL。
|
|
@@ -59,10 +73,21 @@ const user = await userCrud.findById(1)
|
|
|
59
73
|
await reldb.close()
|
|
60
74
|
```
|
|
61
75
|
|
|
62
|
-
`reldb.config` 返回的是**脱敏后的配置快照**:数据库 `password
|
|
76
|
+
`reldb.config` 返回的是**脱敏后的配置快照**:数据库 `password`、URL 内嵌凭证和 TLS 客户端私钥 `ssl.key` 会自动隐藏,便于排查状态但不应用于回放原始连接信息。
|
|
63
77
|
|
|
64
78
|
### 配置
|
|
65
79
|
|
|
80
|
+
`@h-ai/reldb/schemas` 直接导出同一组配置 Schema 和类型,只依赖 Zod,不加载数据库实例或驱动,可用于 CLI 或浏览器的配置校验。MySQL TLS 仅接受 `false` 或严格对象,`{}` 默认校验证书链和 DNS 身份;PostgreSQL 保留其布尔值、字符串模式及对象契约。
|
|
81
|
+
|
|
82
|
+
```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 }
|
|
89
|
+
```
|
|
90
|
+
|
|
66
91
|
支持连接字符串(`url`)或分字段(`host/port/database/user/password`)。
|
|
67
92
|
|
|
68
93
|
```ts
|
|
@@ -70,7 +95,7 @@ await reldb.close()
|
|
|
70
95
|
await reldb.init({ type: 'sqlite', database: './data.db' })
|
|
71
96
|
|
|
72
97
|
// PostgreSQL
|
|
73
|
-
await reldb.init({ type: 'postgresql', url: 'postgres://user:pass@localhost:5432/mydb' })
|
|
98
|
+
await reldb.init({ type: 'postgresql', url: 'postgres://user:pass@localhost:5432/mydb', database: 'mydb' })
|
|
74
99
|
|
|
75
100
|
// MySQL
|
|
76
101
|
await reldb.init({
|
|
@@ -81,9 +106,51 @@ await reldb.init({
|
|
|
81
106
|
user: 'admin',
|
|
82
107
|
password: 'secret',
|
|
83
108
|
mysql: { charset: 'utf8mb4' },
|
|
109
|
+
pool: { max: 10, acquireTimeout: 5000, queueLimit: 100 },
|
|
84
110
|
})
|
|
85
111
|
```
|
|
86
112
|
|
|
113
|
+
#### 连接池与 MySQL URL
|
|
114
|
+
|
|
115
|
+
MySQL 的 `pool` 只接受 `max`、`acquireTimeout`、`queueLimit`,省略时默认分别为 10、10000 毫秒、1000。其他池字段(包括 `min`、`idleTimeout`)以及未实现的 `mysql.timezone` 会在配置校验时失败。空闲连接由 mysql2/服务端断开或 `close()` 释放,本模块不承诺 MySQL 的最小保留数或可配置的空闲回收期限;[mysql2 3.24.5 的真实选项](https://github.com/sidorares/node-mysql2/blob/v3.24.5/typings/mysql/lib/Pool.d.ts)与 PostgreSQL 不同。
|
|
116
|
+
|
|
117
|
+
| 字段 | PostgreSQL(显式提供 `pool` 时的默认值) | MySQL |
|
|
118
|
+
| ---------------- | ---------------------------------------- | -------------------------------------- |
|
|
119
|
+
| `max` | 10 | 10,借用和正在建连合计不超过此值 |
|
|
120
|
+
| `min` | 1,已建立连接的保留下限,不主动预建 | 不支持,明确拒绝 |
|
|
121
|
+
| `idleTimeout` | 30000 毫秒,0 禁用 | 不支持,明确拒绝 |
|
|
122
|
+
| `acquireTimeout` | 10000 毫秒,0 禁用 | 10000 毫秒,必须为 1–2147483647 的整数 |
|
|
123
|
+
| `queueLimit` | 不支持 | 1000,等待请求上限,必须为正整数 |
|
|
124
|
+
|
|
125
|
+
PostgreSQL 未提供 `pool` 时沿用 pg 驱动默认值;保留与空闲回收语义见 [pg.Pool](https://node-postgres.com/apis/pool)。MySQL 所有 SQL、DDL、CRUD、批量和事务入口共用有界 FIFO 借用队列:排队与建立连接共用一个 `acquireTimeout` 截止期限,超时返回 `TIMEOUT`,队列满返回 `POOL_EXHAUSTED`。超时排队请求会移除;已开始但迟到的建连只释放连接,不启动 SQL,结算前仍占容量槽位。取得连接及业务执行前再次校验期限;它不限制已启动 SQL 或事务的执行时长。
|
|
126
|
+
|
|
127
|
+
MySQL `close()` 立即拒绝新请求并结束排队等待,随后等待已借出连接、事务及在途建连结算再关闭池。驱动 `connectTimeout` 使用相同超时值;关闭可能等到该建连时限或现有 SQL/事务结束,应用仍须主动结束事务。环境变量使用 `HAI_DB_POOL_*`,`HAI_DB_POOL_MIN` / `HAI_DB_POOL_IDLETIMEOUT` 仅用于 PostgreSQL,MySQL 的 `HAI_DB_POOL_QUEUELIMIT` 控制等待总量。
|
|
128
|
+
|
|
129
|
+
MySQL 支持仅传 `url`(`mysql://`),未给 URL 时 `database` 必填。URL 中的 host/port/database/user/password 优先,冲突的分字段不参与连接;`pool`、`mysql.charset`、`ssl` 和 `operationLog` 独立适用。URL 仅接受连接地址,query/fragment 会被拒绝,不能用隐藏 URL 参数改变 TLS 或池策略。URL 模式的连接日志会脱敏。以下示例假定已设置 `HAI_DB_URL`:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
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
|
+
```
|
|
136
|
+
|
|
137
|
+
MySQL 的空 `url`(例如空环境变量占位)表示使用 host/port/database/user/password;非空 URL 仍严格校验。
|
|
138
|
+
|
|
139
|
+
#### MySQL TLS
|
|
140
|
+
|
|
141
|
+
`ssl` 省略或为 `false` 时不启用 TLS;启用时只能传严格对象,空对象默认 `{ rejectUnauthorized: true, verifyIdentity: true }`。`ca` 是受信 CA 的 PEM 内容,省略时使用 Node 信任库;可选客户端 `cert` / `key` 也必须是 PEM 内容且成对提供。未知 TLS 字段、`true` 和 PostgreSQL 字符串模式明确拒绝,不提供任意参数/回调透传。
|
|
142
|
+
|
|
143
|
+
当前 [mysql2 3.24.5 的主机身份校验](https://github.com/sidorares/node-mysql2/blob/v3.24.5/lib/base/connection.js)依赖 DNS servername。因此在证书链及身份校验均开启时,IP 目标返回 `CONFIG_ERROR`,须使用与证书 DNS SAN 匹配的主机名。显式 `verifyIdentity: false` 只保留证书链校验;`rejectUnauthorized: false` 放弃服务端认证,即使存在加密也不能确认服务端身份。关闭任一验证会输出固定风险日志,不会称为安全连接。
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
// trustedCaPem 由部署时的受信 CA 提供,不是任意远端返回的证书。
|
|
147
|
+
const initialized = await reldb.init({ type: 'mysql', host: 'db.example.com', database: 'app', ssl: { ca: trustedCaPem } })
|
|
148
|
+
if (!initialized.success)
|
|
149
|
+
throw new Error(initialized.error.message)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
TLS 定向测试需要 OpenSSL(Windows 可使用 Git 附带版本)及本机容器端口转发;每轮生成并销毁 CA/私钥,通过真实 MySQL 验证受信 DNS、错误 CA/主机名拒绝、`REQUIRE X509` 客户端认证和会话 `Ssl_cipher`,不使用关闭验证替代认证成功证据。
|
|
153
|
+
|
|
87
154
|
#### SQLite 执行方式(worker 异步)
|
|
88
155
|
|
|
89
156
|
better-sqlite3 是同步驱动,查询会阻塞 Node 主线程事件循环。WAL 只能改善数据库锁并发,无法避免同步调用占用主线程。需要卸载这一阻塞时,可把 SQLite 切到专用 worker 线程执行(单连接,opt-in,默认 `sync`):
|
|
@@ -113,7 +180,7 @@ database: ./data/app.db
|
|
|
113
180
|
operationLog:
|
|
114
181
|
read: false # query / get / queryPage
|
|
115
182
|
write: false # DDL, execute / batch, tx.begin / tx.commit / tx.rollback
|
|
116
|
-
maxLength: 1000 #
|
|
183
|
+
maxLength: 1000 # 安全数量摘要的最大字符数,0省略摘要;不记录SQL原文/参数值
|
|
117
184
|
level: debug # info | debug | trace,默认 debug
|
|
118
185
|
```
|
|
119
186
|
|
|
@@ -133,6 +200,8 @@ await reldb.init({
|
|
|
133
200
|
})
|
|
134
201
|
```
|
|
135
202
|
|
|
203
|
+
操作日志仅包含固定操作名和SQL长度、参数数量、批量数量等安全摘要;失败路径也不记录SQL原文、值或驱动错误内容。
|
|
204
|
+
|
|
136
205
|
### 事务
|
|
137
206
|
|
|
138
207
|
三种使用方式:
|
|
@@ -302,6 +371,8 @@ await reldb.tx.wrap(async (tx) => {
|
|
|
302
371
|
|
|
303
372
|
业务仓库继承 `BaseReldbCrudRepository`,通过 `fields` 定义字段映射,自动建表、类型转换:
|
|
304
373
|
|
|
374
|
+
Unix 毫秒字段使用 `BIGINT`(SQLite 为 64 位 INTEGER,PostgreSQL/MySQL 为 BIGINT),普通 INTEGER 在 PostgreSQL/MySQL 中无法容纳当前毫秒时间。BIGINT 的原始 SQL/CRUD 和基类读取值保留驱动的 number/string/bigint 类型;需要 number 的业务仓库须校验安全整数后转换,避免静默精度丢失。TIMESTAMP 字段由基类读取为 Date。
|
|
375
|
+
|
|
305
376
|
```ts
|
|
306
377
|
import type { DmlWithTxOperations, ReldbCrudFieldDefinition } from '@h-ai/reldb'
|
|
307
378
|
import { BaseReldbCrudRepository, reldb } from '@h-ai/reldb'
|
|
@@ -443,6 +514,8 @@ await reldb.sql.execute(
|
|
|
443
514
|
)
|
|
444
515
|
```
|
|
445
516
|
|
|
517
|
+
`json.merge(column, patch)` 在三种后端均实现 RFC 7396 的对象补丁:递归保留未改键,补丁 null 删除指定键,数组整体替换。PostgreSQL 用参数化递归 CTE 在单次 SQL 中修改当前行,不要求预装函数,也不先在应用读取再写回。补丁根必须是可 JSON 序列化的对象;此契约不扩展为任意 JSON 根补丁。列表达式由开发者静态提供,动态路径和值全部作为参数。参见 [RFC 7396](https://www.rfc-editor.org/rfc/rfc7396) 和 [PostgreSQL JSON 运算](https://www.postgresql.org/docs/16/functions-json.html)。
|
|
518
|
+
|
|
446
519
|
#### 完整示例
|
|
447
520
|
|
|
448
521
|
```ts
|
|
@@ -503,15 +576,23 @@ if (!result.success) {
|
|
|
503
576
|
|
|
504
577
|
常用错误码:
|
|
505
578
|
|
|
506
|
-
| 错误码 | code | 说明
|
|
507
|
-
| ------------------------------------ | --------------- |
|
|
508
|
-
| `HaiReldbError.NOT_INITIALIZED` | `hai:reldb:010` | 未初始化
|
|
509
|
-
| `HaiReldbError.CONNECTION_FAILED` | `hai:reldb:001` | 连接失败
|
|
510
|
-
| `HaiReldbError.QUERY_FAILED` | `hai:reldb:002` | 查询失败
|
|
511
|
-
| `HaiReldbError.CONSTRAINT_VIOLATION` | `hai:reldb:003` | 约束违反
|
|
512
|
-
| `HaiReldbError.TRANSACTION_FAILED` | `hai:reldb:004` | 事务失败
|
|
513
|
-
| `HaiReldbError.DDL_FAILED` | `hai:reldb:020` | DDL 操作失败
|
|
514
|
-
| `HaiReldbError.CONFIG_ERROR` | `hai:reldb:013` | 配置错误
|
|
579
|
+
| 错误码 | code | 说明 |
|
|
580
|
+
| ------------------------------------ | --------------- | -------------------------- |
|
|
581
|
+
| `HaiReldbError.NOT_INITIALIZED` | `hai:reldb:010` | 未初始化 |
|
|
582
|
+
| `HaiReldbError.CONNECTION_FAILED` | `hai:reldb:001` | 连接失败 |
|
|
583
|
+
| `HaiReldbError.QUERY_FAILED` | `hai:reldb:002` | 查询失败 |
|
|
584
|
+
| `HaiReldbError.CONSTRAINT_VIOLATION` | `hai:reldb:003` | 约束违反 |
|
|
585
|
+
| `HaiReldbError.TRANSACTION_FAILED` | `hai:reldb:004` | 事务失败 |
|
|
586
|
+
| `HaiReldbError.DDL_FAILED` | `hai:reldb:020` | DDL 操作失败 |
|
|
587
|
+
| `HaiReldbError.CONFIG_ERROR` | `hai:reldb:013` | 配置错误 |
|
|
588
|
+
| `HaiReldbError.TIMEOUT` | `hai:reldb:009` | MySQL 连接获取超过截止期限 |
|
|
589
|
+
| `HaiReldbError.POOL_EXHAUSTED` | `hai:reldb:011` | MySQL 连接等待队列已满 |
|
|
590
|
+
|
|
591
|
+
派生 `BaseReldbCrudRepository` 的自定义 SQL 方法先检查 `await this.ensureReady()`;构造函数启动自动建表不等于仓库就绪,服务 init 必须等待候选仓库的就绪入口再发布。PostgreSQL 并发首次 `CREATE TABLE IF NOT EXISTS` 的 `23505/pg_type_typname_nsp_index` 目录冲突仅重执行相同 DDL 一次,其他错误继续失败。
|
|
592
|
+
|
|
593
|
+
## 生产共享状态
|
|
594
|
+
|
|
595
|
+
生产仅使用 PostgreSQL/MySQL;SQLite(含磁盘文件)只用于开发/明确测试,不能作为跨主机共享状态。 NODE_ENV 或 HAI_CORE_ENV 为 production 时在初始化发布成功前校验;仅 HAI_E2E=1/HAI_E2E_MOCK=1 明确测试可豁免。 [状态、并发和恢复边界](../../.agents/instructions/distributed-state.md)。
|
|
515
596
|
|
|
516
597
|
## 开发测试
|
|
517
598
|
|
|
@@ -521,6 +602,8 @@ pnpm --filter @h-ai/reldb test
|
|
|
521
602
|
|
|
522
603
|
> MySQL/PostgreSQL 测试需要 Docker。
|
|
523
604
|
|
|
605
|
+
源码 JSDoc 遵循[统一规范](../../.agents/instructions/module-conventions.md#jsdoc-与示例验证):公开示例使用实际发布入口,内部示例在原模块作用域检查。构建后执行 `pnpm check:jsdoc --package reldb`;示例编译不代替真实后端行为验证。
|
|
606
|
+
|
|
524
607
|
## 许可证
|
|
525
608
|
|
|
526
609
|
Apache-2.0
|