@h-ai/reldb 1.0.0-beta.2 → 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.
package/README.ja.md CHANGED
@@ -4,33 +4,9 @@
4
4
 
5
5
  [フレームワークのホーム](../../README.ja.md) · 関連モジュール:[@h-ai/core](../core/README.ja.md)
6
6
 
7
- ## 概要
8
-
9
7
  共通の `reldb` オブジェクトで SQLite、PostgreSQL、MySQL を操作するリレーショナルデータベースモジュールです。
10
8
 
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
-
25
- ## 主な機能
26
-
27
- - 共通 API で SQLite・PostgreSQL・MySQL に接続。
28
- - パラメーター付き SQL、テーブル・索引管理、ページ取得。
29
- - トランザクション、CRUD、Repository による業務の読み書き。
30
-
31
- ## 使い始める
32
-
33
- ### クイックスタート
9
+ ## クイックスタート
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` は**機密情報をマスクした設定スナップショット**を返します。DB の `password`、URL 内の認証情報、TLS クライアント秘密鍵 `ssl.key` を非表示にします。状態確認用であり、元の接続情報の再利用には使わないでください。
77
53
 
78
- ### 設定
54
+ ## 機能
79
55
 
80
- `@h-ai/reldb/schemas` は同じ設定 Schema と型を Zod のみで公開し、DB インスタンスやドライバーを読み込みません。CLI やブラウザーで設定検証に利用できます。MySQL TLS は `false` または厳密なオブジェクトのみで、`{}` は証明書チェーンと DNS ID を検証します。PostgreSQL の真偽値・文字列モード・オブジェクト契約は維持します。
56
+ 接続はヘルスチェック成功後に公開します。初期化失敗時は候補リソースを解放し、解放にも失敗した場合は初期化エラーの `error.ext.cleanupPending` が `true` になります。`reldb.close()` は `Promise<HaiResult<void>>` を返し、初期化を待機して並行する終了処理をまとめます。解放開始後は `isInitialized` が `false` となり、設定と操作は利用できません。解放に失敗したリソースは保持され、次の `close()` または `init()` で再試行します。解放が成功するまで代替接続は作成しません。終了結果を必ず確認してください。
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
+ // 障害への対処後に一度再試行し、再失敗はアプリへ報告します。
62
+ const retried = await reldb.close()
63
+ if (!retried.success)
64
+ throw new Error(retried.error.message)
65
+ }
89
66
  ```
90
67
 
91
- 接続文字列(`url`)または個別フィールド(`host/port/database/user/password`)を指定できます。
68
+ 終了処理は既存トランザクションのコミット/ロールバックと解放を待ちます。アプリはトランザクションを終了させてください。SQLite の WAL 設定と worker のハンドシェイクも初期化リソースとして管理します。PostgreSQL の切断済みアイドル接続はプールが除去し、次のクエリで置き換えます。プロセスは終了せず、ログには安全なエラー識別子のみを記録します。
92
69
 
