@h-ai/reldb 0.1.0-alpha.70 → 0.1.0-alpha.72

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 CHANGED
@@ -594,6 +594,18 @@ Custom SQL methods in `BaseReldbCrudRepository` subclasses must check `await thi
594
594
 
595
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)。
596
596
 
597
+ ## Module schema migrations
598
+
599
+ `schemaColumns(reldb, table, sql?)` inspects real columns. `migrateSchema(reldb, module, steps, { dryRun: true })` creates no ledger. Modules own their steps. Execution journals versions in `hai_schema_migrations`; SQLite/PostgreSQL are transactional, while MySQL implicit DDL commits require reentrant steps. Failures do not advance versions; retries inspect real state. Older code rejects newer ledgers. Stop old writers and back up DB/session cache first. Rollback restores compatible database and binaries together. See the [upgrade guide](../../docs/usage/README.en.md).
600
+
601
+ ## Error messages and correlated logs
602
+
603
+ 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.
604
+
605
+ HTTP errors expose localized safe messages followed by `(traceId: …)`, with the same ID in `error.traceId` and `X-Trace-Id`. The framework reuses the request ID to correlate browser and server logs. Sanitized exceptions, stacks and operation context remain in logs; internal cause/ext are not exposed. Field validation returns localized field names and messages only.
606
+
607
+ 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.
608
+
597
609
  ## Development and testing
598
610
 
599
611
  ```bash
package/README.ja.md CHANGED
@@ -594,6 +594,18 @@ if (!result.success) {
594
594
 
595
595
  本番は PostgreSQL/MySQL が必須です。ファイルを含む SQLite は開発・明示的テスト用であり、ホスト間の共有状態を保証しません。 NODE_ENV または HAI_CORE_ENV が production の場合、初期化成功前に検証します。明示的な HAI_E2E=1/HAI_E2E_MOCK=1 テストのみ除外します。 [状態・並行制御・復旧範囲(中国語)](../../.agents/instructions/distributed-state.md)。
596
596
 
597
+ ## モジュールのスキーマ移行
598
+
599
+ `schemaColumns(reldb, table, sql?)` は実際の列を読み取ります。`migrateSchema(reldb, module, steps, { dryRun: true })` は台帳を作成しません。各モジュールが手順を所有し、`hai_schema_migrations` に版を記録します。SQLite/PostgreSQL はトランザクション、MySQL の暗黙 DDL コミットには再入可能な手順が必要です。失敗時は版を進めず、実構造を再確認して再試行します。新しい台帳へのダウングレードは拒否します。旧書込を停止し DB/セッションキャッシュをバックアップしてください。復旧は互換 DB とバイナリを一緒に戻します。[更新ガイド](../../docs/usage/README.ja.md)を参照。
600
+
601
+ ## エラー表示とログの関連付け
602
+
603
+ 一意制約違反は `HaiReldbError.DUPLICATE_ENTRY`(409)に統一し、タイムアウトや待機列満杯など既存のコードは保持します。翻訳済みメッセージやドライバー文字列ではなくコードで分岐してください。ログには code/schema/routine/constraint とスタック行番号を残し、数値のバインド値でこれらの診断識別子を隠しません。
604
+
605
+ HTTP エラーはローカライズ済みの安全な表示に `(traceId: …)` を付け、同じ ID を error.traceId と X-Trace-Id に返します。requestId を traceId として共有し、ブラウザーとサーバーのログを関連付けます。脱機密化した例外・スタック・操作情報はログに残し、内部 cause/ext は公開しません。項目検証は翻訳済みの項目名とメッセージだけを返します。
606
+
607
+ HTTP/UI 境界には core.error.toPublic(error, traceId?)、再翻訳後の表示には core.error.formatMessage(error) を使い、ID を重複追加しません。ログには完全な Error を渡してください。DB ログにはドライバーコード、脱機密化したメッセージ・スタック、表・列を残し、SQL 本文、バインド値と接続認証情報を記録しません。
608
+
597
609
  ## 開発とテスト
598
610
 
599
611
  ```bash
package/README.md CHANGED
@@ -594,6 +594,18 @@ if (!result.success) {
594
594
 
595
595
  生产仅使用 PostgreSQL/MySQL;SQLite(含磁盘文件)只用于开发/明确测试,不能作为跨主机共享状态。 NODE_ENV 或 HAI_CORE_ENV 为 production 时在初始化发布成功前校验;仅 HAI_E2E=1/HAI_E2E_MOCK=1 明确测试可豁免。 [状态、并发和恢复边界](../../.agents/instructions/distributed-state.md)。
596
596
 
597
+ ## 模块架构迁移
598
+
599
+ `schemaColumns(reldb, table, sql?)` 只读检查真实结构;`migrateSchema(reldb, module, steps, { dryRun: true })` 不创建账本。版本步骤由所属模块维护,业务不修改框架内部表。执行使用 `hai_schema_migrations`,SQLite/PostgreSQL 整体事务,MySQL DDL 隐式提交因此步骤必须可重入;失败不推进版本,重新检查真实结构后续做,拒绝向低版本降级。升级前停止所有旧写入并备份 DB/会话缓存;回退需恢复兼容数据库与旧代码,不能只换二进制。详见 [升级指南](../../docs/usage/README.md)。
600
+
601
+ ## 错误提示与关联日志
602
+
603
+ 唯一约束冲突统一返回 `HaiReldbError.DUPLICATE_ENTRY`(409);超时、队列已满等已有错误码保持不变。业务按错误码分支,不解析本地化消息或驱动错误文本。日志保留驱动 code/schema/routine/constraint 和堆栈行号;数字绑定值不会误遮蔽这些诊断标识。
604
+
605
+ HTTP 错误只向用户返回本地化的安全提示,末尾显示 `(traceId: …)`;响应同时包含 `error.traceId` 和 `X-Trace-Id`。框架复用本次 requestId 作为 traceId,前后端日志自动关联。底层异常、堆栈和操作上下文经凭据脱敏后写入日志,不通过 cause/ext 向用户公开。字段校验只返回本地化的字段名和错误。
606
+
607
+ `core.error.toPublic(error, traceId?)` 用于接口/UI 边界;`core.error.formatMessage(error)` 用于重新翻译后的提示,重复调用不会重复追加标识。日志传入完整 Error 对象,不能只记录通用标题或提前 String(error) 丢失上下文。数据库失败记录驱动错误码、消息、堆栈及表/列;SQL 原文、绑定值和连接凭据不进入日志。
608
+
597
609
  ## 开发测试
598
610
 
599
611
  ```bash
package/dist/index.d.ts CHANGED
@@ -1458,6 +1458,59 @@ declare abstract class BaseReldbCrudRepository<TItem> implements ReldbCrudReposi
1458
1458
  */
1459
1459
  declare const reldb: ReldbFunctions;
1460
1460
 
1461
+ /** 模块维护自己的迁移;这里只提供跨数据库列检查、版本账本和执行边界。 */
1462
+
1463
+ /**
1464
+ * 一个可重入的模块升级步骤;只接收代码中固定的 SQL 与表名。
1465
+ * @example
1466
+ * ```ts
1467
+ * import type { SchemaMigration } from '@h-ai/reldb'
1468
+ * const steps: SchemaMigration[] = []
1469
+ * ```
1470
+ */
1471
+ interface SchemaMigration {
1472
+ version: number;
1473
+ name: string;
1474
+ needed: (sql: DmlOperations) => Promise<HaiResult<boolean>>;
1475
+ apply: (sql: DmlOperations) => Promise<HaiResult<unknown>>;
1476
+ }
1477
+ /**
1478
+ * 只读预检结果;生产升级需先停旧写入并备份数据库与共享缓存。
1479
+ * @example
1480
+ * ```ts
1481
+ * import type { SchemaMigrationPlan } from '@h-ai/reldb'
1482
+ * function changes(plan: SchemaMigrationPlan) { return plan.changes }
1483
+ * ```
1484
+ */
1485
+ interface SchemaMigrationPlan {
1486
+ module: string;
1487
+ currentVersion: number;
1488
+ targetVersion: number;
1489
+ changes: string[];
1490
+ requiresExclusiveAccess: boolean;
1491
+ }
1492
+ /**
1493
+ * 查询真实列名;不创建表,不把查询错误当成新安装。
1494
+ * @example
1495
+ * ```ts
1496
+ * import { reldb, schemaColumns } from '@h-ai/reldb'
1497
+ * const columns = await schemaColumns(reldb, 'hai_iam_users')
1498
+ * ```
1499
+ */
1500
+ declare function schemaColumns(db: ReldbFunctions, table: string, sql?: DmlOperations): Promise<HaiResult<string[]>>;
1501
+ /**
1502
+ * 预检或执行模块维护的版本迁移。SQLite/PostgreSQL 原子提交;MySQL DDL 隐式提交,步骤须可重入。
1503
+ * 失败不记录目标版本;重启重新检查真实结构。新版本账本拒绝旧代码向下迁移。
1504
+ * @example
1505
+ * ```ts
1506
+ * import { reldb, migrateSchema } from '@h-ai/reldb'
1507
+ * const plan = await migrateSchema(reldb, 'example', [], { dryRun: true })
1508
+ * ```
1509
+ */
1510
+ declare function migrateSchema(db: ReldbFunctions, module: string, steps: readonly SchemaMigration[], options?: {
1511
+ dryRun?: boolean;
1512
+ }): Promise<HaiResult<SchemaMigrationPlan>>;
1513
+
1461
1514
  /**
1462
1515
  * @h-ai/reldb — 安全工具
1463
1516
  *
@@ -1535,4 +1588,4 @@ declare function quoteIdentifier(name: string): string;
1535
1588
  */
1536
1589
  declare function escapeSqlString(value: string): string;
1537
1590
 
1538
- export { BaseReldbCrudRepository, type BaseReldbCrudRepositoryConfig, type ColumnType, type CrudConfig, type CrudManager, type CrudPageOptions, type CrudQueryOptions, DbType, type DdlOperations, type DmlOperations, type DmlWithTxOperations, type ExecuteResult, HaiReldbError, type JsonSqlExpr, type NormalizedPagination, type PaginationOverrides, type PaginationQueryOptions, type QueryRow, type ReldbColumnDef, ReldbConfig, ReldbConfigInput, type ReldbCrudCountOptions, type ReldbCrudFieldDefinition, type ReldbCrudRepository, type ReldbFunctions, type ReldbIndexDef, type ReldbJsonOps, type ReldbTableDef, type TxManager, type TxWrapCallback, createJsonOps, escapeSqlString, parseJsonPath, quoteIdentifier, reldb, validateIdentifier, validateIdentifiers };
1591
+ export { BaseReldbCrudRepository, type BaseReldbCrudRepositoryConfig, type ColumnType, type CrudConfig, type CrudManager, type CrudPageOptions, type CrudQueryOptions, DbType, type DdlOperations, type DmlOperations, type DmlWithTxOperations, type ExecuteResult, HaiReldbError, type JsonSqlExpr, type NormalizedPagination, type PaginationOverrides, type PaginationQueryOptions, type QueryRow, type ReldbColumnDef, ReldbConfig, ReldbConfigInput, type ReldbCrudCountOptions, type ReldbCrudFieldDefinition, type ReldbCrudRepository, type ReldbFunctions, type ReldbIndexDef, type ReldbJsonOps, type ReldbTableDef, type SchemaMigration, type SchemaMigrationPlan, type TxManager, type TxWrapCallback, createJsonOps, escapeSqlString, migrateSchema, parseJsonPath, quoteIdentifier, reldb, schemaColumns, validateIdentifier, validateIdentifiers };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ReldbConfigSchema, MysqlPoolConfigSchema } from './chunk-SDS2ZT3V.js';
2
2
  export { MysqlConfigSchema, MysqlOptionsSchema, MysqlPoolConfigSchema, MysqlSslConfigSchema, PoolConfigSchema, PostgresqlConfigSchema, ReldbConfigSchema, ReldbOperationLogConfigSchema, ReldbTypeSchema, SqliteConfigSchema, SqliteOptionsSchema, SslConfigSchema } from './chunk-SDS2ZT3V.js';