93
- ```ts
94
- // SQLite
95
- await reldb.init({ type: 'sqlite', database: './data.db' })
70
+ ### 操作と入出力
96
71
 
97
- // PostgreSQL
98
- await reldb.init({ type: 'postgresql', url: 'postgres://user:pass@localhost:5432/mydb', database: 'mydb' })
72
+ 本節の完全な初期化手順を先に実行します。例の業務値はホストが検証・認可し、非同期領域結果は success を確認してから data を読みます。void はデータなし完了、null は不存在、同期ファクトリー/ツールは表の型を直接返します。
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
+ 表・列・索引・SQL は信頼するコード、params は業務値の束縛、steps は移行手順、options は事前確認/ページ指定。
112
75
 
113
- #### 接続プールと MySQL URL
76
+ #### reldb.ddl
114
77
 
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 と異なります。
78
+ テーブル・列・索引を管理。信頼する識別子、定義または DDL を渡し、成功は void、構造エラーは失敗。
116
79
 
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、待機リクエスト数の上限、正の整数 |
80
+ | 入口 | 入力引数 | 成功結果 | 呼出し例 |
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 で `pool` を省略した場合は pg の既定値を使用します。保持と回収の仕様は [pg.Pool](https://node-postgres.com/apis/pool) を参照してください。MySQL の SQL、DDL、CRUD、バッチ、トランザクションは共通の有限 FIFO 待機キューを使います。待機と接続確立に一つの期限を適用し、超過時は `TIMEOUT`、満杯時は `POOL_EXHAUSTED` を返します。期限切れ項目は削除し、遅れて取得した接続は SQL を実行せず解放します。確立処理が完了するまで容量枠を保持します。受領時と処理開始前にも期限を確認しますが、開始済み SQL/トランザクションの実行時間は制限しません。
91
+ #### reldb.sql
126
92
 
127
- MySQL の `close()` は新規・待機リクエストを直ちに拒否し、借用接続、トランザクション、接続確立の完了後にプールを閉じます。`connectTimeout` にも同じ値を使うため、その期限や既存処理を待つ場合があります。アプリはトランザクションを終了してください。環境変数は `HAI_DB_POOL_*` です。`MIN` / `IDLETIMEOUT` は PostgreSQL 専用で、MySQL の `QUEUELIMIT` は待機総数を制限します。
93
+ params で値を束縛して業務データを操作。順に行配列、先頭行/null、書込結果、void、ページを返す。batch は文の配列。
128
94
 
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` の設定を前提とします:
95
+ | 入口 | 入力引数 | 成功結果 | 呼出し例 |
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
- ```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
- ```
103
+ #### reldb.pagination
136
104
 
137
- MySQL の空 `url`(空の環境変数を含む)は host/port/database/user/password を使用します。空でない URL は厳密に検証します。
105
+ ページ処理。normalize はページ指定と上限から正規化値、build は items・total・ページ指定からページオブジェクトを直接返す。
138
106
 
139
- #### MySQL TLS
107
+ | 入口 | 入力引数 | 成功結果 | 呼出し例 |
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
- `ssl` を省略するか `false` にすると TLS を使用しません。有効化には厳密なオブジェクトを指定し、空オブジェクトの既定値は `{ rejectUnauthorized: true, verifyIdentity: true }` です。`ca` は信頼する CA の PEM 内容で、省略時は Node の信頼ストアを使います。任意のクライアント `cert` / `key` も PEM 内容で、必ず対で指定します。未知の TLS フィールド、`true`、PostgreSQL の文字列モードは拒否し、任意の設定やコールバックは透過しません。
112
+ #### reldb
142
113
 
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` はサーバー認証を放棄し、暗号化だけでは相手の身元を確認できません。どちらかの検証を無効にすると固定のリスク警告を記録し、安全な構成とは扱いません。
114
+ 永続化の準備。テーブル名から列名、モジュール名・手順・dryRun から移行計画を取得。適用前にバックアップし競合書込を停止。
144
115
 
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 の証拠にはしません。
116
+ | 入口 | 入力引数 | 成功結果 | 呼出し例 |
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
- #### SQLite の実行方式(非同期 worker)
121
+ #### reldb.tx
155
122
 
156
- better-sqlite3 は同期ドライバーなので、クエリは Node のメインイベントループをブロックします。WAL は DB ロックの並行性を改善しますが、同期呼び出しによるブロックは解消しません。専用 worker スレッドで SQLite を実行して処理を移せます(単一接続、任意設定、既定は `sync`):
123
+ 原子的な書込。begin は query/get/execute/batch/queryPage/commit/rollback を持つハンドル、wrap はコールバックの結果を返す。失敗 HaiResult は明示的にロールバックする。
157
124
 
158
- ```ts
159
- await reldb.init({
160
- type: 'sqlite',
161
- database: './data.db',
162
- sqlite: {
163
- executor: 'worker', // 'sync'(默认)| 'worker'
164
- walMode: true, // 默认 true
165
- workerConnectTimeoutMs: 10000, // worker 打开连接超时,默认 10000
166
- },
167
- })
168
- ```
125
+ | 入口 | 入力引数 | 成功結果 | 呼出し例 |
126
+ | ---------------- | -------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
127
+ | `reldb.tx.begin` | 引数なし | `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
- worker モードではクエリ、書き込み、トランザクションを worker スレッドで順番に実行します。公開 API と戻り値は同期モードと同じです。
130
+ #### reldb.crud
171
131
 
172
- #### 操作ログ
132
+ 業務エンティティのテーブル・フィールド対応から CRUD とページ処理のリポジトリを直接作成。DB 接続は露出しない。
173
133
 
174
- DB 設定の `operationLog` で SQL 操作ログを有効にできます。ログは reldb provider-base の DML 層で出力され、`reldb-main.ts` や各 raw provider で重複してラップしません。
134
+ | 入口 | 入力引数 | 成功結果 | 呼出し例 |
135
+ | ------------------ | -------- | ---------------------------- | ----------------------------------------- |
136
+ | `reldb.crud.table` | config | `ReldbCrudRepository<TItem>` | `reldb.crud.table(orderRepositoryConfig)` |
175
137
 
176
- ```yaml
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 # 安全な件数サマリーの最大長。0は省略。SQL本文と値は記録しない
184
- level: debug # info | debug | trace,默认 debug
185
- ```
138
+ #### reldb.json
186
139
 
187
- `core.config.load('db', ...)` で読み込む場合、`HAI_DB_TYPE`、
188
- `HAI_DB_DATABASE`、`HAI_DB_OPERATIONLOG_LEVEL` などの規約に沿う変数が YAML より優先されます。
140
+ 方言共通の JSON 式。信頼する列名、JSON パス、値/patch から SQL 式と束縛値を直接作成し、ここでは実行しない。
189
141
 
190
- ```ts
191
- await reldb.init({
192
- type: 'sqlite',
193
- database: './data.db',
194
- operationLog: {
195
- read: true,
196
- write: true,
197
- maxLength: 500,
198
- level: 'debug',
199
- },
200
- })
201
- ```
142
+ | 入口 | 入力引数 | 成功結果 | 呼出し例 |
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
- 操作ログには固定の操作名、SQL 長、パラメーター数、バッチ件数を記録します。失敗時は、エラーコード、traceId、許可リストに含まれるマスキング済みのドライバーメッセージとスタックも保持します。SQL 全文、バインド値、接続設定をそのまま記録することはありません。
150
+ - 共通 API で SQLite・PostgreSQL・MySQL に接続。
151
+ - パラメーター付き SQL、テーブル・索引管理、ページ取得。
152
+ - トランザクション、CRUD、Repository による業務の読み書き。
204
153
 
205
154
  ### トランザクション
206
155
 
156
+ トランザクションの終了処理を所有できるのは一つの操作だけです。commit または rollback の開始後、新規クエリ、重複 commit、並行 rollback は直ちに TRANSACTION_FAILED を返し、接続は一度だけ返却します。復旧目的で並行終了を試みず、最初の終了結果を待ってください。
157
+
207
158
  3 種類の使用方法:
208
159
 
209
160
  #### wrap(自動コミット/ロールバック)
@@ -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
- { sql: 'INSERT INTO users (name) VALUES (?)', params: ['Alice'] },
276
- { sql: 'INSERT INTO users (name) VALUES (?)', params: ['Bob'] },
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({ where: 'name LIKE ?', params: ['%张%'] })
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({ where: 'email = ?', params: ['test@example.com'] })
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
- { fieldName: 'id', columnName: 'id', def: { type: 'INTEGER', primaryKey: true, autoIncrement: true }, select: true, create: false, update: false },
430
- { fieldName: 'name', columnName: 'name', def: { type: 'TEXT', notNull: true }, select: true, create: true, update: true },
431
- { fieldName: 'email', columnName: 'email', def: { type: 'TEXT', notNull: true }, select: true, create: true, update: true },
432
- { fieldName: 'createdAt', columnName: 'created_at', def: { type: 'TIMESTAMP', notNull: true }, select: true, create: true, update: false },
433
- { fieldName: 'updatedAt', columnName: 'updated_at', def: { type: 'TIMESTAMP', notNull: true }, select: true, create: true, update: false },
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({ where: 'email = ?', params: [email], limit: 1 })
441
+ return this.findAll({
442
+ where: 'email = ?',
443
+ params: [email],
444
+ limit: 1
445
+ })
444
446
  }