3
- import { core, ok, err } from '@h-ai/core';
3
+ import { core, ok, err, HaiCommonError } from '@h-ai/core';
4
4
  import process from 'process';
5
5
  import { createRequire } from 'module';
6
6
  import { isIP } from 'net';
@@ -13,22 +13,23 @@ import { Worker } from 'worker_threads';
13
13
  var en_US_default = {
14
14
  $schema: "https://inlang.com/schema/inlang-message-format",
15
15
  reldb_notInitialized: "Database not initialized, please call db.init() first",
16
- reldb_initFailed: "Failed to initialize database: {error}",
17
- reldb_closeFailed: "Failed to close database: {error}",
18
- reldb_ddlFailed: "DDL operation failed: {error}",
19
- reldb_queryFailed: "Query failed: {error}",
20
- reldb_batchFailed: "Batch execute failed: {error}",
21
- reldb_postgresTxFailed: "Transaction failed: {error}",
16
+ reldb_initFailed: "Failed to initialize database",
17
+ reldb_closeFailed: "Failed to close database",
18
+ reldb_ddlFailed: "DDL operation failed",
19
+ reldb_queryFailed: "Query failed",
20
+ reldb_duplicateEntry: "Record already exists",
21
+ reldb_batchFailed: "Batch execute failed",
22
+ reldb_postgresTxFailed: "Transaction failed",
22
23
  reldb_postgresOnlyPostgresql: "PostgreSQL Provider only supports postgresql type",
23
- reldb_postgresConnectionFailed: "Failed to connect to PostgreSQL: {error}",
24
+ reldb_postgresConnectionFailed: "Failed to connect to PostgreSQL",
24
25
  reldb_sqliteOnlySqlite: "SQLite Provider only supports sqlite type",
25
26
  reldb_sqliteNeedPath: "SQLite requires database path (database field)",
26
- reldb_sqliteConnectionFailed: "Failed to connect to SQLite: {error}",
27
- reldb_sqliteTxFailed: "Transaction failed: {error}",
28
- reldb_sqliteBatchFailed: "Batch execute failed: {error}",
27
+ reldb_sqliteConnectionFailed: "Failed to connect to SQLite",
28
+ reldb_sqliteTxFailed: "Transaction failed",
29
+ reldb_sqliteBatchFailed: "Batch execute failed",
29
30
  reldb_mysqlOnlyMysql: "MySQL Provider only supports mysql type",
30
- reldb_mysqlConnectionFailed: "Failed to connect to MySQL: {error}",
31
- reldb_mysqlTxFailed: "Transaction failed: {error}",
31
+ reldb_mysqlConnectionFailed: "Failed to connect to MySQL",
32
+ reldb_mysqlTxFailed: "Transaction failed",
32
33
  reldb_mysqlPoolQueueFull: "MySQL connection acquisition queue is full",
33
34
  reldb_mysqlPoolAcquireTimeout: "MySQL connection acquisition deadline exceeded",
34
35
  reldb_mysqlPoolReleaseFailed: "MySQL connection release failed; close the database before retrying",
@@ -37,8 +38,8 @@ var en_US_default = {
37
38
  reldb_crudNoValidColumns: "CRUD payload has no valid columns",
38
39
  reldb_crudRecordNotFound: "Record not found: {id}",
39
40
  reldb_invalidIdentifier: "Invalid SQL identifier: {name} (only letters, digits and underscores allowed)",
40
- reldb_configError: "Database config validation failed: {error}",
41
- reldb_txFailed: "Transaction failed: {error}",
41
+ reldb_configError: "Database config validation failed",
42
+ reldb_txFailed: "Transaction failed",
42
43
  reldb_sharedBackendRequired: "Production mode requires a shared backend; local files and memory backends are for development or explicit end-to-end tests.",
43
44
  reldb_jsonPatchObjectRequired: "JSON merge patch must be an object"
44
45
  };
@@ -47,22 +48,23 @@ var en_US_default = {
47
48
  var ja_JP_default = {
48
49
  $schema: "https://inlang.com/schema/inlang-message-format",
49
50
  reldb_notInitialized: "\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u304C\u521D\u671F\u5316\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002\u5148\u306Bdb.init()\u3092\u547C\u3073\u51FA\u3057\u3066\u304F\u3060\u3055\u3044",
50
- reldb_initFailed: "\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u306E\u521D\u671F\u5316\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
51
- reldb_closeFailed: "\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u306E\u7D42\u4E86\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
52
- reldb_ddlFailed: "DDL\u64CD\u4F5C\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
53
- reldb_queryFailed: "\u30AF\u30A8\u30EA\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
54
- reldb_batchFailed: "\u30D0\u30C3\u30C1\u5B9F\u884C\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
55
- reldb_postgresTxFailed: "\u30C8\u30E9\u30F3\u30B6\u30AF\u30B7\u30E7\u30F3\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
51
+ reldb_initFailed: "\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u306E\u521D\u671F\u5316\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
52
+ reldb_closeFailed: "\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u306E\u7D42\u4E86\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
53
+ reldb_ddlFailed: "DDL\u64CD\u4F5C\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
54
+ reldb_queryFailed: "\u30AF\u30A8\u30EA\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
55
+ reldb_duplicateEntry: "\u30EC\u30B3\u30FC\u30C9\u306F\u65E2\u306B\u5B58\u5728\u3057\u307E\u3059",
56
+ reldb_batchFailed: "\u30D0\u30C3\u30C1\u5B9F\u884C\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
57
+ reldb_postgresTxFailed: "\u30C8\u30E9\u30F3\u30B6\u30AF\u30B7\u30E7\u30F3\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
56
58
  reldb_postgresOnlyPostgresql: "PostgreSQL\u30D7\u30ED\u30D0\u30A4\u30C0\u30FC\u306Fpostgresql\u578B\u306E\u307F\u306B\u5BFE\u5FDC\u3057\u3066\u3044\u307E\u3059",
57
- reldb_postgresConnectionFailed: "PostgreSQL\u3078\u306E\u63A5\u7D9A\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
59
+ reldb_postgresConnectionFailed: "PostgreSQL\u3078\u306E\u63A5\u7D9A\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
58
60
  reldb_sqliteOnlySqlite: "SQLite\u30D7\u30ED\u30D0\u30A4\u30C0\u30FC\u306Fsqlite\u578B\u306E\u307F\u306B\u5BFE\u5FDC\u3057\u3066\u3044\u307E\u3059",
59
61
  reldb_sqliteNeedPath: "SQLite\u306B\u306F\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u306E\u30D1\u30B9\uFF08database\u30D5\u30A3\u30FC\u30EB\u30C9\uFF09\u304C\u5FC5\u8981\u3067\u3059",
60
- reldb_sqliteConnectionFailed: "SQLite\u3078\u306E\u63A5\u7D9A\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
61
- reldb_sqliteTxFailed: "\u30C8\u30E9\u30F3\u30B6\u30AF\u30B7\u30E7\u30F3\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
62
- reldb_sqliteBatchFailed: "\u30D0\u30C3\u30C1\u5B9F\u884C\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
62
+ reldb_sqliteConnectionFailed: "SQLite\u3078\u306E\u63A5\u7D9A\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
63
+ reldb_sqliteTxFailed: "\u30C8\u30E9\u30F3\u30B6\u30AF\u30B7\u30E7\u30F3\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
64
+ reldb_sqliteBatchFailed: "\u30D0\u30C3\u30C1\u5B9F\u884C\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
63
65
  reldb_mysqlOnlyMysql: "MySQL\u30D7\u30ED\u30D0\u30A4\u30C0\u30FC\u306Fmysql\u578B\u306E\u307F\u306B\u5BFE\u5FDC\u3057\u3066\u3044\u307E\u3059",
64
- reldb_mysqlConnectionFailed: "MySQL\u3078\u306E\u63A5\u7D9A\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
65
- reldb_mysqlTxFailed: "\u30C8\u30E9\u30F3\u30B6\u30AF\u30B7\u30E7\u30F3\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
66
+ reldb_mysqlConnectionFailed: "MySQL\u3078\u306E\u63A5\u7D9A\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
67
+ reldb_mysqlTxFailed: "\u30C8\u30E9\u30F3\u30B6\u30AF\u30B7\u30E7\u30F3\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
66
68
  reldb_mysqlPoolQueueFull: "MySQL \u63A5\u7D9A\u53D6\u5F97\u30AD\u30E5\u30FC\u304C\u6E80\u676F\u3067\u3059",
67
69
  reldb_mysqlPoolAcquireTimeout: "MySQL \u63A5\u7D9A\u53D6\u5F97\u306E\u5F85\u6A5F\u671F\u9650\u3092\u8D85\u904E\u3057\u307E\u3057\u305F",
68
70
  reldb_mysqlPoolReleaseFailed: "MySQL \u63A5\u7D9A\u306E\u89E3\u653E\u306B\u5931\u6557\u3057\u307E\u3057\u305F\u3002\u518D\u8A66\u884C\u3059\u308B\u524D\u306B\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u3092\u7D42\u4E86\u3057\u3066\u304F\u3060\u3055\u3044",
@@ -71,8 +73,8 @@ var ja_JP_default = {
71
73
  reldb_crudNoValidColumns: "CRUD\u30DA\u30A4\u30ED\u30FC\u30C9\u306B\u6709\u52B9\u306A\u5217\u304C\u3042\u308A\u307E\u305B\u3093",
72
74
  reldb_crudRecordNotFound: "\u30EC\u30B3\u30FC\u30C9\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: {id}",
73
75
  reldb_invalidIdentifier: "SQL\u8B58\u5225\u5B50\u304C\u7121\u52B9\u3067\u3059: {name}\uFF08\u82F1\u6570\u5B57\u3068\u30A2\u30F3\u30C0\u30FC\u30B9\u30B3\u30A2\u306E\u307F\u4F7F\u7528\u3067\u304D\u307E\u3059\uFF09",
74
- reldb_configError: "\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u8A2D\u5B9A\u306E\u691C\u8A3C\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
75
- reldb_txFailed: "\u30C8\u30E9\u30F3\u30B6\u30AF\u30B7\u30E7\u30F3\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
76
+ reldb_configError: "\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u8A2D\u5B9A\u306E\u691C\u8A3C\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
77
+ reldb_txFailed: "\u30C8\u30E9\u30F3\u30B6\u30AF\u30B7\u30E7\u30F3\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
76
78
  reldb_sharedBackendRequired: "\u672C\u756A\u74B0\u5883\u306B\u306F\u5171\u6709\u30D0\u30C3\u30AF\u30A8\u30F3\u30C9\u304C\u5FC5\u8981\u3067\u3059\u3002\u30ED\u30FC\u30AB\u30EB\u30D5\u30A1\u30A4\u30EB\u3068\u30E1\u30E2\u30EA\u30FC\u30D0\u30C3\u30AF\u30A8\u30F3\u30C9\u306F\u958B\u767A\u74B0\u5883\u307E\u305F\u306F\u660E\u793A\u7684\u306A E2E \u30C6\u30B9\u30C8\u3067\u306E\u307F\u4F7F\u7528\u3067\u304D\u307E\u3059\u3002",
77
79
  reldb_jsonPatchObjectRequired: "JSON \u30DE\u30FC\u30B8\u30D1\u30C3\u30C1\u306F\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044"
78
80
  };
@@ -81,22 +83,23 @@ var ja_JP_default = {
81
83
  var zh_CN_default = {
82
84
  $schema: "https://inlang.com/schema/inlang-message-format",
83
85
  reldb_notInitialized: "\u6570\u636E\u5E93\u672A\u521D\u59CB\u5316\uFF0C\u8BF7\u5148\u8C03\u7528 db.init()",
84
- reldb_initFailed: "\u521D\u59CB\u5316\u6570\u636E\u5E93\u5931\u8D25: {error}",
85
- reldb_closeFailed: "\u5173\u95ED\u6570\u636E\u5E93\u5931\u8D25: {error}",
86
- reldb_ddlFailed: "DDL \u64CD\u4F5C\u5931\u8D25: {error}",
87
- reldb_queryFailed: "\u67E5\u8BE2\u5931\u8D25: {error}",
88
- reldb_batchFailed: "\u6279\u91CF\u6267\u884C\u5931\u8D25: {error}",
89
- reldb_postgresTxFailed: "\u4E8B\u52A1\u6267\u884C\u5931\u8D25: {error}",
86
+ reldb_initFailed: "\u521D\u59CB\u5316\u6570\u636E\u5E93\u5931\u8D25",
87
+ reldb_closeFailed: "\u5173\u95ED\u6570\u636E\u5E93\u5931\u8D25",
88
+ reldb_ddlFailed: "DDL \u64CD\u4F5C\u5931\u8D25",
89
+ reldb_queryFailed: "\u67E5\u8BE2\u5931\u8D25",
90
+ reldb_duplicateEntry: "\u8BB0\u5F55\u5DF2\u5B58\u5728",
91
+ reldb_batchFailed: "\u6279\u91CF\u6267\u884C\u5931\u8D25",
92
+ reldb_postgresTxFailed: "\u4E8B\u52A1\u6267\u884C\u5931\u8D25",
90
93
  reldb_postgresOnlyPostgresql: "PostgreSQL Provider \u4EC5\u652F\u6301 postgresql \u7C7B\u578B",
91
- reldb_postgresConnectionFailed: "\u8FDE\u63A5 PostgreSQL \u5931\u8D25: {error}",
94
+ reldb_postgresConnectionFailed: "\u8FDE\u63A5 PostgreSQL \u5931\u8D25",
92
95
  reldb_sqliteOnlySqlite: "SQLite Provider \u4EC5\u652F\u6301 sqlite \u7C7B\u578B",
93
96
  reldb_sqliteNeedPath: "SQLite \u9700\u8981\u63D0\u4F9B\u6570\u636E\u5E93\u8DEF\u5F84\uFF08database \u5B57\u6BB5\uFF09",
94
- reldb_sqliteConnectionFailed: "\u8FDE\u63A5 SQLite \u5931\u8D25: {error}",
95
- reldb_sqliteTxFailed: "\u4E8B\u52A1\u6267\u884C\u5931\u8D25: {error}",
96
- reldb_sqliteBatchFailed: "\u6279\u91CF\u6267\u884C\u5931\u8D25: {error}",
97
+ reldb_sqliteConnectionFailed: "\u8FDE\u63A5 SQLite \u5931\u8D25",
98
+ reldb_sqliteTxFailed: "\u4E8B\u52A1\u6267\u884C\u5931\u8D25",
99
+ reldb_sqliteBatchFailed: "\u6279\u91CF\u6267\u884C\u5931\u8D25",
97
100
  reldb_mysqlOnlyMysql: "MySQL Provider \u4EC5\u652F\u6301 mysql \u7C7B\u578B",
98
- reldb_mysqlConnectionFailed: "\u8FDE\u63A5 MySQL \u5931\u8D25: {error}",
99
- reldb_mysqlTxFailed: "\u4E8B\u52A1\u6267\u884C\u5931\u8D25: {error}",
101
+ reldb_mysqlConnectionFailed: "\u8FDE\u63A5 MySQL \u5931\u8D25",
102
+ reldb_mysqlTxFailed: "\u4E8B\u52A1\u6267\u884C\u5931\u8D25",
100
103
  reldb_mysqlPoolQueueFull: "MySQL \u8FDE\u63A5\u83B7\u53D6\u7B49\u5F85\u961F\u5217\u5DF2\u6EE1",
101
104
  reldb_mysqlPoolAcquireTimeout: "MySQL \u8FDE\u63A5\u83B7\u53D6\u5DF2\u8D85\u8FC7\u622A\u6B62\u671F\u9650",
102
105
  reldb_mysqlPoolReleaseFailed: "MySQL \u8FDE\u63A5\u5F52\u8FD8\u5931\u8D25\uFF0C\u8BF7\u5173\u95ED\u6570\u636E\u5E93\u540E\u91CD\u8BD5",
@@ -105,8 +108,8 @@ var zh_CN_default = {
105
108
  reldb_crudNoValidColumns: "CRUD \u6570\u636E\u6CA1\u6709\u6709\u6548\u5217",
106
109
  reldb_crudRecordNotFound: "\u8BB0\u5F55\u4E0D\u5B58\u5728: {id}",
107
110
  reldb_invalidIdentifier: "\u975E\u6CD5\u7684 SQL \u6807\u8BC6\u7B26: {name}\uFF08\u4EC5\u5141\u8BB8\u5B57\u6BCD\u3001\u6570\u5B57\u548C\u4E0B\u5212\u7EBF\uFF09",
108
- reldb_configError: "\u6570\u636E\u5E93\u914D\u7F6E\u6821\u9A8C\u5931\u8D25\uFF1A{error}",
109
- reldb_txFailed: "\u4E8B\u52A1\u6267\u884C\u5931\u8D25: {error}",
111
+ reldb_configError: "\u6570\u636E\u5E93\u914D\u7F6E\u6821\u9A8C\u5931\u8D25",
112
+ reldb_txFailed: "\u4E8B\u52A1\u6267\u884C\u5931\u8D25",
110
113
  reldb_sharedBackendRequired: "\u751F\u4EA7\u6A21\u5F0F\u9700\u8981\u8DE8\u8282\u70B9\u5171\u4EAB\u540E\u7AEF\uFF1B\u672C\u673A\u6587\u4EF6\u6216\u5185\u5B58\u540E\u7AEF\u4EC5\u7528\u4E8E\u5F00\u53D1\u548C\u663E\u5F0F\u7AEF\u5230\u7AEF\u6D4B\u8BD5\u3002",
111
114
  reldb_jsonPatchObjectRequired: "JSON \u5408\u5E76\u8865\u4E01\u5FC5\u987B\u662F\u5BF9\u8C61"
112
115
  };
@@ -1592,11 +1595,57 @@ async function wrapOp(ctx, fn, errorDef, errorLabel, errorMeta) {
1592
1595
  if (!ctx.isConnected()) {
1593
1596
  return err(HaiReldbError.NOT_INITIALIZED, reldbM("reldb_notInitialized"));
1594
1597
  }
1598
+ const failed = (error, original) => {
1599
+ let definition = original ?? errorDef;
1600
+ const traceId = core.context.get().traceId ?? core.context.get().requestId ?? original?.traceId ?? core.id.uuid();
1601
+ const fields = error && typeof error === "object" ? error : {};
1602
+ if (definition.code === HaiReldbError.QUERY_FAILED.code && (["23505", "ER_DUP_ENTRY", "SQLITE_CONSTRAINT_UNIQUE", "SQLITE_CONSTRAINT_PRIMARYKEY"].includes(String(fields.code)) || fields.errno === 1062))
1603
+ definition = HaiReldbError.DUPLICATE_ENTRY;
1604
+ const secrets = /* @__PURE__ */ new Set();
1605
+ const seen = /* @__PURE__ */ new WeakSet();
1606
+ const collect = (value) => {
1607
+ if (typeof value === "string" && value) {
1608
+ secrets.add(value);
1609
+ } else if (typeof value === "number" || typeof value === "bigint") {
1610
+ secrets.add(String(value));
1611
+ } else if (value && typeof value === "object" && !seen.has(value)) {
1612
+ seen.add(value);
1613
+ Object.values(value).forEach(collect);
1614
+ }
1615
+ };
1616
+ collect(errorMeta?.params);
1617
+ const payloads = [errorMeta, ...Array.isArray(errorMeta?.statements) ? errorMeta.statements : []];
1618
+ for (const payload of payloads) {
1619
+ collect(payload?.params);
1620
+ for (const match of (typeof payload?.sql === "string" ? payload.sql : "").matchAll(/'((?:''|[^'])*)'/g))
1621
+ secrets.add(match[1].replaceAll("''", "'"));
1622
+ }
1623
+ const diagnostic = {};
1624
+ for (const key2 of ["name", "message", "stack", "code", "errno", "sqlState", "detail", "hint", "constraint", "schema", "routine", "severity", "table", "column"]) {
1625
+ const value = error instanceof Error && key2 === "message" ? error.message : error instanceof Error && key2 === "stack" ? error.stack : fields[key2];
1626
+ if (typeof value === "string") {
1627
+ if (["code", "sqlState", "schema", "routine", "severity", "constraint", "table", "column"].includes(key2)) {
1628
+ diagnostic[key2] = core.sanitize.sanitizeText(value);
1629
+ } else {
1630
+ const frames = key2 === "stack" ? value.search(/^\s+at\s/m) : -1;
1631
+ const body = frames < 0 ? value : value.slice(0, frames);
1632
+ diagnostic[key2] = core.sanitize.sanitizeText(body, [...secrets]).replace(/'(?:''|[^'])*'/g, "'[REDACTED]'") + (frames < 0 ? "" : core.sanitize.sanitizeText(value.slice(frames)));
1633
+ }
1634
+ } else if (typeof value === "number") {
1635
+ diagnostic[key2] = value;
1636
+ }
1637
+ }
1638
+ if (!Object.keys(diagnostic).length)
1639
+ diagnostic.message = core.sanitize.sanitizeText(String(error), [...secrets]);
1640
+ ctx.logger.error(errorLabel, { ...summarizeOperationPayload(errorMeta), table: errorMeta?.tableName, column: errorMeta?.columnName, errorCode: definition.code, traceId, error: diagnostic });
1641
+ const key = definition.code === HaiReldbError.DDL_FAILED.code ? "reldb_ddlFailed" : definition.code === HaiReldbError.TRANSACTION_FAILED.code ? "reldb_txFailed" : "reldb_queryFailed";
1642
+ return err({ ...definition, traceId, message: definition.code === HaiReldbError.DUPLICATE_ENTRY.code ? reldbM("reldb_duplicateEntry") : original ? core.sanitize.sanitizeText(original.message, [...secrets]) : reldbM(key), cause: diagnostic });
1643
+ };
1595
1644
  try {
1596
- return await fn();
1645
+ const result = await fn();
1646
+ return result.success ? result : failed(result.error.cause ?? result.error, result.error);
1597
1647
  } catch (error) {
1598
- ctx.logger.error(errorLabel, { ...summarizeOperationPayload(errorMeta), errorCode: errorDef.code });
1599
- return err(errorDef, reldbM("reldb_queryFailed", { params: { error: String(error) } }), error);
1648
+ return failed(error);
1600
1649
  }
1601
1650
  }
1602
1651
  function createBaseDdlOps(ctx, raw) {
@@ -1670,7 +1719,7 @@ function createBaseDdlOps(ctx, raw) {
1670
1719
  },
1671
1720
  raw(sql) {
1672
1721
  logReldbOperation(ctx, "write", "ddl.raw", { sql });
1673
- return wrapOp(ctx, () => raw.raw(sql), HaiReldbError.DDL_FAILED, "DDL: raw failed");
1722
+ return wrapOp(ctx, () => raw.raw(sql), HaiReldbError.DDL_FAILED, "DDL: raw failed", { sql });
1674
1723
  }
1675
1724
  };
1676
1725
  }
@@ -1678,23 +1727,23 @@ function createBaseDmlOps(ctx, raw) {
1678
1727
  return {
1679
1728
  query: (sql, params) => {
1680
1729
  logReldbOperation(ctx, "read", "query", { sql, params });
1681
- return wrapOp(ctx, () => raw.query(sql, params), HaiReldbError.QUERY_FAILED, "DML: query failed");
1730
+ return wrapOp(ctx, () => raw.query(sql, params), HaiReldbError.QUERY_FAILED, "DML: query failed", { sql, params });
1682
1731
  },
1683
1732
  get: (sql, params) => {
1684
1733
  logReldbOperation(ctx, "read", "get", { sql, params });
1685
- return wrapOp(ctx, () => raw.get(sql, params), HaiReldbError.QUERY_FAILED, "DML: get failed");
1734
+ return wrapOp(ctx, () => raw.get(sql, params), HaiReldbError.QUERY_FAILED, "DML: get failed", { sql, params });
1686
1735
  },
1687
1736
  execute: (sql, params) => {
1688
1737
  logReldbOperation(ctx, "write", "execute", { sql, params });
1689
- return wrapOp(ctx, () => raw.execute(sql, params), HaiReldbError.QUERY_FAILED, "DML: execute failed");
1738
+ return wrapOp(ctx, () => raw.execute(sql, params), HaiReldbError.QUERY_FAILED, "DML: execute failed", { sql, params });
1690
1739
  },
1691
1740
  batch: (statements) => {
1692
1741
  logReldbOperation(ctx, "write", "batch", { count: statements.length, statements });
1693
- return wrapOp(ctx, () => raw.batch(statements), HaiReldbError.QUERY_FAILED, "DML: batch failed");
1742
+ return wrapOp(ctx, () => raw.batch(statements), HaiReldbError.QUERY_FAILED, "DML: batch failed", { statements });
1694
1743
  },
1695
1744
  queryPage: (options) => {
1696
1745
  logReldbOperation(ctx, "read", "queryPage", options);
1697
- return wrapOp(ctx, () => raw.queryPage(options), HaiReldbError.QUERY_FAILED, "DML: queryPage failed");
1746
+ return wrapOp(ctx, () => raw.queryPage(options), HaiReldbError.QUERY_FAILED, "DML: queryPage failed", { ...options });
1698
1747
  }
1699
1748
  };
1700
1749
  }
@@ -3363,7 +3412,99 @@ var reldb = {
3363
3412
  return await closingPromise;
3364
3413
  }
3365
3414
  };
3415
+ async function schemaColumns(db, table, sql = db.sql) {
3416
+ if (!/^[a-z][a-z0-9_]{0,62}$/.test(table))
3417
+ return err(HaiCommonError.VALIDATION_ERROR, core.i18n.coreM("core_errorValidation"));
3418
+ const type = db.config?.type;
3419
+ const found = type === "sqlite" ? await sql.query(`PRAGMA table_info(${table})`) : await sql.query(`SELECT column_name AS name FROM information_schema.columns WHERE table_schema=${type === "mysql" ? "DATABASE()" : "current_schema()"} AND table_name=?`, [table]);
3420
+ return found.success ? ok(found.data.map((row) => row.name)) : found;
3421
+ }
3422
+ async function migrateSchema(db, module, steps, options = {}) {
3423
+ if (!db.isInitialized || !/^[a-z][a-z0-9-]{0,62}$/.test(module) || steps.some((step, index) => step.version !== index + 1))
3424
+ return err(HaiCommonError.VALIDATION_ERROR, core.i18n.coreM("core_errorValidation"));
3425
+ const table = "hai_schema_migrations";
3426
+ const targetVersion = steps.length;
3427
+ const failed = () => err({ ...HaiCommonError.INIT_FAILED, message: core.i18n.coreM("core_runtimeStartFailed", { params: { module } }), ext: { module, phase: "migration" } });
3428
+ async function inspect(sql) {
3429
+ const columns = await schemaColumns(db, table, sql);
3430
+ if (!columns.success)
3431
+ return failed();
3432
+ const row = columns.data.length ? await sql.get(`SELECT version FROM ${table} WHERE module=?`, [module]) : ok(null);
3433
+ if (!row.success)
3434
+ return failed();
3435
+ const currentVersion = Number(row.data?.version ?? 0);
3436
+ if (currentVersion > targetVersion)
3437
+ return failed();
3438
+ const changes = [];
3439
+ for (const step of steps) {
3440
+ const needed = await step.needed(sql);
3441
+ if (!needed.success)
3442
+ return failed();
3443
+ if (needed.data)
3444
+ changes.push(step.name);
3445
+ }
3446
+ return ok({ module, currentVersion, targetVersion, changes, requiresExclusiveAccess: changes.length > 0 });
3447
+ }
3448
+ if (options.dryRun)
3449
+ return inspect(db.sql);
3450
+ const created = await db.sql.execute(`CREATE TABLE IF NOT EXISTS ${table} (module VARCHAR(64) PRIMARY KEY, version INTEGER NOT NULL)`);
3451
+ if (!created.success)
3452
+ return failed();
3453
+ const seeded = await db.sql.execute(db.config?.type === "mysql" ? `INSERT IGNORE INTO ${table}(module,version) VALUES (?,0)` : `INSERT INTO ${table}(module,version) VALUES (?,0) ON CONFLICT(module) DO NOTHING`, [module]);
3454
+ if (!seeded.success)
3455
+ return failed();
3456
+ const started = await db.tx.begin();
3457
+ if (!started.success)
3458
+ return failed();
3459
+ const tx = started.data;
3460
+ let mysqlLock = false;
3461
+ let committed = false;
3462
+ try {
3463
+ if (db.config?.type === "mysql") {
3464
+ const lock = await tx.get("SELECT GET_LOCK(?,30) AS acquired", [`hai-schema:${module}`]);
3465
+ if (!lock.success || Number(lock.data?.acquired) !== 1)
3466
+ return failed();
3467
+ mysqlLock = true;
3468
+ }
3469
+ const locked = await tx.execute(`UPDATE ${table} SET version=version WHERE module=?`, [module]);
3470
+ if (!locked.success)
3471
+ return failed();
3472
+ const plan = await inspect(tx);
3473
+ if (!plan.success)
3474
+ return plan;
3475
+ for (const step of steps) {
3476
+ const needed = await step.needed(tx);
3477
+ if (!needed.success)
3478
+ return failed();
3479
+ if (needed.data) {
3480
+ const applied = await step.apply(tx);
3481
+ if (!applied.success)
3482
+ return failed();
3483
+ const remaining = await step.needed(tx);
3484
+ if (!remaining.success || remaining.data)
3485
+ return failed();
3486
+ }
3487
+ }
3488
+ const recorded = await tx.execute(`UPDATE ${table} SET version=? WHERE module=?`, [targetVersion, module]);
3489
+ if (!recorded.success)
3490
+ return failed();
3491
+ if (mysqlLock) {
3492
+ await tx.get("SELECT RELEASE_LOCK(?)", [`hai-schema:${module}`]);
3493
+ mysqlLock = false;
3494
+ }
3495
+ const result = await tx.commit();
3496
+ committed = result.success;
3497
+ return result.success ? plan : failed();
3498
+ } catch {
3499
+ return failed();
3500
+ } finally {
3501
+ if (mysqlLock)
3502
+ await tx.get("SELECT RELEASE_LOCK(?)", [`hai-schema:${module}`]);
3503
+ if (!committed)
3504
+ await tx.rollback();
3505
+ }
3506
+ }
3366
3507
 
3367
- export { BaseReldbCrudRepository, HaiReldbError, createJsonOps, escapeSqlString, parseJsonPath, quoteIdentifier, reldb, validateIdentifier, validateIdentifiers };
3508
+ export { BaseReldbCrudRepository, HaiReldbError, createJsonOps, escapeSqlString, migrateSchema, parseJsonPath, quoteIdentifier, reldb, schemaColumns, validateIdentifier, validateIdentifiers };
3368
3509
  //# sourceMappingURL=index.js.map
3369
3510
  //# sourceMappingURL=index.js.map