445
447
 
446
448
  /** 自定义方法中使用 this.sql(tx) 自动路由到事务 */
@@ -554,7 +556,7 @@ await reldb.sql.execute(
554
556
  )
555
557
  ```
556
558
 
557
- `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)。
559
+ `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).
558
560
 
559
561
  #### 完全な使用例
560
562
 
@@ -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
- [JSON.stringify({ status: 'draft', tags: ['new'], version: 1 })],
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
+ ### 本番の共有状態
603
+
604
+ 本番は PostgreSQL/MySQL が必須です。ファイルを含む SQLite は開発・明示的テスト用であり、ホスト間の共有状態を保証しません。 NODE_ENV または HAI_CORE_ENV が production の場合、初期化成功前に検証します。明示的な HAI_E2E=1/HAI_E2E_MOCK=1 テストのみ除外します。 [状態・並行制御・復旧範囲(中国語)](../../.agents/instructions/distributed-state.md).
605
+
606
+ ### モジュールのスキーマ移行
607
+
608
+ `reldb.schemaColumns(table, sql?)` は実際の列を読み取ります。`reldb.migrateSchema(module, steps, { dryRun: true })` は台帳を作成しません。各モジュールが手順を所有し、`hai_schema_migrations` に版を記録します。SQLite/PostgreSQL はトランザクション、MySQL の暗黙 DDL コミットには再入可能な手順が必要です。失敗時は版を進めず、実構造を再確認して再試行します。新しい台帳へのダウングレードは拒否します。旧書込を停止し DB/セッションキャッシュをバックアップしてください。復旧は互換 DB とバイナリを一緒に戻します。[更新ガイド](../../docs/usage/README.ja.md)を参照。
609
+
610
+ ## 設定
611
+
612
+ ライフサイクル。init にバックエンド/業務設定を渡し成功後に使用します。借用依存先の解放前に close の完了を待ち、正確な戻り値は例と公開署名に従います。
613
+
614
+ `@h-ai/reldb/schemas` は同じ設定 Schema と型を Zod のみで公開し、DB インスタンスやドライバーを読み込みません。CLI やブラウザーで設定検証に利用できます。MySQL TLS は `false` または厳密なオブジェクトのみで、`{}` は証明書チェーンと DNS ID を検証します。PostgreSQL の真偽値・文字列モード・オブジェクト契約は維持します。
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
+ 接続文字列(`url`)または個別フィールド(`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
+ ### 接続プールと MySQL URL
648
+
649
+ 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 と異なります。
650
+
651
+ | フィールド | PostgreSQL(`pool` 指定時の既定値) | MySQL |
652
+ | ---------------- | ----------------------------------- | -------------------------------------- |
653
+ | `max` | 10 | 10、借用中と接続確立中の合計上限 |
654
+ | `min` | 1、既存接続の保持下限。事前作成なし | 未対応、拒否 |
655
+ | `idleTimeout` | 30000 ミリ秒、0 は無効化 | 未対応、拒否 |
656
+ | `acquireTimeout` | 10000 ミリ秒、0 は無効化 | 10000 ミリ秒、1–2147483647 の整数 |
657
+ | `queueLimit` | 未対応 | 1000、待機リクエスト数の上限、正の整数 |
658
+
659
+ PostgreSQL で `pool` を省略した場合は pg の既定値を使用します。保持と回収の仕様は [pg.Pool](https://node-postgres.com/apis/pool) を参照してください。MySQL の SQL、DDL、CRUD、バッチ、トランザクションは共通の有限 FIFO 待機キューを使います。待機と接続確立に一つの期限を適用し、超過時は `TIMEOUT`、満杯時は `POOL_EXHAUSTED` を返します。期限切れ項目は削除し、遅れて取得した接続は SQL を実行せず解放します。確立処理が完了するまで容量枠を保持します。受領時と処理開始前にも期限を確認しますが、開始済み SQL/トランザクションの実行時間は制限しません。
660
+
661
+ MySQL の `close()` は新規・待機リクエストを直ちに拒否し、借用接続、トランザクション、接続確立の完了後にプールを閉じます。`connectTimeout` にも同じ値を使うため、その期限や既存処理を待つ場合があります。アプリはトランザクションを終了してください。環境変数は `HAI_DB_POOL_*` です。`MIN` / `IDLETIMEOUT` は PostgreSQL 専用で、MySQL の `QUEUELIMIT` は待機総数を制限します。
662
+
663
+ MySQL は `mysql://` の `url` だけでも接続でき、URL がなければ `database` が必須です。URL の host/port/database/user/password を優先し、競合する個別フィールドは接続に使用しません。`pool`、`mysql.charset`、`ssl`、`operationLog` は独立して適用します。query/fragment は拒否し、URL 内の隠れた設定で TLS やプールを変更できません。接続ログの URL は機密情報を伏せます。以下は `HAI_DB_URL` の設定を前提とします:
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
+ MySQL の空 `url`(空の環境変数を含む)は host/port/database/user/password を使用します。空でない URL は厳密に検証します。
676
+
677
+ #### MySQL TLS
678
+
679
+ `ssl` を省略するか `false` にすると TLS を使用しません。有効化には厳密なオブジェクトを指定し、空オブジェクトの既定値は `{ rejectUnauthorized: true, verifyIdentity: true }` です。`ca` は信頼する CA の PEM 内容で、省略時は Node の信頼ストアを使います。任意のクライアント `cert` / `key` も PEM 内容で、必ず対で指定します。未知の TLS フィールド、`true`、PostgreSQL の文字列モードは拒否し、任意の設定やコールバックは透過しません。
680
+
681
+ [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` はサーバー認証を放棄し、暗号化だけでは相手の身元を確認できません。どちらかの検証を無効にすると固定のリスク警告を記録し、安全な構成とは扱いません。
682
+
683
+ ```ts
684
+ // trustedCaPem は配置時の信頼設定から供給し、任意の接続先証明書をそのまま信頼しません。
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 テストには OpenSSL(Windows は Git 同梱版も可)とローカルのコンテナー転送が必要です。各実行で CA/秘密鍵を生成・削除し、信頼済み DNS、誤った CA/主機名の拒否、`REQUIRE X509` 認証、実際の `Ssl_cipher` を検証します。検証無効の接続を認証済み TLS の証拠にはしません。
696
+
697
+ #### SQLite の実行方式(非同期 worker)
698
+
699
+ better-sqlite3 は同期ドライバーなので、クエリは Node のメインイベントループをブロックします。WAL は DB ロックの並行性を改善しますが、同期呼び出しによるブロックは解消しません。専用 worker スレッドで SQLite を実行して処理を移せます(単一接続、任意設定、既定は `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
+ worker モードではクエリ、書き込み、トランザクションを worker スレッドで順番に実行します。公開 API と戻り値は同期モードと同じです。
714
+
715
+ #### 操作ログ
716
+
717
+ DB 設定の `operationLog` で SQL 操作ログを有効にできます。ログは reldb provider-base の DML 層で出力され、`reldb-main.ts` や各 raw provider で重複してラップしません。
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 # 安全な件数サマリーの最大長。0は省略。SQL本文と値は記録しない
727
+ level: debug # info | debug | trace,默认 debug
728
+ ```
729
+
730
+ `core.config.load('db', ...)` で読み込む場合、`HAI_DB_TYPE`、
731
+ `HAI_DB_DATABASE`、`HAI_DB_OPERATIONLOG_LEVEL` などの規約に沿う変数が YAML より優先されます。
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
+ 操作ログには固定の操作名、SQL 長、パラメーター数、バッチ件数を記録します。失敗時は、エラーコード、traceId、許可リストに含まれるマスキング済みのドライバーメッセージとスタックも保持します。SQL 全文、バインド値、接続設定をそのまま記録することはありません。
747
+
748
+ 設定はホストが読込・検証してから渡します。秘密はサーバーの非公開環境に保存し、利用場面に応じてバックエンド・容量・期限・ログを選びます。変更後は再初期化の結果を確認してください。
749
+
750
+ [項目別の用途・適用場面・例](../../docs/usage/configuration/data.md).
751
+
752
+ ## エラー処理
753
+
754
+ 領域操作は result.success を確認し、失敗時はモジュールのエラー定義と安全な説明を使います。コード・関連 ID・秘匿した原因をサーバーログに残します。同期ツール・ファクトリー・ストリーム反復・ライフサイクルは各署名に従い、Promise 完了だけで成功と判断しません。回復条件は各機能を参照してください。
595
755
 
596
756
  すべての操作は `HaiResult<T>` を返し、`result.success` で成功・失敗を判定します。
597
757
 
@@ -630,15 +790,7 @@ if (!result.success) {
630
790
 
631
791
  `BaseReldbCrudRepository` の派生クラスで独自 SQL を実行する前に `await this.ensureReady()` を確認します。構築時に建表を開始するだけなので、サービス init は候補の準備完了後に公開します。PostgreSQL の自動コミットによる単一の `CREATE TABLE/INDEX IF NOT EXISTS` は、`23505/pg_class_relname_nsp_index`(建表は `pg_type_typname_nsp_index` も対象)の競合、または建表時にサーバー routine が `heap_create_with_catalog` / `TypeCreate` である `42710` の行型競合に限り、DDL/SQL の両入口で一度だけ再実行します。再試行も失敗した場合はエラーを返します。トランザクション、業務の一意制約、その他の失敗は再試行しません。初期接続確認の一時 DNS エラー `EAI_AGAIN` は 250/500 ms 間隔で最大三回試行し、業務 SQL は再送しません。
632
792
 
633
- ## 本番の共有状態
634
-
635
- 本番は PostgreSQL/MySQL が必須です。ファイルを含む SQLite は開発・明示的テスト用であり、ホスト間の共有状態を保証しません。 NODE_ENV または HAI_CORE_ENV が production の場合、初期化成功前に検証します。明示的な HAI_E2E=1/HAI_E2E_MOCK=1 テストのみ除外します。 [状態・並行制御・復旧範囲(中国語)](../../.agents/instructions/distributed-state.md)。
636
-
637
- ## モジュールのスキーマ移行
638
-
639
- `schemaColumns(reldb, table, sql?)` は実際の列を読み取ります。`migrateSchema(reldb, module, steps, { dryRun: true })` は台帳を作成しません。各モジュールが手順を所有し、`hai_schema_migrations` に版を記録します。SQLite/PostgreSQL はトランザクション、MySQL の暗黙 DDL コミットには再入可能な手順が必要です。失敗時は版を進めず、実構造を再確認して再試行します。新しい台帳へのダウングレードは拒否します。旧書込を停止し DB/セッションキャッシュをバックアップしてください。復旧は互換 DB とバイナリを一緒に戻します。[更新ガイド](../../docs/usage/README.ja.md)を参照。
640
-
641
- ## エラー表示とログの関連付け
793
+ ### エラー表示とログの関連付け
642
794
 
643
795
  一意制約違反は `HaiReldbError.DUPLICATE_ENTRY`(409)に統一し、タイムアウトや待機列満杯など既存のコードは保持します。翻訳済みメッセージやドライバー文字列ではなくコードで分岐してください。ログには code/schema/routine/constraint とスタック行番号を残し、数値のバインド値でこれらの診断識別子を隠しません。
644
796