@h-ai/reldb 0.1.0-alpha8 → 1.0.0-beta.1
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 +661 -0
- package/README.ja.md +661 -0
- package/README.md +258 -58
- package/dist/chunk-SDS2ZT3V.js +132 -0
- package/dist/chunk-SDS2ZT3V.js.map +1 -0
- package/dist/index.d.ts +650 -551
- package/dist/index.js +1293 -386
- package/dist/index.js.map +1 -1
- package/dist/providers/sqlite/reldb-sqlite-worker.mjs +143 -0
- package/dist/schemas.d.ts +490 -0
- package/dist/schemas.js +3 -0
- package/dist/schemas.js.map +1 -0
- package/package.json +25 -16
package/dist/index.js
CHANGED
|
@@ -1,173 +1,127 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { ReldbConfigSchema, MysqlPoolConfigSchema } from './chunk-SDS2ZT3V.js';
|
|
2
|
+
export { MysqlConfigSchema, MysqlOptionsSchema, MysqlPoolConfigSchema, MysqlSslConfigSchema, PoolConfigSchema, PostgresqlConfigSchema, ReldbConfigSchema, ReldbOperationLogConfigSchema, ReldbTypeSchema, SqliteConfigSchema, SqliteOptionsSchema, SslConfigSchema } from './chunk-SDS2ZT3V.js';
|
|
3
|
+
import { core, ok, err, HaiCommonError } from '@h-ai/core';
|
|
4
|
+
import process from 'process';
|
|
3
5
|
import { createRequire } from 'module';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
10
|
-
});
|
|
11
|
-
var ReldbTypeSchema = z.enum(["sqlite", "postgresql", "mysql"]);
|
|
12
|
-
var PoolConfigSchema = z.object({
|
|
13
|
-
/** 最小连接数(默认 1) */
|
|
14
|
-
min: z.number().int().min(0).default(1),
|
|
15
|
-
/** 最大连接数(默认 10) */
|
|
16
|
-
max: z.number().int().min(1).default(10),
|
|
17
|
-
/** 空闲连接超时时间,单位毫秒(默认 30000) */
|
|
18
|
-
idleTimeout: z.number().int().min(0).default(3e4),
|
|
19
|
-
/** 获取连接超时时间,单位毫秒(默认 10000) */
|
|
20
|
-
acquireTimeout: z.number().int().min(0).default(1e4)
|
|
21
|
-
});
|
|
22
|
-
var SslConfigSchema = z.union([
|
|
23
|
-
z.boolean(),
|
|
24
|
-
z.enum(["require", "prefer", "allow", "disable"]),
|
|
25
|
-
z.record(z.string(), z.unknown())
|
|
26
|
-
]);
|
|
27
|
-
var SqliteOptionsSchema = z.object({
|
|
28
|
-
/** 是否启用 WAL 模式(默认 true),可提高并发读写性能 */
|
|
29
|
-
walMode: z.boolean().default(true),
|
|
30
|
-
/** 是否只读模式(默认 false) */
|
|
31
|
-
readonly: z.boolean().default(false)
|
|
32
|
-
});
|
|
33
|
-
var MysqlOptionsSchema = z.object({
|
|
34
|
-
/** 字符集(默认 utf8mb4),推荐使用 utf8mb4 支持完整 Unicode */
|
|
35
|
-
charset: z.string().default("utf8mb4"),
|
|
36
|
-
/** 时区设置,如 '+08:00' */
|
|
37
|
-
timezone: z.string().optional()
|
|
38
|
-
});
|
|
39
|
-
var NetworkConfigSchema = z.object({
|
|
40
|
-
/**
|
|
41
|
-
* 连接字符串(可选)
|
|
42
|
-
* 如果提供 url,将优先使用,忽略 host/port 等字段
|
|
43
|
-
*/
|
|
44
|
-
url: z.string().optional(),
|
|
45
|
-
/** 数据库主机地址(默认 localhost) */
|
|
46
|
-
host: z.string().default("localhost"),
|
|
47
|
-
/**
|
|
48
|
-
* 数据库端口
|
|
49
|
-
* - PostgreSQL 默认 5432
|
|
50
|
-
* - MySQL 默认 3306
|
|
51
|
-
*/
|
|
52
|
-
port: z.number().int().min(1).max(65535).optional(),
|
|
53
|
-
/** 数据库名称 */
|
|
54
|
-
database: z.string(),
|
|
55
|
-
/** 数据库用户名 */
|
|
56
|
-
user: z.string().optional(),
|
|
57
|
-
/** 数据库密码 */
|
|
58
|
-
password: z.string().optional(),
|
|
59
|
-
/** SSL/TLS 连接配置 */
|
|
60
|
-
ssl: SslConfigSchema.optional(),
|
|
61
|
-
/** 连接池配置 */
|
|
62
|
-
pool: PoolConfigSchema.optional()
|
|
63
|
-
});
|
|
64
|
-
var SqliteConfigSchema = z.object({
|
|
65
|
-
type: z.literal("sqlite"),
|
|
66
|
-
/** 数据库文件路径(如 './data.db')或 ':memory:'(内存数据库) */
|
|
67
|
-
database: z.string(),
|
|
68
|
-
/** SQLite 特定选项 */
|
|
69
|
-
sqlite: SqliteOptionsSchema.optional()
|
|
70
|
-
});
|
|
71
|
-
var PostgresqlConfigSchema = NetworkConfigSchema.extend({
|
|
72
|
-
type: z.literal("postgresql")
|
|
73
|
-
});
|
|
74
|
-
var MysqlConfigSchema = NetworkConfigSchema.extend({
|
|
75
|
-
type: z.literal("mysql"),
|
|
76
|
-
/** MySQL 特定选项 */
|
|
77
|
-
mysql: MysqlOptionsSchema.optional()
|
|
78
|
-
});
|
|
79
|
-
var ReldbConfigSchema = z.discriminatedUnion("type", [
|
|
80
|
-
SqliteConfigSchema,
|
|
81
|
-
PostgresqlConfigSchema,
|
|
82
|
-
MysqlConfigSchema
|
|
83
|
-
]);
|
|
6
|
+
import { isIP } from 'net';
|
|
7
|
+
import { performance as performance$1 } from 'perf_hooks';
|
|
8
|
+
import { setTimeout as setTimeout$1 } from 'timers/promises';
|
|
9
|
+
import { existsSync } from 'fs';
|
|
10
|
+
import { Worker } from 'worker_threads';
|
|
84
11
|
|
|
85
12
|
// messages/en-US.json
|
|
86
13
|
var en_US_default = {
|
|
87
14
|
$schema: "https://inlang.com/schema/inlang-message-format",
|
|
88
15
|
reldb_notInitialized: "Database not initialized, please call db.init() first",
|
|
89
|
-
reldb_initFailed: "Failed to initialize database
|
|
90
|
-
reldb_closeFailed: "Failed to close database
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
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",
|
|
97
23
|
reldb_postgresOnlyPostgresql: "PostgreSQL Provider only supports postgresql type",
|
|
98
|
-
reldb_postgresConnectionFailed: "Failed to connect to PostgreSQL
|
|
24
|
+
reldb_postgresConnectionFailed: "Failed to connect to PostgreSQL",
|
|
99
25
|
reldb_sqliteOnlySqlite: "SQLite Provider only supports sqlite type",
|
|
100
26
|
reldb_sqliteNeedPath: "SQLite requires database path (database field)",
|
|
101
|
-
reldb_sqliteConnectionFailed: "Failed to connect to SQLite
|
|
102
|
-
reldb_sqliteTxFailed: "Transaction failed
|
|
103
|
-
|
|
104
|
-
reldb_sqliteDropTableFailed: "Failed to drop table {tableName}: {error}",
|
|
105
|
-
reldb_sqliteAddColumnFailed: "Failed to add column {columnName} to table {tableName}: {error}",
|
|
106
|
-
reldb_sqliteDropColumnFailed: "Failed to drop column {columnName} from table {tableName}: {error}",
|
|
107
|
-
reldb_sqliteRenameTableFailed: "Failed to rename table {oldName} to {newName}: {error}",
|
|
108
|
-
reldb_sqliteCreateIndexFailed: "Failed to create index {indexName}: {error}",
|
|
109
|
-
reldb_sqliteDropIndexFailed: "Failed to drop index {indexName}: {error}",
|
|
110
|
-
reldb_sqliteExecDdlFailed: "Failed to execute DDL: {error}",
|
|
111
|
-
reldb_sqliteQueryFailed: "Query failed: {error}",
|
|
112
|
-
reldb_sqliteExecuteFailed: "Execute failed: {error}",
|
|
113
|
-
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",
|
|
114
30
|
reldb_mysqlOnlyMysql: "MySQL Provider only supports mysql type",
|
|
115
|
-
reldb_mysqlConnectionFailed: "Failed to connect to MySQL
|
|
116
|
-
reldb_mysqlTxFailed: "Transaction failed
|
|
31
|
+
reldb_mysqlConnectionFailed: "Failed to connect to MySQL",
|
|
32
|
+
reldb_mysqlTxFailed: "Transaction failed",
|
|
33
|
+
reldb_mysqlPoolQueueFull: "MySQL connection acquisition queue is full",
|
|
34
|
+
reldb_mysqlPoolAcquireTimeout: "MySQL connection acquisition deadline exceeded",
|
|
35
|
+
reldb_mysqlPoolReleaseFailed: "MySQL connection release failed; close the database before retrying",
|
|
36
|
+
reldb_mysqlTlsDnsRequired: "MySQL hostname verification requires a DNS hostname matching the certificate; the current driver cannot verify an IP address identity",
|
|
117
37
|
reldb_crudEmptyPayload: "CRUD payload is empty, please provide data",
|
|
118
38
|
reldb_crudNoValidColumns: "CRUD payload has no valid columns",
|
|
119
39
|
reldb_crudRecordNotFound: "Record not found: {id}",
|
|
120
40
|
reldb_invalidIdentifier: "Invalid SQL identifier: {name} (only letters, digits and underscores allowed)",
|
|
121
|
-
reldb_configError: "Database config validation failed
|
|
122
|
-
reldb_txFailed: "Transaction failed
|
|
41
|
+
reldb_configError: "Database config validation failed",
|
|
42
|
+
reldb_txFailed: "Transaction failed",
|
|
43
|
+
reldb_sharedBackendRequired: "Production mode requires a shared backend; local files and memory backends are for development or explicit end-to-end tests.",
|
|
44
|
+
reldb_jsonPatchObjectRequired: "JSON merge patch must be an object"
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// messages/ja-JP.json
|
|
48
|
+
var ja_JP_default = {
|
|
49
|
+
$schema: "https://inlang.com/schema/inlang-message-format",
|
|
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",
|
|
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",
|
|
58
|
+
reldb_postgresOnlyPostgresql: "PostgreSQL\u30D7\u30ED\u30D0\u30A4\u30C0\u30FC\u306Fpostgresql\u578B\u306E\u307F\u306B\u5BFE\u5FDC\u3057\u3066\u3044\u307E\u3059",
|
|
59
|
+
reldb_postgresConnectionFailed: "PostgreSQL\u3078\u306E\u63A5\u7D9A\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
|
|
60
|
+
reldb_sqliteOnlySqlite: "SQLite\u30D7\u30ED\u30D0\u30A4\u30C0\u30FC\u306Fsqlite\u578B\u306E\u307F\u306B\u5BFE\u5FDC\u3057\u3066\u3044\u307E\u3059",
|
|
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",
|
|
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",
|
|
65
|
+
reldb_mysqlOnlyMysql: "MySQL\u30D7\u30ED\u30D0\u30A4\u30C0\u30FC\u306Fmysql\u578B\u306E\u307F\u306B\u5BFE\u5FDC\u3057\u3066\u3044\u307E\u3059",
|
|
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",
|
|
68
|
+
reldb_mysqlPoolQueueFull: "MySQL \u63A5\u7D9A\u53D6\u5F97\u30AD\u30E5\u30FC\u304C\u6E80\u676F\u3067\u3059",
|
|
69
|
+
reldb_mysqlPoolAcquireTimeout: "MySQL \u63A5\u7D9A\u53D6\u5F97\u306E\u5F85\u6A5F\u671F\u9650\u3092\u8D85\u904E\u3057\u307E\u3057\u305F",
|
|
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
|
+
reldb_mysqlTlsDnsRequired: "MySQL \u306E\u30DB\u30B9\u30C8\u540D\u691C\u8A3C\u306B\u306F\u8A3C\u660E\u66F8\u3068\u4E00\u81F4\u3059\u308B DNS \u30DB\u30B9\u30C8\u540D\u304C\u5FC5\u8981\u3067\u3059\u3002\u73FE\u5728\u306E\u30C9\u30E9\u30A4\u30D0\u30FC\u3067\u306F IP \u30A2\u30C9\u30EC\u30B9\u306E\u8B58\u5225\u60C5\u5831\u3092\u691C\u8A3C\u3067\u304D\u307E\u305B\u3093",
|
|
72
|
+
reldb_crudEmptyPayload: "CRUD\u30DA\u30A4\u30ED\u30FC\u30C9\u304C\u7A7A\u3067\u3059\u3002\u30C7\u30FC\u30BF\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044",
|
|
73
|
+
reldb_crudNoValidColumns: "CRUD\u30DA\u30A4\u30ED\u30FC\u30C9\u306B\u6709\u52B9\u306A\u5217\u304C\u3042\u308A\u307E\u305B\u3093",
|
|
74
|
+
reldb_crudRecordNotFound: "\u30EC\u30B3\u30FC\u30C9\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: {id}",
|
|
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",
|
|
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",
|
|
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",
|
|
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"
|
|
123
80
|
};
|
|
124
81
|
|
|
125
82
|
// messages/zh-CN.json
|
|
126
83
|
var zh_CN_default = {
|
|
127
84
|
$schema: "https://inlang.com/schema/inlang-message-format",
|
|
128
85
|
reldb_notInitialized: "\u6570\u636E\u5E93\u672A\u521D\u59CB\u5316\uFF0C\u8BF7\u5148\u8C03\u7528 db.init()",
|
|
129
|
-
reldb_initFailed: "\u521D\u59CB\u5316\u6570\u636E\u5E93\u5931\u8D25
|
|
130
|
-
reldb_closeFailed: "\u5173\u95ED\u6570\u636E\u5E93\u5931\u8D25
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
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",
|
|
137
93
|
reldb_postgresOnlyPostgresql: "PostgreSQL Provider \u4EC5\u652F\u6301 postgresql \u7C7B\u578B",
|
|
138
|
-
reldb_postgresConnectionFailed: "\u8FDE\u63A5 PostgreSQL \u5931\u8D25
|
|
94
|
+
reldb_postgresConnectionFailed: "\u8FDE\u63A5 PostgreSQL \u5931\u8D25",
|
|
139
95
|
reldb_sqliteOnlySqlite: "SQLite Provider \u4EC5\u652F\u6301 sqlite \u7C7B\u578B",
|
|
140
96
|
reldb_sqliteNeedPath: "SQLite \u9700\u8981\u63D0\u4F9B\u6570\u636E\u5E93\u8DEF\u5F84\uFF08database \u5B57\u6BB5\uFF09",
|
|
141
|
-
reldb_sqliteConnectionFailed: "\u8FDE\u63A5 SQLite \u5931\u8D25
|
|
142
|
-
reldb_sqliteTxFailed: "\u4E8B\u52A1\u6267\u884C\u5931\u8D25
|
|
143
|
-
|
|
144
|
-
reldb_sqliteDropTableFailed: "\u5220\u9664\u8868 {tableName} \u5931\u8D25: {error}",
|
|
145
|
-
reldb_sqliteAddColumnFailed: "\u5411\u8868 {tableName} \u6DFB\u52A0\u5217 {columnName} \u5931\u8D25: {error}",
|
|
146
|
-
reldb_sqliteDropColumnFailed: "\u4ECE\u8868 {tableName} \u5220\u9664\u5217 {columnName} \u5931\u8D25: {error}",
|
|
147
|
-
reldb_sqliteRenameTableFailed: "\u91CD\u547D\u540D\u8868 {oldName} \u4E3A {newName} \u5931\u8D25: {error}",
|
|
148
|
-
reldb_sqliteCreateIndexFailed: "\u521B\u5EFA\u7D22\u5F15 {indexName} \u5931\u8D25: {error}",
|
|
149
|
-
reldb_sqliteDropIndexFailed: "\u5220\u9664\u7D22\u5F15 {indexName} \u5931\u8D25: {error}",
|
|
150
|
-
reldb_sqliteExecDdlFailed: "\u6267\u884C DDL \u5931\u8D25: {error}",
|
|
151
|
-
reldb_sqliteQueryFailed: "\u67E5\u8BE2\u5931\u8D25: {error}",
|
|
152
|
-
reldb_sqliteExecuteFailed: "\u6267\u884C\u5931\u8D25: {error}",
|
|
153
|
-
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",
|
|
154
100
|
reldb_mysqlOnlyMysql: "MySQL Provider \u4EC5\u652F\u6301 mysql \u7C7B\u578B",
|
|
155
|
-
reldb_mysqlConnectionFailed: "\u8FDE\u63A5 MySQL \u5931\u8D25
|
|
156
|
-
reldb_mysqlTxFailed: "\u4E8B\u52A1\u6267\u884C\u5931\u8D25
|
|
101
|
+
reldb_mysqlConnectionFailed: "\u8FDE\u63A5 MySQL \u5931\u8D25",
|
|
102
|
+
reldb_mysqlTxFailed: "\u4E8B\u52A1\u6267\u884C\u5931\u8D25",
|
|
103
|
+
reldb_mysqlPoolQueueFull: "MySQL \u8FDE\u63A5\u83B7\u53D6\u7B49\u5F85\u961F\u5217\u5DF2\u6EE1",
|
|
104
|
+
reldb_mysqlPoolAcquireTimeout: "MySQL \u8FDE\u63A5\u83B7\u53D6\u5DF2\u8D85\u8FC7\u622A\u6B62\u671F\u9650",
|
|
105
|
+
reldb_mysqlPoolReleaseFailed: "MySQL \u8FDE\u63A5\u5F52\u8FD8\u5931\u8D25\uFF0C\u8BF7\u5173\u95ED\u6570\u636E\u5E93\u540E\u91CD\u8BD5",
|
|
106
|
+
reldb_mysqlTlsDnsRequired: "MySQL \u5F53\u524D\u9A71\u52A8\u7684\u4E3B\u673A\u8EAB\u4EFD\u9A8C\u8BC1\u8981\u6C42\u4F7F\u7528\u4E0E\u8BC1\u4E66\u5339\u914D\u7684 DNS \u4E3B\u673A\u540D\uFF0C\u4E0D\u80FD\u9A8C\u8BC1 IP \u5730\u5740\u8EAB\u4EFD",
|
|
157
107
|
reldb_crudEmptyPayload: "CRUD \u6570\u636E\u4E3A\u7A7A\uFF0C\u8BF7\u63D0\u4F9B\u6570\u636E",
|
|
158
108
|
reldb_crudNoValidColumns: "CRUD \u6570\u636E\u6CA1\u6709\u6709\u6548\u5217",
|
|
159
109
|
reldb_crudRecordNotFound: "\u8BB0\u5F55\u4E0D\u5B58\u5728: {id}",
|
|
160
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",
|
|
161
|
-
reldb_configError: "\u6570\u636E\u5E93\u914D\u7F6E\u6821\u9A8C\u5931\u8D25
|
|
162
|
-
reldb_txFailed: "\u4E8B\u52A1\u6267\u884C\u5931\u8D25
|
|
111
|
+
reldb_configError: "\u6570\u636E\u5E93\u914D\u7F6E\u6821\u9A8C\u5931\u8D25",
|
|
112
|
+
reldb_txFailed: "\u4E8B\u52A1\u6267\u884C\u5931\u8D25",
|
|
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",
|
|
114
|
+
reldb_jsonPatchObjectRequired: "JSON \u5408\u5E76\u8865\u4E01\u5FC5\u987B\u662F\u5BF9\u8C61"
|
|
163
115
|
};
|
|
164
116
|
|
|
165
117
|
// src/reldb-i18n.ts
|
|
166
118
|
var reldbM = core.i18n.createMessageGetter({
|
|
167
119
|
"zh-CN": zh_CN_default,
|
|
168
|
-
"en-US": en_US_default
|
|
120
|
+
"en-US": en_US_default,
|
|
121
|
+
"ja-JP": ja_JP_default
|
|
169
122
|
});
|
|
170
123
|
var ReldbErrorInfo = {
|
|
124
|
+
// ─── 通用 001-009 ───
|
|
171
125
|
CONNECTION_FAILED: "001:500",
|
|
172
126
|
QUERY_FAILED: "002:500",
|
|
173
127
|
CONSTRAINT_VIOLATION: "003:409",
|
|
@@ -177,11 +131,13 @@ var ReldbErrorInfo = {
|
|
|
177
131
|
DUPLICATE_ENTRY: "007:409",
|
|
178
132
|
DEADLOCK: "008:500",
|
|
179
133
|
TIMEOUT: "009:504",
|
|
180
|
-
|
|
181
|
-
NOT_INITIALIZED: "
|
|
182
|
-
|
|
183
|
-
UNSUPPORTED_TYPE: "
|
|
184
|
-
CONFIG_ERROR: "
|
|
134
|
+
// ─── 初始化 010-019 ───
|
|
135
|
+
NOT_INITIALIZED: "010:500",
|
|
136
|
+
POOL_EXHAUSTED: "011:503",
|
|
137
|
+
UNSUPPORTED_TYPE: "012:400",
|
|
138
|
+
CONFIG_ERROR: "013:500",
|
|
139
|
+
// ─── 业务操作 020+ ───
|
|
140
|
+
DDL_FAILED: "020:500"
|
|
185
141
|
};
|
|
186
142
|
var HaiReldbError = core.error.buildHaiErrorsDef("reldb", ReldbErrorInfo);
|
|
187
143
|
|
|
@@ -420,12 +376,13 @@ var BaseReldbCrudRepository = class {
|
|
|
420
376
|
*/
|
|
421
377
|
mapRow(row) {
|
|
422
378
|
const result = {};
|
|
379
|
+
const resultRecord = result;
|
|
423
380
|
for (const field of this.fields) {
|
|
424
381
|
if (!field.select) {
|
|
425
382
|
continue;
|
|
426
383
|
}
|
|
427
384
|
const value = this.fromDbValue(row[field.columnName], field.def);
|
|
428
|
-
|
|
385
|
+
resultRecord[field.fieldName] = value;
|
|
429
386
|
}
|
|
430
387
|
return result;
|
|
431
388
|
}
|
|
@@ -498,7 +455,11 @@ var BaseReldbCrudRepository = class {
|
|
|
498
455
|
return payload;
|
|
499
456
|
}
|
|
500
457
|
/**
|
|
501
|
-
*
|
|
458
|
+
* 等待初始化完成(自动建表等)
|
|
459
|
+
*
|
|
460
|
+
* 内置 CRUD 方法均已在执行前调用;自定义方法若通过 `this.sql()` 直接执行原始
|
|
461
|
+
* SQL(尤其是写操作或在事务中),应先 `await this.ensureReady()` 以避免表尚未
|
|
462
|
+
* 建好时(例如刚 dropTable 后、或后端建表较慢时)产生「表不存在」的竞态失败。
|
|
502
463
|
*
|
|
503
464
|
* @returns 初始化结果
|
|
504
465
|
*/
|
|
@@ -517,6 +478,17 @@ var BaseReldbCrudRepository = class {
|
|
|
517
478
|
* @param data - 业务字段与值的映射
|
|
518
479
|
* @param tx - 可选事务句柄
|
|
519
480
|
* @returns 插入结果(含 changes、lastInsertRowid)
|
|
481
|
+
* @example
|
|
482
|
+
* ```ts
|
|
483
|
+
* import type { ReldbCrudRepository } from '@h-ai/reldb'
|
|
484
|
+
*
|
|
485
|
+
* async function use(repo: ReldbCrudRepository<{ id: string, name: string }>) {
|
|
486
|
+
* const result = await repo.create({ name: 'Alice' })
|
|
487
|
+
* if (!result.success) throw new Error(result.error.message)
|
|
488
|
+
* return result.data
|
|
489
|
+
* }
|
|
490
|
+
* void use
|
|
491
|
+
* ```
|
|
520
492
|
*/
|
|
521
493
|
async create(data, tx) {
|
|
522
494
|
const ready = await this.ensureReady();
|
|
@@ -537,6 +509,17 @@ var BaseReldbCrudRepository = class {
|
|
|
537
509
|
* @param items - 待插入的业务数据数组
|
|
538
510
|
* @param tx - 可选事务句柄
|
|
539
511
|
* @returns 批量插入结果
|
|
512
|
+
* @example
|
|
513
|
+
* ```ts
|
|
514
|
+
* import type { ReldbCrudRepository } from '@h-ai/reldb'
|
|
515
|
+
*
|
|
516
|
+
* async function use(repo: ReldbCrudRepository<{ id: string, name: string }>) {
|
|
517
|
+
* const result = await repo.createMany([{ name: 'Alice' }, { name: 'Bob' }])
|
|
518
|
+
* if (!result.success) throw new Error(result.error.message)
|
|
519
|
+
* return result.data
|
|
520
|
+
* }
|
|
521
|
+
* void use
|
|
522
|
+
* ```
|
|
540
523
|
*/
|
|
541
524
|
async createMany(items, tx) {
|
|
542
525
|
const ready = await this.ensureReady();
|
|
@@ -571,6 +554,17 @@ var BaseReldbCrudRepository = class {
|
|
|
571
554
|
* @param data - 业务字段与值的映射
|
|
572
555
|
* @param tx - 可选事务句柄
|
|
573
556
|
* @returns 执行结果(含 changes)
|
|
557
|
+
* @example
|
|
558
|
+
* ```ts
|
|
559
|
+
* import type { ReldbCrudRepository } from '@h-ai/reldb'
|
|
560
|
+
*
|
|
561
|
+
* async function use(repo: ReldbCrudRepository<{ id: string, name: string }>) {
|
|
562
|
+
* const result = await repo.createOrUpdate({ id: 'u1', name: 'Alice' })
|
|
563
|
+
* if (!result.success) throw new Error(result.error.message)
|
|
564
|
+
* return result.data
|
|
565
|
+
* }
|
|
566
|
+
* void use
|
|
567
|
+
* ```
|
|
574
568
|
*/
|
|
575
569
|
async createOrUpdate(data, tx) {
|
|
576
570
|
const ready = await this.ensureReady();
|
|
@@ -613,6 +607,17 @@ var BaseReldbCrudRepository = class {
|
|
|
613
607
|
* @param id - 主键值
|
|
614
608
|
* @param tx - 可选事务句柄
|
|
615
609
|
* @returns 业务模型对象或 null(未找到)
|
|
610
|
+
* @example
|
|
611
|
+
* ```ts
|
|
612
|
+
* import type { ReldbCrudRepository } from '@h-ai/reldb'
|
|
613
|
+
*
|
|
614
|
+
* async function use(repo: ReldbCrudRepository<{ id: string, name: string }>) {
|
|
615
|
+
* const result = await repo.findById('u1')
|
|
616
|
+
* if (!result.success) throw new Error(result.error.message)
|
|
617
|
+
* return result.data
|
|
618
|
+
* }
|
|
619
|
+
* void use
|
|
620
|
+
* ```
|
|
616
621
|
*/
|
|
617
622
|
async findById(id, tx) {
|
|
618
623
|
const ready = await this.ensureReady();
|
|
@@ -629,6 +634,17 @@ var BaseReldbCrudRepository = class {
|
|
|
629
634
|
* @param id - 主键值
|
|
630
635
|
* @param tx - 可选事务句柄
|
|
631
636
|
* @returns 业务模型对象(不存在时返回错误)
|
|
637
|
+
* @example
|
|
638
|
+
* ```ts
|
|
639
|
+
* import type { ReldbCrudRepository } from '@h-ai/reldb'
|
|
640
|
+
*
|
|
641
|
+
* async function use(repo: ReldbCrudRepository<{ id: string, name: string }>) {
|
|
642
|
+
* const result = await repo.getById('u1')
|
|
643
|
+
* if (!result.success) throw new Error(result.error.message)
|
|
644
|
+
* return result.data
|
|
645
|
+
* }
|
|
646
|
+
* void use
|
|
647
|
+
* ```
|
|
632
648
|
*/
|
|
633
649
|
async getById(id, tx) {
|
|
634
650
|
const ready = await this.ensureReady();
|
|
@@ -643,6 +659,17 @@ var BaseReldbCrudRepository = class {
|
|
|
643
659
|
* @param options - 查询条件(where、orderBy、limit、offset)
|
|
644
660
|
* @param tx - 可选事务句柄
|
|
645
661
|
* @returns 业务模型数组
|
|
662
|
+
* @example
|
|
663
|
+
* ```ts
|
|
664
|
+
* import type { ReldbCrudRepository } from '@h-ai/reldb'
|
|
665
|
+
*
|
|
666
|
+
* async function use(repo: ReldbCrudRepository<{ id: string, name: string }>) {
|
|
667
|
+
* const result = await repo.findAll({ where: 'name = ?', params: ['Alice'] })
|
|
668
|
+
* if (!result.success) throw new Error(result.error.message)
|
|
669
|
+
* return result.data
|
|
670
|
+
* }
|
|
671
|
+
* void use
|
|
672
|
+
* ```
|
|
646
673
|
*/
|
|
647
674
|
async findAll(options, tx) {
|
|
648
675
|
const ready = await this.ensureReady();
|
|
@@ -657,6 +684,17 @@ var BaseReldbCrudRepository = class {
|
|
|
657
684
|
* @param options - 分页查询条件(where、orderBy、pagination、overrides)
|
|
658
685
|
* @param tx - 可选事务句柄
|
|
659
686
|
* @returns 分页结果(含 items、total、page、pageSize)
|
|
687
|
+
* @example
|
|
688
|
+
* ```ts
|
|
689
|
+
* import type { ReldbCrudRepository } from '@h-ai/reldb'
|
|
690
|
+
*
|
|
691
|
+
* async function use(repo: ReldbCrudRepository<{ id: string, name: string }>) {
|
|
692
|
+
* const result = await repo.findPage({ pagination: { page: 1, pageSize: 10 }, orderBy: 'id ASC' })
|
|
693
|
+
* if (!result.success) throw new Error(result.error.message)
|
|
694
|
+
* return result.data
|
|
695
|
+
* }
|
|
696
|
+
* void use
|
|
697
|
+
* ```
|
|
660
698
|
*/
|
|
661
699
|
async findPage(options, tx) {
|
|
662
700
|
const ready = await this.ensureReady();
|
|
@@ -674,6 +712,17 @@ var BaseReldbCrudRepository = class {
|
|
|
674
712
|
* @param data - 待更新的业务字段
|
|
675
713
|
* @param tx - 可选事务句柄
|
|
676
714
|
* @returns 更新结果(含 changes)
|
|
715
|
+
* @example
|
|
716
|
+
* ```ts
|
|
717
|
+
* import type { ReldbCrudRepository } from '@h-ai/reldb'
|
|
718
|
+
*
|
|
719
|
+
* async function use(repo: ReldbCrudRepository<{ id: string, name: string }>) {
|
|
720
|
+
* const result = await repo.updateById('u1', { name: 'Bob' })
|
|
721
|
+
* if (!result.success) throw new Error(result.error.message)
|
|
722
|
+
* return result.data
|
|
723
|
+
* }
|
|
724
|
+
* void use
|
|
725
|
+
* ```
|
|
677
726
|
*/
|
|
678
727
|
async updateById(id, data, tx) {
|
|
679
728
|
const ready = await this.ensureReady();
|
|
@@ -692,6 +741,17 @@ var BaseReldbCrudRepository = class {
|
|
|
692
741
|
* @param id - 主键值
|
|
693
742
|
* @param tx - 可选事务句柄
|
|
694
743
|
* @returns 删除结果(含 changes)
|
|
744
|
+
* @example
|
|
745
|
+
* ```ts
|
|
746
|
+
* import type { ReldbCrudRepository } from '@h-ai/reldb'
|
|
747
|
+
*
|
|
748
|
+
* async function use(repo: ReldbCrudRepository<{ id: string, name: string }>) {
|
|
749
|
+
* const result = await repo.deleteById('u1')
|
|
750
|
+
* if (!result.success) throw new Error(result.error.message)
|
|
751
|
+
* return result.data
|
|
752
|
+
* }
|
|
753
|
+
* void use
|
|
754
|
+
* ```
|
|
695
755
|
*/
|
|
696
756
|
async deleteById(id, tx) {
|
|
697
757
|
const ready = await this.ensureReady();
|
|
@@ -706,6 +766,17 @@ var BaseReldbCrudRepository = class {
|
|
|
706
766
|
* @param options - 查询条件(where、params)
|
|
707
767
|
* @param tx - 可选事务句柄
|
|
708
768
|
* @returns 记录数
|
|
769
|
+
* @example
|
|
770
|
+
* ```ts
|
|
771
|
+
* import type { ReldbCrudRepository } from '@h-ai/reldb'
|
|
772
|
+
*
|
|
773
|
+
* async function use(repo: ReldbCrudRepository<{ id: string, name: string }>) {
|
|
774
|
+
* const result = await repo.count({ where: 'name = ?', params: ['Alice'] })
|
|
775
|
+
* if (!result.success) throw new Error(result.error.message)
|
|
776
|
+
* return result.data
|
|
777
|
+
* }
|
|
778
|
+
* void use
|
|
779
|
+
* ```
|
|
709
780
|
*/
|
|
710
781
|
async count(options, tx) {
|
|
711
782
|
const ready = await this.ensureReady();
|
|
@@ -720,6 +791,17 @@ var BaseReldbCrudRepository = class {
|
|
|
720
791
|
* @param options - 查询条件(where、params)
|
|
721
792
|
* @param tx - 可选事务句柄
|
|
722
793
|
* @returns 是否存在
|
|
794
|
+
* @example
|
|
795
|
+
* ```ts
|
|
796
|
+
* import type { ReldbCrudRepository } from '@h-ai/reldb'
|
|
797
|
+
*
|
|
798
|
+
* async function use(repo: ReldbCrudRepository<{ id: string, name: string }>) {
|
|
799
|
+
* const result = await repo.exists({ where: 'name = ?', params: ['Alice'] })
|
|
800
|
+
* if (!result.success) throw new Error(result.error.message)
|
|
801
|
+
* return result.data
|
|
802
|
+
* }
|
|
803
|
+
* void use
|
|
804
|
+
* ```
|
|
723
805
|
*/
|
|
724
806
|
async exists(options, tx) {
|
|
725
807
|
const ready = await this.ensureReady();
|
|
@@ -734,6 +816,17 @@ var BaseReldbCrudRepository = class {
|
|
|
734
816
|
* @param id - 主键值
|
|
735
817
|
* @param tx - 可选事务句柄
|
|
736
818
|
* @returns 是否存在
|
|
819
|
+
* @example
|
|
820
|
+
* ```ts
|
|
821
|
+
* import type { ReldbCrudRepository } from '@h-ai/reldb'
|
|
822
|
+
*
|
|
823
|
+
* async function use(repo: ReldbCrudRepository<{ id: string, name: string }>) {
|
|
824
|
+
* const result = await repo.existsById('u1')
|
|
825
|
+
* if (!result.success) throw new Error(result.error.message)
|
|
826
|
+
* return result.data
|
|
827
|
+
* }
|
|
828
|
+
* void use
|
|
829
|
+
* ```
|
|
737
830
|
*/
|
|
738
831
|
async existsById(id, tx) {
|
|
739
832
|
const ready = await this.ensureReady();
|
|
@@ -745,6 +838,7 @@ var BaseReldbCrudRepository = class {
|
|
|
745
838
|
};
|
|
746
839
|
|
|
747
840
|
// src/reldb-json.ts
|
|
841
|
+
var JSON_PATH_OBJECT_SEGMENT_REGEX = /^([^.[]+)/;
|
|
748
842
|
function parseJsonPath(path) {
|
|
749
843
|
let remaining = path.startsWith("$") ? path.slice(1) : path;
|
|
750
844
|
const segments = [];
|
|
@@ -763,7 +857,7 @@ function parseJsonPath(path) {
|
|
|
763
857
|
remaining = remaining.slice(1);
|
|
764
858
|
if (remaining.startsWith("["))
|
|
765
859
|
continue;
|
|
766
|
-
const match = remaining.match(
|
|
860
|
+
const match = remaining.match(JSON_PATH_OBJECT_SEGMENT_REGEX);
|
|
767
861
|
if (match) {
|
|
768
862
|
segments.push(match[1]);
|
|
769
863
|
remaining = remaining.slice(match[1].length);
|
|
@@ -808,6 +902,46 @@ function createSqliteJsonOps() {
|
|
|
808
902
|
}
|
|
809
903
|
};
|
|
810
904
|
}
|
|
905
|
+
function buildPostgresMergePatch(column, patch) {
|
|
906
|
+
const normalized = JSON.parse(JSON.stringify(patch));
|
|
907
|
+
if (!normalized || typeof normalized !== "object" || Array.isArray(normalized))
|
|
908
|
+
throw new TypeError(reldbM("reldb_jsonPatchObjectRequired"));
|
|
909
|
+
const steps = [];
|
|
910
|
+
const pending = Object.entries(normalized).reverse().map(([key, value]) => ({ path: [key], value }));
|
|
911
|
+
while (pending.length > 0) {
|
|
912
|
+
const entry = pending.pop();
|
|
913
|
+
const object = entry.value !== null && typeof entry.value === "object" && !Array.isArray(entry.value);
|
|
914
|
+
steps.push({ path: entry.path, value: object ? {} : entry.value, object });
|
|
915
|
+
if (object) {
|
|
916
|
+
for (const [key, value] of Object.entries(entry.value).reverse())
|
|
917
|
+
pending.push({ path: [...entry.path, key], value });
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
return {
|
|
921
|
+
sql: `(WITH RECURSIVE
|
|
922
|
+
patch_steps AS (
|
|
923
|
+
SELECT ordinality AS step,
|
|
924
|
+
ARRAY(SELECT jsonb_array_elements_text(item->'path')) AS path,
|
|
925
|
+
item->'value' AS value, (item->>'object')::boolean AS object
|
|
926
|
+
FROM jsonb_array_elements(?::jsonb) WITH ORDINALITY AS input(item, ordinality)
|
|
927
|
+
),
|
|
928
|
+
merged(step, document) AS (
|
|
929
|
+
SELECT 0::bigint, CASE WHEN jsonb_typeof(${column}) = 'object' THEN ${column} ELSE '{}'::jsonb END
|
|
930
|
+
UNION ALL
|
|
931
|
+
SELECT patch_steps.step,
|
|
932
|
+
CASE
|
|
933
|
+
WHEN patch_steps.object THEN jsonb_set(merged.document, patch_steps.path,
|
|
934
|
+
CASE WHEN jsonb_typeof(merged.document #> patch_steps.path) = 'object'
|
|
935
|
+
THEN merged.document #> patch_steps.path ELSE '{}'::jsonb END, true)
|
|
936
|
+
WHEN patch_steps.value = 'null'::jsonb THEN merged.document #- patch_steps.path
|
|
937
|
+
ELSE jsonb_set(merged.document, patch_steps.path, patch_steps.value, true)
|
|
938
|
+
END
|
|
939
|
+
FROM merged JOIN patch_steps ON patch_steps.step = merged.step + 1
|
|
940
|
+
)
|
|
941
|
+
SELECT document FROM merged ORDER BY step DESC LIMIT 1)`,
|
|
942
|
+
params: [JSON.stringify(steps)]
|
|
943
|
+
};
|
|
944
|
+
}
|
|
811
945
|
function createPostgresJsonOps() {
|
|
812
946
|
return {
|
|
813
947
|
extract(column, path) {
|
|
@@ -839,10 +973,7 @@ function createPostgresJsonOps() {
|
|
|
839
973
|
};
|
|
840
974
|
},
|
|
841
975
|
merge(column, patch) {
|
|
842
|
-
return
|
|
843
|
-
sql: `${column} || ?::jsonb`,
|
|
844
|
-
params: [JSON.stringify(patch)]
|
|
845
|
-
};
|
|
976
|
+
return buildPostgresMergePatch(column, patch);
|
|
846
977
|
}
|
|
847
978
|
};
|
|
848
979
|
}
|
|
@@ -977,6 +1108,7 @@ function escapeSqlString(value) {
|
|
|
977
1108
|
|
|
978
1109
|
// src/reldb-crud-kernel.ts
|
|
979
1110
|
var logger = core.logger.child({ module: "reldb", scope: "crud-kernel" });
|
|
1111
|
+
var ORDER_BY_SEGMENT_REGEX = /^(\w+)(?:\s+(ASC|DESC))?$/i;
|
|
980
1112
|
function buildSelectColumns(select) {
|
|
981
1113
|
if (!select || select.length === 0) {
|
|
982
1114
|
return "*";
|
|
@@ -1045,7 +1177,7 @@ function createCrud(ops, config) {
|
|
|
1045
1177
|
const parts = orderBy.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1046
1178
|
const safeParts = [];
|
|
1047
1179
|
for (const part of parts) {
|
|
1048
|
-
const match = part.match(
|
|
1180
|
+
const match = part.match(ORDER_BY_SEGMENT_REGEX);
|
|
1049
1181
|
if (!match) {
|
|
1050
1182
|
logger.debug("Skipped invalid orderBy segment", { table, segment: part });
|
|
1051
1183
|
continue;
|
|
@@ -1433,15 +1565,87 @@ function createTxWrap(beginTx) {
|
|
|
1433
1565
|
}
|
|
1434
1566
|
|
|
1435
1567
|
// src/providers/reldb-provider-base.ts
|
|
1568
|
+
function summarizeOperationPayload(payload) {
|
|
1569
|
+
try {
|
|
1570
|
+
if (!payload || typeof payload !== "object")
|
|
1571
|
+
return {};
|
|
1572
|
+
const fields = payload;
|
|
1573
|
+
return {
|
|
1574
|
+
...typeof fields.sql === "string" ? { sqlLength: fields.sql.length } : {},
|
|
1575
|
+
...Array.isArray(fields.params) ? { parameterCount: fields.params.length } : {},
|
|
1576
|
+
...Array.isArray(fields.statements) ? { statementCount: fields.statements.length } : {},
|
|
1577
|
+
...fields.columns && typeof fields.columns === "object" ? { columnCount: Object.keys(fields.columns).length } : {}
|
|
1578
|
+
};
|
|
1579
|
+
} catch {
|
|
1580
|
+
return {};
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
function logReldbOperation(ctx, category, operation, payload) {
|
|
1584
|
+
const config = ctx.operationLog?.();
|
|
1585
|
+
if (!config?.[category]) {
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
ctx.logger[config.level]("RelDB operation executed", {
|
|
1589
|
+
category,
|
|
1590
|
+
operation,
|
|
1591
|
+
payload: config.maxLength <= 0 ? "" : JSON.stringify(summarizeOperationPayload(payload)).slice(0, config.maxLength)
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1436
1594
|
async function wrapOp(ctx, fn, errorDef, errorLabel, errorMeta) {
|
|
1437
1595
|
if (!ctx.isConnected()) {
|
|
1438
1596
|
return err(HaiReldbError.NOT_INITIALIZED, reldbM("reldb_notInitialized"));
|
|
1439
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
|
+
};
|
|
1440
1644
|
try {
|
|
1441
|
-
|
|
1645
|
+
const result = await fn();
|
|
1646
|
+
return result.success ? result : failed(result.error.cause ?? result.error, result.error);
|
|
1442
1647
|
} catch (error) {
|
|
1443
|
-
|
|
1444
|
-
return err(errorDef, reldbM("reldb_queryFailed", { params: { error: String(error) } }), error);
|
|
1648
|
+
return failed(error);
|
|
1445
1649
|
}
|
|
1446
1650
|
}
|
|
1447
1651
|
function createBaseDdlOps(ctx, raw) {
|
|
@@ -1453,12 +1657,14 @@ function createBaseDdlOps(ctx, raw) {
|
|
|
1453
1657
|
const v2 = validateIdentifiers(Object.keys(columns));
|
|
1454
1658
|
if (!v2.success)
|
|
1455
1659
|
return Promise.resolve(v2);
|
|
1660
|
+
logReldbOperation(ctx, "write", "ddl.createTable", { tableName, columns, ifNotExists });
|
|
1456
1661
|
return wrapOp(ctx, () => raw.createTable(tableName, columns, ifNotExists), HaiReldbError.DDL_FAILED, "DDL: createTable failed", { tableName });
|
|
1457
1662
|
},
|
|
1458
1663
|
dropTable(tableName, ifExists = true) {
|
|
1459
1664
|
const v = validateIdentifier(tableName);
|
|
1460
1665
|
if (!v.success)
|
|
1461
1666
|
return Promise.resolve(v);
|
|
1667
|
+
logReldbOperation(ctx, "write", "ddl.dropTable", { tableName, ifExists });
|
|
1462
1668
|
return wrapOp(ctx, () => raw.dropTable(tableName, ifExists), HaiReldbError.DDL_FAILED, "DDL: dropTable failed", { tableName });
|
|
1463
1669
|
},
|
|
1464
1670
|
addColumn(tableName, columnName, columnDef) {
|
|
@@ -1468,6 +1674,7 @@ function createBaseDdlOps(ctx, raw) {
|
|
|
1468
1674
|
const v2 = validateIdentifier(columnName);
|
|
1469
1675
|
if (!v2.success)
|
|
1470
1676
|
return Promise.resolve(v2);
|
|
1677
|
+
logReldbOperation(ctx, "write", "ddl.addColumn", { tableName, columnName, columnDef });
|
|
1471
1678
|
return wrapOp(ctx, () => raw.addColumn(tableName, columnName, columnDef), HaiReldbError.DDL_FAILED, "DDL: addColumn failed", { tableName, columnName });
|
|
1472
1679
|
},
|
|
1473
1680
|
dropColumn(tableName, columnName) {
|
|
@@ -1477,6 +1684,7 @@ function createBaseDdlOps(ctx, raw) {
|
|
|
1477
1684
|
const v2 = validateIdentifier(columnName);
|
|
1478
1685
|
if (!v2.success)
|
|
1479
1686
|
return Promise.resolve(v2);
|
|
1687
|
+
logReldbOperation(ctx, "write", "ddl.dropColumn", { tableName, columnName });
|
|
1480
1688
|
return wrapOp(ctx, () => raw.dropColumn(tableName, columnName), HaiReldbError.DDL_FAILED, "DDL: dropColumn failed", { tableName, columnName });
|
|
1481
1689
|
},
|
|
1482
1690
|
renameTable(oldName, newName) {
|
|
@@ -1486,6 +1694,7 @@ function createBaseDdlOps(ctx, raw) {
|
|
|
1486
1694
|
const v2 = validateIdentifier(newName);
|
|
1487
1695
|
if (!v2.success)
|
|
1488
1696
|
return Promise.resolve(v2);
|
|
1697
|
+
logReldbOperation(ctx, "write", "ddl.renameTable", { oldName, newName });
|
|
1489
1698
|
return wrapOp(ctx, () => raw.renameTable(oldName, newName), HaiReldbError.DDL_FAILED, "DDL: renameTable failed", { oldName, newName });
|
|
1490
1699
|
},
|
|
1491
1700
|
createIndex(tableName, indexName, indexDef) {
|
|
@@ -1498,30 +1707,66 @@ function createBaseDdlOps(ctx, raw) {
|
|
|
1498
1707
|
const v3 = validateIdentifiers(indexDef.columns);
|
|
1499
1708
|
if (!v3.success)
|
|
1500
1709
|
return Promise.resolve(v3);
|
|
1710
|
+
logReldbOperation(ctx, "write", "ddl.createIndex", { tableName, indexName, indexDef });
|
|
1501
1711
|
return wrapOp(ctx, () => raw.createIndex(tableName, indexName, indexDef), HaiReldbError.DDL_FAILED, "DDL: createIndex failed", { tableName, indexName });
|
|
1502
1712
|
},
|
|
1503
1713
|
dropIndex(indexName, ifExists = true) {
|
|
1504
1714
|
const v = validateIdentifier(indexName);
|
|
1505
1715
|
if (!v.success)
|
|
1506
1716
|
return Promise.resolve(v);
|
|
1717
|
+
logReldbOperation(ctx, "write", "ddl.dropIndex", { indexName, ifExists });
|
|
1507
1718
|
return wrapOp(ctx, () => raw.dropIndex(indexName, ifExists), HaiReldbError.DDL_FAILED, "DDL: dropIndex failed", { indexName });
|
|
1508
1719
|
},
|
|
1509
1720
|
raw(sql) {
|
|
1510
|
-
|
|
1721
|
+
logReldbOperation(ctx, "write", "ddl.raw", { sql });
|
|
1722
|
+
return wrapOp(ctx, () => raw.raw(sql), HaiReldbError.DDL_FAILED, "DDL: raw failed", { sql });
|
|
1511
1723
|
}
|
|
1512
1724
|
};
|
|
1513
1725
|
}
|
|
1514
1726
|
function createBaseDmlOps(ctx, raw) {
|
|
1515
1727
|
return {
|
|
1516
|
-
query: (sql, params) =>
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1728
|
+
query: (sql, params) => {
|
|
1729
|
+
logReldbOperation(ctx, "read", "query", { sql, params });
|
|
1730
|
+
return wrapOp(ctx, () => raw.query(sql, params), HaiReldbError.QUERY_FAILED, "DML: query failed", { sql, params });
|
|
1731
|
+
},
|
|
1732
|
+
get: (sql, params) => {
|
|
1733
|
+
logReldbOperation(ctx, "read", "get", { sql, params });
|
|
1734
|
+
return wrapOp(ctx, () => raw.get(sql, params), HaiReldbError.QUERY_FAILED, "DML: get failed", { sql, params });
|
|
1735
|
+
},
|
|
1736
|
+
execute: (sql, params) => {
|
|
1737
|
+
logReldbOperation(ctx, "write", "execute", { sql, params });
|
|
1738
|
+
return wrapOp(ctx, () => raw.execute(sql, params), HaiReldbError.QUERY_FAILED, "DML: execute failed", { sql, params });
|
|
1739
|
+
},
|
|
1740
|
+
batch: (statements) => {
|
|
1741
|
+
logReldbOperation(ctx, "write", "batch", { count: statements.length, statements });
|
|
1742
|
+
return wrapOp(ctx, () => raw.batch(statements), HaiReldbError.QUERY_FAILED, "DML: batch failed", { statements });
|
|
1743
|
+
},
|
|
1744
|
+
queryPage: (options) => {
|
|
1745
|
+
logReldbOperation(ctx, "read", "queryPage", options);
|
|
1746
|
+
return wrapOp(ctx, () => raw.queryPage(options), HaiReldbError.QUERY_FAILED, "DML: queryPage failed", { ...options });
|
|
1747
|
+
}
|
|
1521
1748
|
};
|
|
1522
1749
|
}
|
|
1523
1750
|
function createBaseTxManager(ctx, beginTx) {
|
|
1524
|
-
const begin = () =>
|
|
1751
|
+
const begin = async () => {
|
|
1752
|
+
logReldbOperation(ctx, "write", "tx.begin", {});
|
|
1753
|
+
const result = await wrapOp(ctx, beginTx, HaiReldbError.TRANSACTION_FAILED, "TX: begin failed");
|
|
1754
|
+
if (!result.success) {
|
|
1755
|
+
return result;
|
|
1756
|
+
}
|
|
1757
|
+
const tx = result.data;
|
|
1758
|
+
return ok({
|
|
1759
|
+
...tx,
|
|
1760
|
+
commit: async () => {
|
|
1761
|
+
logReldbOperation(ctx, "write", "tx.commit", {});
|
|
1762
|
+
return tx.commit();
|
|
1763
|
+
},
|
|
1764
|
+
rollback: async () => {
|
|
1765
|
+
logReldbOperation(ctx, "write", "tx.rollback", {});
|
|
1766
|
+
return tx.rollback();
|
|
1767
|
+
}
|
|
1768
|
+
});
|
|
1769
|
+
};
|
|
1525
1770
|
return {
|
|
1526
1771
|
begin,
|
|
1527
1772
|
wrap: createTxWrap(begin)
|
|
@@ -1609,9 +1854,149 @@ function buildDefaultDropIndexSql(quoteId, indexName, ifExists) {
|
|
|
1609
1854
|
var logger2 = core.logger.child({ module: "reldb", scope: "mysql" });
|
|
1610
1855
|
function createMysqlProvider() {
|
|
1611
1856
|
let pool = null;
|
|
1857
|
+
let ready = false;
|
|
1858
|
+
let closing = null;
|
|
1859
|
+
let poolConfig = null;
|
|
1860
|
+
let activeSlots = 0;
|
|
1861
|
+
let slotsDrained = Promise.resolve();
|
|
1862
|
+
let resolveSlots = null;
|
|
1863
|
+
const waiters = [];
|
|
1864
|
+
let currentConfig2 = null;
|
|
1865
|
+
function releaseSlot() {
|
|
1866
|
+
activeSlots--;
|
|
1867
|
+
if (ready && poolConfig) {
|
|
1868
|
+
const maximum = poolConfig.max;
|
|
1869
|
+
while (waiters.length > 0) {
|
|
1870
|
+
if (activeSlots >= maximum)
|
|
1871
|
+
break;
|
|
1872
|
+
waiters.shift().start();
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
if (activeSlots === 0) {
|
|
1876
|
+
resolveSlots?.();
|
|
1877
|
+
resolveSlots = null;
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
function acquireConnection() {
|
|
1881
|
+
const owned = pool;
|
|
1882
|
+
const limits = poolConfig;
|
|
1883
|
+
if (!ready || !owned || !limits)
|
|
1884
|
+
return Promise.resolve(err(HaiReldbError.NOT_INITIALIZED, reldbM("reldb_notInitialized")));
|
|
1885
|
+
if (activeSlots >= limits.max && waiters.length >= limits.queueLimit)
|
|
1886
|
+
return Promise.resolve(err(HaiReldbError.POOL_EXHAUSTED, reldbM("reldb_mysqlPoolQueueFull")));
|
|
1887
|
+
const deadline = performance$1.now() + limits.acquireTimeout;
|
|
1888
|
+
return new Promise((resolve) => {
|
|
1889
|
+
let completed = false;
|
|
1890
|
+
let timer;
|
|
1891
|
+
const timeoutResult = () => err(HaiReldbError.TIMEOUT, reldbM("reldb_mysqlPoolAcquireTimeout"));
|
|
1892
|
+
const finish = (result) => {
|
|
1893
|
+
if (completed)
|
|
1894
|
+
return;
|
|
1895
|
+
completed = true;
|
|
1896
|
+
clearTimeout(timer);
|
|
1897
|
+
resolve(result);
|
|
1898
|
+
};
|
|
1899
|
+
const waiter = {
|
|
1900
|
+
cancel: () => finish(err(HaiReldbError.NOT_INITIALIZED, reldbM("reldb_notInitialized"))),
|
|
1901
|
+
start: () => {
|
|
1902
|
+
if (!ready || performance$1.now() >= deadline) {
|
|
1903
|
+
finish(ready ? timeoutResult() : err(HaiReldbError.NOT_INITIALIZED, reldbM("reldb_notInitialized")));
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
if (activeSlots++ === 0)
|
|
1907
|
+
slotsDrained = new Promise((resolve2) => {
|
|
1908
|
+
resolveSlots = resolve2;
|
|
1909
|
+
});
|
|
1910
|
+
void Promise.resolve().then(() => owned.getConnection()).then((connection) => {
|
|
1911
|
+
if (completed || !ready || performance$1.now() >= deadline) {
|
|
1912
|
+
try {
|
|
1913
|
+
connection.release();
|
|
1914
|
+
} finally {
|
|
1915
|
+
releaseSlot();
|
|
1916
|
+
}
|
|
1917
|
+
finish(ready ? timeoutResult() : err(HaiReldbError.NOT_INITIALIZED, reldbM("reldb_notInitialized")));
|
|
1918
|
+
return;
|
|
1919
|
+
}
|
|
1920
|
+
let released = false;
|
|
1921
|
+
finish(ok({
|
|
1922
|
+
connection,
|
|
1923
|
+
deadline,
|
|
1924
|
+
release: () => {
|
|
1925
|
+
if (released)
|
|
1926
|
+
return;
|
|
1927
|
+
released = true;
|
|
1928
|
+
try {
|
|
1929
|
+
connection.release();
|
|
1930
|
+
} finally {
|
|
1931
|
+
releaseSlot();
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
}));
|
|
1935
|
+
}, (error) => {
|
|
1936
|
+
releaseSlot();
|
|
1937
|
+
finish(err(HaiReldbError.CONNECTION_FAILED, reldbM("reldb_mysqlConnectionFailed", { params: { error: String(error) } }), error));
|
|
1938
|
+
}).catch((error) => {
|
|
1939
|
+
ready = false;
|
|
1940
|
+
finish(err(HaiReldbError.CONNECTION_FAILED, reldbM("reldb_mysqlConnectionFailed", { params: { error: reldbM("reldb_mysqlPoolReleaseFailed") } }), error));
|
|
1941
|
+
logger2.warn("MySQL connection release failed; pool requires cleanup");
|
|
1942
|
+
});
|
|
1943
|
+
}
|
|
1944
|
+
};
|
|
1945
|
+
timer = setTimeout(() => {
|
|
1946
|
+
const index = waiters.indexOf(waiter);
|
|
1947
|
+
if (index >= 0)
|
|
1948
|
+
waiters.splice(index, 1);
|
|
1949
|
+
finish(timeoutResult());
|
|
1950
|
+
}, limits.acquireTimeout);
|
|
1951
|
+
if (activeSlots < limits.max)
|
|
1952
|
+
waiter.start();
|
|
1953
|
+
else
|
|
1954
|
+
waiters.push(waiter);
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1957
|
+
async function withConnection(fn) {
|
|
1958
|
+
const acquired = await acquireConnection();
|
|
1959
|
+
if (!acquired.success)
|
|
1960
|
+
return acquired;
|
|
1961
|
+
try {
|
|
1962
|
+
if (!ready)
|
|
1963
|
+
return err(HaiReldbError.NOT_INITIALIZED, reldbM("reldb_notInitialized"));
|
|
1964
|
+
if (performance$1.now() >= acquired.data.deadline)
|
|
1965
|
+
return err(HaiReldbError.TIMEOUT, reldbM("reldb_mysqlPoolAcquireTimeout"));
|
|
1966
|
+
return await fn(acquired.data.connection);
|
|
1967
|
+
} finally {
|
|
1968
|
+
acquired.data.release();
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
function closeResource() {
|
|
1972
|
+
ready = false;
|
|
1973
|
+
currentConfig2 = null;
|
|
1974
|
+
for (const waiter of waiters.splice(0))
|
|
1975
|
+
waiter.cancel();
|
|
1976
|
+
if (closing)
|
|
1977
|
+
return closing;
|
|
1978
|
+
const owned = pool;
|
|
1979
|
+
if (!owned)
|
|
1980
|
+
return Promise.resolve(ok(void 0));
|
|
1981
|
+
closing = (async () => {
|
|
1982
|
+
try {
|
|
1983
|
+
await slotsDrained;
|
|
1984
|
+
await owned.end();
|
|
1985
|
+
pool = null;
|
|
1986
|
+
poolConfig = null;
|
|
1987
|
+
return ok(void 0);
|
|
1988
|
+
} catch (error) {
|
|
1989
|
+
return err(HaiReldbError.CONNECTION_FAILED, reldbM("reldb_mysqlConnectionFailed", { params: { error: String(error) } }), error);
|
|
1990
|
+
}
|
|
1991
|
+
})().finally(() => {
|
|
1992
|
+
closing = null;
|
|
1993
|
+
});
|
|
1994
|
+
return closing;
|
|
1995
|
+
}
|
|
1612
1996
|
const ctx = {
|
|
1613
|
-
isConnected: () => pool !== null,
|
|
1614
|
-
logger: logger2
|
|
1997
|
+
isConnected: () => ready && pool !== null,
|
|
1998
|
+
logger: logger2,
|
|
1999
|
+
operationLog: () => currentConfig2?.operationLog
|
|
1615
2000
|
};
|
|
1616
2001
|
function mysqlQuoteId(name) {
|
|
1617
2002
|
return `\`${name}\``;
|
|
@@ -1619,17 +2004,15 @@ function createMysqlProvider() {
|
|
|
1619
2004
|
function mysqlTxErrorMessage(detail) {
|
|
1620
2005
|
return reldbM("reldb_mysqlTxFailed", { params: { error: detail } });
|
|
1621
2006
|
}
|
|
1622
|
-
async function findIndexTableName(
|
|
1623
|
-
|
|
1624
|
-
const [rows] = await
|
|
2007
|
+
async function findIndexTableName(indexName) {
|
|
2008
|
+
return withConnection(async (connection) => {
|
|
2009
|
+
const [rows] = await connection.query(
|
|
1625
2010
|
"SELECT TABLE_NAME as name FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND INDEX_NAME = ? LIMIT 1",
|
|
1626
2011
|
[indexName]
|
|
1627
2012
|
);
|
|
1628
2013
|
const data = rows;
|
|
1629
2014
|
return ok(data[0]?.name ?? null);
|
|
1630
|
-
}
|
|
1631
|
-
return err(HaiReldbError.QUERY_FAILED, reldbM("reldb_queryFailed", { params: { error: String(error) } }), error);
|
|
1632
|
-
}
|
|
2015
|
+
});
|
|
1633
2016
|
}
|
|
1634
2017
|
function mapMysqlType(def) {
|
|
1635
2018
|
switch (def.type) {
|
|
@@ -1637,6 +2020,8 @@ function createMysqlProvider() {
|
|
|
1637
2020
|
return "VARCHAR(255)";
|
|
1638
2021
|
case "INTEGER":
|
|
1639
2022
|
return def.autoIncrement ? "BIGINT" : "INT";
|
|
2023
|
+
case "BIGINT":
|
|
2024
|
+
return "BIGINT";
|
|
1640
2025
|
case "REAL":
|
|
1641
2026
|
return "DOUBLE";
|
|
1642
2027
|
case "BLOB":
|
|
@@ -1706,39 +2091,39 @@ function createMysqlProvider() {
|
|
|
1706
2091
|
const ifNotExistsClause = ifNotExists ? "IF NOT EXISTS " : "";
|
|
1707
2092
|
return `CREATE TABLE ${ifNotExistsClause}${quotedTable} (${columnDefs.join(", ")}) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`;
|
|
1708
2093
|
}
|
|
2094
|
+
function executeDdl(sql) {
|
|
2095
|
+
return withConnection(async (connection) => {
|
|
2096
|
+
await connection.query(sql);
|
|
2097
|
+
return ok(void 0);
|
|
2098
|
+
});
|
|
2099
|
+
}
|
|
1709
2100
|
const rawDdl = {
|
|
1710
2101
|
async createTable(name, columns, ifNotExists = true) {
|
|
1711
2102
|
const quotedTable = mysqlQuoteId(name);
|
|
1712
2103
|
const sql = mysqlBuildCreateTableSql(quotedTable, columns, ifNotExists);
|
|
1713
|
-
|
|
1714
|
-
return ok(void 0);
|
|
2104
|
+
return executeDdl(sql);
|
|
1715
2105
|
},
|
|
1716
2106
|
async dropTable(name, ifExists) {
|
|
1717
2107
|
const ifExistsClause = ifExists ? "IF EXISTS " : "";
|
|
1718
|
-
|
|
1719
|
-
return ok(void 0);
|
|
2108
|
+
return executeDdl(`DROP TABLE ${ifExistsClause}${mysqlQuoteId(name)}`);
|
|
1720
2109
|
},
|
|
1721
2110
|
async addColumn(table, column, def) {
|
|
1722
2111
|
const colSql = mysqlBuildColumnSql(column, def);
|
|
1723
|
-
|
|
1724
|
-
return ok(void 0);
|
|
2112
|
+
return executeDdl(`ALTER TABLE ${mysqlQuoteId(table)} ADD COLUMN ${colSql}`);
|
|
1725
2113
|
},
|
|
1726
2114
|
async dropColumn(table, column) {
|
|
1727
|
-
|
|
1728
|
-
return ok(void 0);
|
|
2115
|
+
return executeDdl(`ALTER TABLE ${mysqlQuoteId(table)} DROP COLUMN ${mysqlQuoteId(column)}`);
|
|
1729
2116
|
},
|
|
1730
2117
|
async renameTable(oldName, newName) {
|
|
1731
|
-
|
|
1732
|
-
return ok(void 0);
|
|
2118
|
+
return executeDdl(`RENAME TABLE ${mysqlQuoteId(oldName)} TO ${mysqlQuoteId(newName)}`);
|
|
1733
2119
|
},
|
|
1734
2120
|
async createIndex(table, index, def) {
|
|
1735
2121
|
const uniqueClause = def.unique ? "UNIQUE " : "";
|
|
1736
2122
|
const columns = def.columns.map((c) => mysqlQuoteId(c)).join(", ");
|
|
1737
|
-
|
|
1738
|
-
return ok(void 0);
|
|
2123
|
+
return executeDdl(`CREATE ${uniqueClause}INDEX ${mysqlQuoteId(index)} ON ${mysqlQuoteId(table)} (${columns})`);
|
|
1739
2124
|
},
|
|
1740
2125
|
async dropIndex(index, ifExists) {
|
|
1741
|
-
const tableResult = await findIndexTableName(
|
|
2126
|
+
const tableResult = await findIndexTableName(index);
|
|
1742
2127
|
if (!tableResult.success)
|
|
1743
2128
|
return tableResult;
|
|
1744
2129
|
if (!tableResult.data) {
|
|
@@ -1746,58 +2131,58 @@ function createMysqlProvider() {
|
|
|
1746
2131
|
return ok(void 0);
|
|
1747
2132
|
return err(HaiReldbError.DDL_FAILED, reldbM("reldb_ddlFailed", { params: { error: `index not found: ${index}` } }));
|
|
1748
2133
|
}
|
|
1749
|
-
|
|
1750
|
-
return ok(void 0);
|
|
2134
|
+
return executeDdl(`DROP INDEX ${mysqlQuoteId(index)} ON ${mysqlQuoteId(tableResult.data)}`);
|
|
1751
2135
|
},
|
|
1752
2136
|
async raw(sql) {
|
|
1753
|
-
|
|
1754
|
-
return ok(void 0);
|
|
2137
|
+
return executeDdl(sql);
|
|
1755
2138
|
}
|
|
1756
2139
|
};
|
|
1757
2140
|
const rawDml = {
|
|
1758
2141
|
async query(sql, params) {
|
|
1759
|
-
|
|
1760
|
-
|
|
2142
|
+
return withConnection(async (connection) => {
|
|
2143
|
+
const [rows] = await connection.query(sql, params);
|
|
2144
|
+
return ok(rows);
|
|
2145
|
+
});
|
|
1761
2146
|
},
|
|
1762
2147
|
async get(sql, params) {
|
|
1763
|
-
|
|
1764
|
-
|
|
2148
|
+
return withConnection(async (connection) => {
|
|
2149
|
+
const [rows] = await connection.query(sql, params);
|
|
2150
|
+
return ok(rows[0] ?? null);
|
|
2151
|
+
});
|
|
1765
2152
|
},
|
|
1766
2153
|
async execute(sql, params) {
|
|
1767
|
-
|
|
1768
|
-
|
|
2154
|
+
return withConnection(async (connection) => {
|
|
2155
|
+
const [result] = await connection.execute({ sql, values: params });
|
|
2156
|
+
return ok({ changes: result.affectedRows, lastInsertRowid: result.insertId });
|
|
2157
|
+
});
|
|
1769
2158
|
},
|
|
1770
2159
|
async batch(statements) {
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
} catch (error) {
|
|
1781
|
-
if (connection) {
|
|
2160
|
+
return withConnection(async (connection) => {
|
|
2161
|
+
try {
|
|
2162
|
+
await connection.beginTransaction();
|
|
2163
|
+
for (const { sql: s, params } of statements) {
|
|
2164
|
+
await connection.execute({ sql: s, values: params });
|
|
2165
|
+
}
|
|
2166
|
+
await connection.commit();
|
|
2167
|
+
return ok(void 0);
|
|
2168
|
+
} catch (error) {
|
|
1782
2169
|
await connection.rollback().catch(() => {
|
|
1783
2170
|
});
|
|
2171
|
+
return err(HaiReldbError.QUERY_FAILED, reldbM("reldb_batchFailed", { params: { error: String(error) } }), error);
|
|
1784
2172
|
}
|
|
1785
|
-
|
|
1786
|
-
} finally {
|
|
1787
|
-
if (connection) {
|
|
1788
|
-
connection.release();
|
|
1789
|
-
}
|
|
1790
|
-
}
|
|
2173
|
+
});
|
|
1791
2174
|
},
|
|
1792
2175
|
async queryPage(options) {
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
2176
|
+
return withConnection(async (connection) => {
|
|
2177
|
+
const result = await queryPageAsync(
|
|
2178
|
+
async (sql, params) => {
|
|
2179
|
+
const [rows] = await connection.query(sql, params);
|
|
2180
|
+
return rows;
|
|
2181
|
+
},
|
|
2182
|
+
options
|
|
2183
|
+
);
|
|
2184
|
+
return ok(result);
|
|
2185
|
+
});
|
|
1801
2186
|
}
|
|
1802
2187
|
};
|
|
1803
2188
|
function createMysqlTxDmlOps(conn) {
|
|
@@ -1811,12 +2196,12 @@ function createMysqlProvider() {
|
|
|
1811
2196
|
return ok(rows[0] ?? null);
|
|
1812
2197
|
},
|
|
1813
2198
|
async execute(sql, params) {
|
|
1814
|
-
const [result] = await conn.execute(sql, params);
|
|
2199
|
+
const [result] = await conn.execute({ sql, values: params });
|
|
1815
2200
|
return ok({ changes: result.affectedRows, lastInsertRowid: result.insertId });
|
|
1816
2201
|
},
|
|
1817
2202
|
async batch(statements) {
|
|
1818
2203
|
for (const { sql: s, params } of statements) {
|
|
1819
|
-
await conn.execute(s, params);
|
|
2204
|
+
await conn.execute({ sql: s, values: params });
|
|
1820
2205
|
}
|
|
1821
2206
|
return ok(void 0);
|
|
1822
2207
|
},
|
|
@@ -1833,25 +2218,36 @@ function createMysqlProvider() {
|
|
|
1833
2218
|
};
|
|
1834
2219
|
}
|
|
1835
2220
|
async function beginTx() {
|
|
1836
|
-
|
|
2221
|
+
const acquired = await acquireConnection();
|
|
2222
|
+
if (!acquired.success)
|
|
2223
|
+
return acquired;
|
|
2224
|
+
let connection = acquired.data.connection;
|
|
1837
2225
|
try {
|
|
1838
|
-
|
|
2226
|
+
if (!ready || performance$1.now() >= acquired.data.deadline) {
|
|
2227
|
+
acquired.data.release();
|
|
2228
|
+
return ready ? err(HaiReldbError.TIMEOUT, reldbM("reldb_mysqlPoolAcquireTimeout")) : err(HaiReldbError.NOT_INITIALIZED, reldbM("reldb_notInitialized"));
|
|
2229
|
+
}
|
|
1839
2230
|
await connection.beginTransaction();
|
|
1840
2231
|
} catch (error) {
|
|
1841
|
-
|
|
1842
|
-
connection.release();
|
|
1843
|
-
}
|
|
2232
|
+
acquired.data.release();
|
|
1844
2233
|
return err(HaiReldbError.TRANSACTION_FAILED, mysqlTxErrorMessage(String(error)), error);
|
|
1845
2234
|
}
|
|
1846
2235
|
const txDmlOps = createMysqlTxDmlOps(connection);
|
|
1847
|
-
|
|
2236
|
+
const guardedTxDmlOps = createBaseDmlOps({ ...ctx, isConnected: () => connection !== null }, txDmlOps);
|
|
2237
|
+
return ok(createTxHandle(guardedTxDmlOps, {
|
|
1848
2238
|
commit: async () => {
|
|
1849
2239
|
await connection.commit();
|
|
1850
2240
|
},
|
|
1851
2241
|
rollback: async () => {
|
|
1852
2242
|
await connection.rollback();
|
|
1853
2243
|
},
|
|
1854
|
-
release: () =>
|
|
2244
|
+
release: () => {
|
|
2245
|
+
try {
|
|
2246
|
+
acquired.data.release();
|
|
2247
|
+
} finally {
|
|
2248
|
+
connection = null;
|
|
2249
|
+
}
|
|
2250
|
+
},
|
|
1855
2251
|
errorMessage: mysqlTxErrorMessage
|
|
1856
2252
|
}));
|
|
1857
2253
|
}
|
|
@@ -1861,42 +2257,44 @@ function createMysqlProvider() {
|
|
|
1861
2257
|
if (config.type !== "mysql") {
|
|
1862
2258
|
return err(HaiReldbError.UNSUPPORTED_TYPE, reldbM("reldb_mysqlOnlyMysql"));
|
|
1863
2259
|
}
|
|
2260
|
+
const previousCleanup = await closeResource();
|
|
2261
|
+
if (!previousCleanup.success)
|
|
2262
|
+
return previousCleanup;
|
|
2263
|
+
const host = config.url ? new URL(config.url).hostname : config.host;
|
|
2264
|
+
if (config.ssl && config.ssl.rejectUnauthorized && config.ssl.verifyIdentity && isIP(host.replace(/^\[|\]$/g, ""))) {
|
|
2265
|
+
return err(HaiReldbError.CONFIG_ERROR, reldbM("reldb_mysqlTlsDnsRequired"));
|
|
2266
|
+
}
|
|
2267
|
+
if (config.ssl && (!config.ssl.rejectUnauthorized || !config.ssl.verifyIdentity)) {
|
|
2268
|
+
logger2.warn("MySQL TLS server verification is explicitly disabled", {
|
|
2269
|
+
certificateVerification: config.ssl.rejectUnauthorized,
|
|
2270
|
+
hostnameVerification: config.ssl.rejectUnauthorized && config.ssl.verifyIdentity
|
|
2271
|
+
});
|
|
2272
|
+
}
|
|
1864
2273
|
try {
|
|
1865
|
-
const mysql =
|
|
2274
|
+
const mysql = createRequire(import.meta.url)("mysql2/promise");
|
|
2275
|
+
poolConfig = config.pool ?? MysqlPoolConfigSchema.parse({});
|
|
1866
2276
|
pool = mysql.createPool({
|
|
1867
|
-
|
|
1868
|
-
host: config.host,
|
|
1869
|
-
port: config.port,
|
|
1870
|
-
database: config.database,
|
|
1871
|
-
user: config.user,
|
|
1872
|
-
password: config.password,
|
|
2277
|
+
// mysql2 会用非空的分字段覆盖 URI;URL 模式不能传 schema 默认 host 或冲突字段。
|
|
2278
|
+
...config.url ? { uri: config.url } : { host: config.host, port: config.port, database: config.database, user: config.user, password: config.password },
|
|
1873
2279
|
ssl: config.ssl,
|
|
1874
|
-
connectionLimit:
|
|
1875
|
-
|
|
1876
|
-
|
|
2280
|
+
connectionLimit: poolConfig.max,
|
|
2281
|
+
// 排队由本 Provider 管理,超时项可直接移除;驱动绝不保留取消后的等待者。
|
|
2282
|
+
waitForConnections: false,
|
|
2283
|
+
connectTimeout: poolConfig.acquireTimeout,
|
|
1877
2284
|
charset: config.mysql?.charset ?? "utf8mb4"
|
|
1878
2285
|
});
|
|
1879
2286
|
await pool.query("SELECT 1");
|
|
1880
|
-
|
|
2287
|
+
ready = true;
|
|
2288
|
+
currentConfig2 = config;
|
|
2289
|
+
logger2.info("Connected to MySQL", config.url ? { target: core.sanitize.sanitizeUrl(config.url) } : { host: config.host, port: config.port, database: config.database });
|
|
1881
2290
|
return ok(void 0);
|
|
1882
2291
|
} catch (error) {
|
|
1883
|
-
|
|
1884
|
-
|
|
2292
|
+
const cleanup = await closeResource();
|
|
2293
|
+
const result = err(HaiReldbError.CONNECTION_FAILED, reldbM("reldb_mysqlConnectionFailed", { params: { error: String(error) } }), error);
|
|
2294
|
+
return cleanup.success || result.success ? result : err({ ...result.error, ext: { cleanupPending: true, cleanupError: cleanup.error } });
|
|
1885
2295
|
}
|
|
1886
2296
|
},
|
|
1887
|
-
|
|
1888
|
-
if (pool) {
|
|
1889
|
-
try {
|
|
1890
|
-
await pool.end();
|
|
1891
|
-
} catch (error) {
|
|
1892
|
-
pool = null;
|
|
1893
|
-
return err(HaiReldbError.CONNECTION_FAILED, reldbM("reldb_mysqlConnectionFailed", { params: { error: String(error) } }), error);
|
|
1894
|
-
}
|
|
1895
|
-
pool = null;
|
|
1896
|
-
logger2.info("Disconnected from MySQL");
|
|
1897
|
-
}
|
|
1898
|
-
return ok(void 0);
|
|
1899
|
-
},
|
|
2297
|
+
close: closeResource,
|
|
1900
2298
|
isConnected: () => ctx.isConnected(),
|
|
1901
2299
|
ddl: createBaseDdlOps(ctx, rawDdl),
|
|
1902
2300
|
sql: dmlOps,
|
|
@@ -1905,15 +2303,143 @@ function createMysqlProvider() {
|
|
|
1905
2303
|
};
|
|
1906
2304
|
}
|
|
1907
2305
|
var logger3 = core.logger.child({ module: "reldb", scope: "postgres" });
|
|
2306
|
+
var PG_DOLLAR_TAG_REGEX = /^\$([A-Z_]\w*)?\$/i;
|
|
1908
2307
|
function createPostgresProvider() {
|
|
1909
2308
|
let pool = null;
|
|
2309
|
+
let ready = false;
|
|
2310
|
+
let closing = null;
|
|
2311
|
+
let activeTransactions = 0;
|
|
2312
|
+
let transactionsDrained = Promise.resolve();
|
|
2313
|
+
let resolveTransactions = null;
|
|
2314
|
+
function retainTransaction() {
|
|
2315
|
+
if (activeTransactions++ === 0)
|
|
2316
|
+
transactionsDrained = new Promise((resolve) => {
|
|
2317
|
+
resolveTransactions = resolve;
|
|
2318
|
+
});
|
|
2319
|
+
return () => {
|
|
2320
|
+
if (--activeTransactions === 0) {
|
|
2321
|
+
resolveTransactions?.();
|
|
2322
|
+
resolveTransactions = null;
|
|
2323
|
+
}
|
|
2324
|
+
};
|
|
2325
|
+
}
|
|
2326
|
+
let currentConfig2 = null;
|
|
2327
|
+
function closeResource() {
|
|
2328
|
+
ready = false;
|
|
2329
|
+
currentConfig2 = null;
|
|
2330
|
+
if (closing)
|
|
2331
|
+
return closing;
|
|
2332
|
+
const owned = pool;
|
|
2333
|
+
if (!owned)
|
|
2334
|
+
return Promise.resolve(ok(void 0));
|
|
2335
|
+
closing = (async () => {
|
|
2336
|
+
try {
|
|
2337
|
+
await transactionsDrained;
|
|
2338
|
+
await owned.end();
|
|
2339
|
+
pool = null;
|
|
2340
|
+
return ok(void 0);
|
|
2341
|
+
} catch (error) {
|
|
2342
|
+
return err(HaiReldbError.CONNECTION_FAILED, reldbM("reldb_postgresConnectionFailed", { params: { error: String(error) } }), error);
|
|
2343
|
+
}
|
|
2344
|
+
})().finally(() => {
|
|
2345
|
+
closing = null;
|
|
2346
|
+
});
|
|
2347
|
+
return closing;
|
|
2348
|
+
}
|
|
1910
2349
|
const ctx = {
|
|
1911
|
-
isConnected: () => pool !== null,
|
|
1912
|
-
logger: logger3
|
|
2350
|
+
isConnected: () => ready && pool !== null,
|
|
2351
|
+
logger: logger3,
|
|
2352
|
+
operationLog: () => currentConfig2?.operationLog
|
|
1913
2353
|
};
|
|
1914
2354
|
function convertPlaceholders(sql) {
|
|
2355
|
+
let out = "";
|
|
1915
2356
|
let index = 0;
|
|
1916
|
-
|
|
2357
|
+
let i = 0;
|
|
2358
|
+
const len = sql.length;
|
|
2359
|
+
while (i < len) {
|
|
2360
|
+
const ch = sql[i];
|
|
2361
|
+
if (ch === "'") {
|
|
2362
|
+
out += ch;
|
|
2363
|
+
i++;
|
|
2364
|
+
while (i < len) {
|
|
2365
|
+
const c = sql[i];
|
|
2366
|
+
out += c;
|
|
2367
|
+
i++;
|
|
2368
|
+
if (c === "'") {
|
|
2369
|
+
if (sql[i] === "'") {
|
|
2370
|
+
out += sql[i];
|
|
2371
|
+
i++;
|
|
2372
|
+
continue;
|
|
2373
|
+
}
|
|
2374
|
+
break;
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
continue;
|
|
2378
|
+
}
|
|
2379
|
+
if (ch === '"') {
|
|
2380
|
+
out += ch;
|
|
2381
|
+
i++;
|
|
2382
|
+
while (i < len) {
|
|
2383
|
+
const c = sql[i];
|
|
2384
|
+
out += c;
|
|
2385
|
+
i++;
|
|
2386
|
+
if (c === '"') {
|
|
2387
|
+
if (sql[i] === '"') {
|
|
2388
|
+
out += sql[i];
|
|
2389
|
+
i++;
|
|
2390
|
+
continue;
|
|
2391
|
+
}
|
|
2392
|
+
break;
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
continue;
|
|
2396
|
+
}
|
|
2397
|
+
if (ch === "$") {
|
|
2398
|
+
const tagMatch = sql.slice(i).match(PG_DOLLAR_TAG_REGEX);
|
|
2399
|
+
if (tagMatch) {
|
|
2400
|
+
const tag = tagMatch[0];
|
|
2401
|
+
const end = sql.indexOf(tag, i + tag.length);
|
|
2402
|
+
if (end === -1) {
|
|
2403
|
+
out += sql.slice(i);
|
|
2404
|
+
i = len;
|
|
2405
|
+
} else {
|
|
2406
|
+
out += sql.slice(i, end + tag.length);
|
|
2407
|
+
i = end + tag.length;
|
|
2408
|
+
}
|
|
2409
|
+
continue;
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
if (ch === "-" && sql[i + 1] === "-") {
|
|
2413
|
+
const nl = sql.indexOf("\n", i);
|
|
2414
|
+
if (nl === -1) {
|
|
2415
|
+
out += sql.slice(i);
|
|
2416
|
+
i = len;
|
|
2417
|
+
} else {
|
|
2418
|
+
out += sql.slice(i, nl + 1);
|
|
2419
|
+
i = nl + 1;
|
|
2420
|
+
}
|
|
2421
|
+
continue;
|
|
2422
|
+
}
|
|
2423
|
+
if (ch === "/" && sql[i + 1] === "*") {
|
|
2424
|
+
const endIdx = sql.indexOf("*/", i + 2);
|
|
2425
|
+
if (endIdx === -1) {
|
|
2426
|
+
out += sql.slice(i);
|
|
2427
|
+
i = len;
|
|
2428
|
+
} else {
|
|
2429
|
+
out += sql.slice(i, endIdx + 2);
|
|
2430
|
+
i = endIdx + 2;
|
|
2431
|
+
}
|
|
2432
|
+
continue;
|
|
2433
|
+
}
|
|
2434
|
+
if (ch === "?") {
|
|
2435
|
+
out += `$${++index}`;
|
|
2436
|
+
i++;
|
|
2437
|
+
continue;
|
|
2438
|
+
}
|
|
2439
|
+
out += ch;
|
|
2440
|
+
i++;
|
|
2441
|
+
}
|
|
2442
|
+
return out;
|
|
1917
2443
|
}
|
|
1918
2444
|
function pgTxErrorMessage(detail) {
|
|
1919
2445
|
return reldbM("reldb_postgresTxFailed", { params: { error: detail } });
|
|
@@ -1924,6 +2450,8 @@ function createPostgresProvider() {
|
|
|
1924
2450
|
return "TEXT";
|
|
1925
2451
|
case "INTEGER":
|
|
1926
2452
|
return def.autoIncrement ? "BIGSERIAL" : "INTEGER";
|
|
2453
|
+
case "BIGINT":
|
|
2454
|
+
return "BIGINT";
|
|
1927
2455
|
case "REAL":
|
|
1928
2456
|
return "DOUBLE PRECISION";
|
|
1929
2457
|
case "BLOB":
|
|
@@ -1957,11 +2485,23 @@ function createPostgresProvider() {
|
|
|
1957
2485
|
const result = await queryFn(convertPlaceholders(sql), params);
|
|
1958
2486
|
return result.rows;
|
|
1959
2487
|
}
|
|
2488
|
+
async function executeStatement(sql, params) {
|
|
2489
|
+
try {
|
|
2490
|
+
return await pool.query(sql, params);
|
|
2491
|
+
} catch (error) {
|
|
2492
|
+
const kind = /^\s*CREATE\s+(TABLE|(?:UNIQUE\s+)?INDEX)\s+IF\s+NOT\s+EXISTS\s+/i.exec(sql)?.[1]?.toUpperCase();
|
|
2493
|
+
const single = !sql.trim().replace(/;$/, "").includes(";");
|
|
2494
|
+
const catalogConflict = typeof error === "object" && error !== null && "code" in error && (error.code === "23505" && "schema" in error && error.schema === "pg_catalog" && "constraint" in error && (error.constraint === "pg_class_relname_nsp_index" || kind === "TABLE" && error.constraint === "pg_type_typname_nsp_index") || kind === "TABLE" && error.code === "42710" && "routine" in error && (error.routine === "heap_create_with_catalog" || error.routine === "TypeCreate"));
|
|
2495
|
+
if (!kind || !single || !catalogConflict)
|
|
2496
|
+
throw error;
|
|
2497
|
+
return pool.query(sql, params);
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
1960
2500
|
const rawDdl = {
|
|
1961
2501
|
async createTable(name, columns, ifNotExists = true) {
|
|
1962
2502
|
const quotedTable = quoteIdentifier(name);
|
|
1963
2503
|
const sql = buildDefaultCreateTableSql(pgBuildColumnSql, quotedTable, columns, ifNotExists);
|
|
1964
|
-
await
|
|
2504
|
+
await executeStatement(sql);
|
|
1965
2505
|
return ok(void 0);
|
|
1966
2506
|
},
|
|
1967
2507
|
async dropTable(name, ifExists) {
|
|
@@ -1984,7 +2524,7 @@ function createPostgresProvider() {
|
|
|
1984
2524
|
},
|
|
1985
2525
|
async createIndex(table, index, def) {
|
|
1986
2526
|
const sql = buildDefaultCreateIndexSql(quoteIdentifier, quoteIdentifier(table), quoteIdentifier(index), def);
|
|
1987
|
-
await
|
|
2527
|
+
await executeStatement(sql);
|
|
1988
2528
|
return ok(void 0);
|
|
1989
2529
|
},
|
|
1990
2530
|
async dropIndex(index, ifExists = true) {
|
|
@@ -1993,7 +2533,7 @@ function createPostgresProvider() {
|
|
|
1993
2533
|
return ok(void 0);
|
|
1994
2534
|
},
|
|
1995
2535
|
async raw(sql) {
|
|
1996
|
-
await
|
|
2536
|
+
await executeStatement(sql);
|
|
1997
2537
|
return ok(void 0);
|
|
1998
2538
|
}
|
|
1999
2539
|
};
|
|
@@ -2007,7 +2547,7 @@ function createPostgresProvider() {
|
|
|
2007
2547
|
return ok(rows[0] ?? null);
|
|
2008
2548
|
},
|
|
2009
2549
|
async execute(sql, params) {
|
|
2010
|
-
const result = await
|
|
2550
|
+
const result = await executeStatement(convertPlaceholders(sql), params);
|
|
2011
2551
|
return ok({ changes: result.rowCount ?? 0 });
|
|
2012
2552
|
},
|
|
2013
2553
|
async batch(statements) {
|
|
@@ -2078,24 +2618,35 @@ function createPostgresProvider() {
|
|
|
2078
2618
|
}
|
|
2079
2619
|
async function beginTx() {
|
|
2080
2620
|
let client = null;
|
|
2621
|
+
const releaseOwnership = retainTransaction();
|
|
2081
2622
|
try {
|
|
2082
2623
|
client = await pool.connect();
|
|
2083
2624
|
await client.query("BEGIN");
|
|
2084
2625
|
} catch (error) {
|
|
2085
|
-
|
|
2086
|
-
client
|
|
2626
|
+
try {
|
|
2627
|
+
client?.release();
|
|
2628
|
+
} finally {
|
|
2629
|
+
releaseOwnership();
|
|
2087
2630
|
}
|
|
2088
2631
|
return err(HaiReldbError.TRANSACTION_FAILED, pgTxErrorMessage(String(error)), error);
|
|
2089
2632
|
}
|
|
2090
2633
|
const txDmlOps = createPgTxDmlOps(client);
|
|
2091
|
-
|
|
2634
|
+
const guardedTxDmlOps = createBaseDmlOps({ ...ctx, isConnected: () => client !== null }, txDmlOps);
|
|
2635
|
+
return ok(createTxHandle(guardedTxDmlOps, {
|
|
2092
2636
|
commit: async () => {
|
|
2093
2637
|
await client.query("COMMIT");
|
|
2094
2638
|
},
|
|
2095
2639
|
rollback: async () => {
|
|
2096
2640
|
await client.query("ROLLBACK");
|
|
2097
2641
|
},
|
|
2098
|
-
release: () =>
|
|
2642
|
+
release: () => {
|
|
2643
|
+
try {
|
|
2644
|
+
client.release();
|
|
2645
|
+
} finally {
|
|
2646
|
+
client = null;
|
|
2647
|
+
releaseOwnership();
|
|
2648
|
+
}
|
|
2649
|
+
},
|
|
2099
2650
|
errorMessage: pgTxErrorMessage
|
|
2100
2651
|
}));
|
|
2101
2652
|
}
|
|
@@ -2105,8 +2656,11 @@ function createPostgresProvider() {
|
|
|
2105
2656
|
if (config.type !== "postgresql") {
|
|
2106
2657
|
return err(HaiReldbError.UNSUPPORTED_TYPE, reldbM("reldb_postgresOnlyPostgresql"));
|
|
2107
2658
|
}
|
|
2659
|
+
const previousCleanup = await closeResource();
|
|
2660
|
+
if (!previousCleanup.success)
|
|
2661
|
+
return previousCleanup;
|
|
2108
2662
|
try {
|
|
2109
|
-
const { Pool } =
|
|
2663
|
+
const { Pool } = createRequire(import.meta.url)("pg");
|
|
2110
2664
|
pool = new Pool({
|
|
2111
2665
|
connectionString: config.url,
|
|
2112
2666
|
host: config.host,
|
|
@@ -2120,27 +2674,34 @@ function createPostgresProvider() {
|
|
|
2120
2674
|
idleTimeoutMillis: config.pool?.idleTimeout,
|
|
2121
2675
|
connectionTimeoutMillis: config.pool?.acquireTimeout
|
|
2122
2676
|
});
|
|
2123
|
-
|
|
2677
|
+
pool.on("error", (error) => {
|
|
2678
|
+
const code = error && typeof error === "object" && "code" in error && typeof error.code === "string" && /^[A-Z0-9_]{1,32}$/.test(error.code) ? error.code : void 0;
|
|
2679
|
+
logger3.warn("PostgreSQL idle connection error; pool will replace connection", {
|
|
2680
|
+
code,
|
|
2681
|
+
name: error instanceof Error ? "Error" : "UnknownError"
|
|
2682
|
+
});
|
|
2683
|
+
});
|
|
2684
|
+
for (let attempt = 0; ; attempt++) {
|
|
2685
|
+
try {
|
|
2686
|
+
await pool.query("SELECT 1");
|
|
2687
|
+
break;
|
|
2688
|
+
} catch (error) {
|
|
2689
|
+
if (attempt >= 2 || typeof error !== "object" || error === null || !("code" in error) || error.code !== "EAI_AGAIN")
|
|
2690
|
+
throw error;
|
|
2691
|
+
await setTimeout$1(250 * (attempt + 1));
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
ready = true;
|
|
2695
|
+
currentConfig2 = config;
|
|
2124
2696
|
logger3.info("Connected to PostgreSQL", { host: config.host, port: config.port, database: config.database });
|
|
2125
2697
|
return ok(void 0);
|
|
2126
2698
|
} catch (error) {
|
|
2127
|
-
|
|
2128
|
-
|
|
2699
|
+
const cleanup = await closeResource();
|
|
2700
|
+
const result = err(HaiReldbError.CONNECTION_FAILED, reldbM("reldb_postgresConnectionFailed", { params: { error: String(error) } }), error);
|
|
2701
|
+
return cleanup.success || result.success ? result : err({ ...result.error, ext: { cleanupPending: true, cleanupError: cleanup.error } });
|
|
2129
2702
|
}
|
|
2130
2703
|
},
|
|
2131
|
-
|
|
2132
|
-
if (pool) {
|
|
2133
|
-
try {
|
|
2134
|
-
await pool.end();
|
|
2135
|
-
} catch (error) {
|
|
2136
|
-
pool = null;
|
|
2137
|
-
return err(HaiReldbError.CONNECTION_FAILED, reldbM("reldb_postgresConnectionFailed", { params: { error: String(error) } }), error);
|
|
2138
|
-
}
|
|
2139
|
-
pool = null;
|
|
2140
|
-
logger3.info("Disconnected from PostgreSQL");
|
|
2141
|
-
}
|
|
2142
|
-
return ok(void 0);
|
|
2143
|
-
},
|
|
2704
|
+
close: closeResource,
|
|
2144
2705
|
isConnected: () => ctx.isConnected(),
|
|
2145
2706
|
ddl: createBaseDdlOps(ctx, rawDdl),
|
|
2146
2707
|
sql: dmlOps,
|
|
@@ -2149,10 +2710,234 @@ function createPostgresProvider() {
|
|
|
2149
2710
|
};
|
|
2150
2711
|
}
|
|
2151
2712
|
var require2 = createRequire(import.meta.url);
|
|
2713
|
+
function createSyncSqliteEngine(options) {
|
|
2714
|
+
const Database = require2("better-sqlite3");
|
|
2715
|
+
const db = new Database(options.database, { readonly: options.readonly });
|
|
2716
|
+
let initialized = false;
|
|
2717
|
+
return {
|
|
2718
|
+
async initialize() {
|
|
2719
|
+
if (!initialized && options.walMode !== false) {
|
|
2720
|
+
const deadline = performance.now() + 5e3;
|
|
2721
|
+
let contention;
|
|
2722
|
+
try {
|
|
2723
|
+
for (; ; ) {
|
|
2724
|
+
if (contention && performance.now() >= deadline)
|
|
2725
|
+
throw contention;
|
|
2726
|
+
db.pragma(`busy_timeout = ${Math.max(1, Math.ceil(deadline - performance.now()))}`);
|
|
2727
|
+
try {
|
|
2728
|
+
db.pragma("journal_mode = WAL");
|
|
2729
|
+
break;
|
|
2730
|
+
} catch (error) {
|
|
2731
|
+
if (!error || typeof error !== "object" || !("code" in error) || typeof error.code !== "string" || !/^SQLITE_BUSY(?:_|$)/.test(error.code) || performance.now() >= deadline) {
|
|
2732
|
+
throw error;
|
|
2733
|
+
}
|
|
2734
|
+
contention = error;
|
|
2735
|
+
await setTimeout$1(Math.min(25, Math.max(1, deadline - performance.now())));
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
} finally {
|
|
2739
|
+
db.pragma("busy_timeout = 5000");
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
initialized = true;
|
|
2743
|
+
},
|
|
2744
|
+
async all(sql, params) {
|
|
2745
|
+
const stmt = db.prepare(sql);
|
|
2746
|
+
return params ? stmt.all(...params) : stmt.all();
|
|
2747
|
+
},
|
|
2748
|
+
async get(sql, params) {
|
|
2749
|
+
const stmt = db.prepare(sql);
|
|
2750
|
+
return (params ? stmt.get(...params) : stmt.get()) ?? null;
|
|
2751
|
+
},
|
|
2752
|
+
async run(sql, params) {
|
|
2753
|
+
const stmt = db.prepare(sql);
|
|
2754
|
+
const result = params ? stmt.run(...params) : stmt.run();
|
|
2755
|
+
return { changes: result.changes, lastInsertRowid: result.lastInsertRowid };
|
|
2756
|
+
},
|
|
2757
|
+
async exec(sql) {
|
|
2758
|
+
db.exec(sql);
|
|
2759
|
+
},
|
|
2760
|
+
async batch(statements) {
|
|
2761
|
+
const tx = db.transaction((items) => {
|
|
2762
|
+
for (const item of items) {
|
|
2763
|
+
const stmt = db.prepare(item.sql);
|
|
2764
|
+
if (item.params)
|
|
2765
|
+
stmt.run(...item.params);
|
|
2766
|
+
else stmt.run();
|
|
2767
|
+
}
|
|
2768
|
+
});
|
|
2769
|
+
tx(statements);
|
|
2770
|
+
},
|
|
2771
|
+
async close() {
|
|
2772
|
+
initialized = false;
|
|
2773
|
+
if (db.open) {
|
|
2774
|
+
db.close();
|
|
2775
|
+
}
|
|
2776
|
+
},
|
|
2777
|
+
isOpen: () => initialized && db.open
|
|
2778
|
+
};
|
|
2779
|
+
}
|
|
2780
|
+
function resolveWorkerUrl() {
|
|
2781
|
+
const candidates = [
|
|
2782
|
+
// 源码 / 测试:引擎与 worker 同目录。
|
|
2783
|
+
new URL("./reldb-sqlite-worker.mjs", import.meta.url),
|
|
2784
|
+
// 打包:引擎被并入 dist/index.js,worker 复制到 dist/providers/sqlite/。
|
|
2785
|
+
new URL("./providers/sqlite/reldb-sqlite-worker.mjs", import.meta.url)
|
|
2786
|
+
];
|
|
2787
|
+
for (const candidate of candidates) {
|
|
2788
|
+
if (existsSync(candidate))
|
|
2789
|
+
return candidate;
|
|
2790
|
+
}
|
|
2791
|
+
return candidates[0];
|
|
2792
|
+
}
|
|
2793
|
+
function createWorkerSqliteEngine(options, connectTimeoutMs) {
|
|
2794
|
+
const worker = new Worker(resolveWorkerUrl(), {
|
|
2795
|
+
workerData: { database: options.database, readonly: options.readonly, walMode: options.walMode }
|
|
2796
|
+
});
|
|
2797
|
+
let seq = 0;
|
|
2798
|
+
let open = false;
|
|
2799
|
+
let exited = false;
|
|
2800
|
+
let closing = null;
|
|
2801
|
+
const pending = /* @__PURE__ */ new Map();
|
|
2802
|
+
function rejectAll(error) {
|
|
2803
|
+
for (const entry of pending.values())
|
|
2804
|
+
entry.reject(error);
|
|
2805
|
+
pending.clear();
|
|
2806
|
+
}
|
|
2807
|
+
worker.on("message", (msg) => {
|
|
2808
|
+
if (!("id" in msg))
|
|
2809
|
+
return;
|
|
2810
|
+
const entry = pending.get(msg.id);
|
|
2811
|
+
if (!entry)
|
|
2812
|
+
return;
|
|
2813
|
+
pending.delete(msg.id);
|
|
2814
|
+
if (msg.ok)
|
|
2815
|
+
entry.resolve(msg.result);
|
|
2816
|
+
else entry.reject(new Error(msg.error?.message ?? "sqlite worker error"));
|
|
2817
|
+
});
|
|
2818
|
+
worker.on("error", (error) => {
|
|
2819
|
+
open = false;
|
|
2820
|
+
rejectAll(error instanceof Error ? error : new Error(String(error)));
|
|
2821
|
+
});
|
|
2822
|
+
worker.on("exit", (code) => {
|
|
2823
|
+
exited = true;
|
|
2824
|
+
open = false;
|
|
2825
|
+
if (pending.size > 0)
|
|
2826
|
+
rejectAll(new Error(`sqlite worker exited unexpectedly with code ${code}`));
|
|
2827
|
+
});
|
|
2828
|
+
function initialize() {
|
|
2829
|
+
return new Promise((resolve, reject) => {
|
|
2830
|
+
const timer = setTimeout(() => {
|
|
2831
|
+
finish(new Error(`sqlite worker init timed out after ${connectTimeoutMs}ms`));
|
|
2832
|
+
}, connectTimeoutMs);
|
|
2833
|
+
function finish(error) {
|
|
2834
|
+
clearTimeout(timer);
|
|
2835
|
+
worker.off("message", onHandshake);
|
|
2836
|
+
worker.off("error", onError);
|
|
2837
|
+
worker.off("exit", onExit);
|
|
2838
|
+
if (error) {
|
|
2839
|
+
reject(error);
|
|
2840
|
+
} else {
|
|
2841
|
+
open = true;
|
|
2842
|
+
resolve();
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
function onError(error) {
|
|
2846
|
+
finish(error);
|
|
2847
|
+
}
|
|
2848
|
+
function onExit(code) {
|
|
2849
|
+
finish(new Error(`sqlite worker exited before ready with code ${code}`));
|
|
2850
|
+
}
|
|
2851
|
+
function onHandshake(msg) {
|
|
2852
|
+
if (msg?.type === "ready") {
|
|
2853
|
+
finish();
|
|
2854
|
+
} else if (msg?.type === "init-error") {
|
|
2855
|
+
finish(new Error(msg.error?.message ?? "sqlite worker init failed"));
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
worker.on("message", onHandshake);
|
|
2859
|
+
worker.once("error", onError);
|
|
2860
|
+
worker.once("exit", onExit);
|
|
2861
|
+
if (exited)
|
|
2862
|
+
finish(new Error("sqlite worker exited before initialization"));
|
|
2863
|
+
});
|
|
2864
|
+
}
|
|
2865
|
+
function post(op, payload) {
|
|
2866
|
+
if (!open)
|
|
2867
|
+
return Promise.reject(new Error("sqlite worker is not open"));
|
|
2868
|
+
const id = ++seq;
|
|
2869
|
+
return new Promise((resolve, reject) => {
|
|
2870
|
+
pending.set(id, { resolve, reject });
|
|
2871
|
+
worker.postMessage({ id, op, ...payload });
|
|
2872
|
+
});
|
|
2873
|
+
}
|
|
2874
|
+
return {
|
|
2875
|
+
initialize,
|
|
2876
|
+
// 边界反序列化:worker 通过结构化克隆回传,行/结果类型在此断言。
|
|
2877
|
+
async all(sql, params) {
|
|
2878
|
+
return await post("all", { sql, params });
|
|
2879
|
+
},
|
|
2880
|
+
async get(sql, params) {
|
|
2881
|
+
return await post("get", { sql, params });
|
|
2882
|
+
},
|
|
2883
|
+
async run(sql, params) {
|
|
2884
|
+
return await post("run", { sql, params });
|
|
2885
|
+
},
|
|
2886
|
+
async exec(sql) {
|
|
2887
|
+
await post("exec", { sql });
|
|
2888
|
+
},
|
|
2889
|
+
async batch(statements) {
|
|
2890
|
+
await post("batch", { statements });
|
|
2891
|
+
},
|
|
2892
|
+
close() {
|
|
2893
|
+
if (closing)
|
|
2894
|
+
return closing;
|
|
2895
|
+
const wasOpen = open;
|
|
2896
|
+
const gracefulClose = wasOpen ? post("close", {}) : Promise.resolve();
|
|
2897
|
+
open = false;
|
|
2898
|
+
closing = (async () => {
|
|
2899
|
+
await gracefulClose.catch(() => void 0);
|
|
2900
|
+
if (!exited)
|
|
2901
|
+
await worker.terminate();
|
|
2902
|
+
})().finally(() => {
|
|
2903
|
+
closing = null;
|
|
2904
|
+
});
|
|
2905
|
+
return closing;
|
|
2906
|
+
},
|
|
2907
|
+
isOpen: () => open
|
|
2908
|
+
};
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2911
|
+
// src/providers/reldb-provider-sqlite.ts
|
|
2152
2912
|
var logger4 = core.logger.child({ module: "reldb", scope: "sqlite" });
|
|
2153
2913
|
function createSqliteProvider() {
|
|
2154
|
-
let
|
|
2914
|
+
let engine = null;
|
|
2915
|
+
let ready = false;
|
|
2916
|
+
let closing = null;
|
|
2155
2917
|
let txChain = Promise.resolve();
|
|
2918
|
+
let currentConfig2 = null;
|
|
2919
|
+
function closeResource() {
|
|
2920
|
+
ready = false;
|
|
2921
|
+
currentConfig2 = null;
|
|
2922
|
+
if (closing)
|
|
2923
|
+
return closing;
|
|
2924
|
+
const owned = engine;
|
|
2925
|
+
if (!owned)
|
|
2926
|
+
return Promise.resolve(ok(void 0));
|
|
2927
|
+
closing = (async () => {
|
|
2928
|
+
try {
|
|
2929
|
+
await txChain.catch(() => void 0);
|
|
2930
|
+
await owned.close();
|
|
2931
|
+
engine = null;
|
|
2932
|
+
return ok(void 0);
|
|
2933
|
+
} catch (error) {
|
|
2934
|
+
return err(HaiReldbError.CONNECTION_FAILED, reldbM("reldb_sqliteConnectionFailed", { params: { error: String(error) } }), error);
|
|
2935
|
+
}
|
|
2936
|
+
})().finally(() => {
|
|
2937
|
+
closing = null;
|
|
2938
|
+
});
|
|
2939
|
+
return closing;
|
|
2940
|
+
}
|
|
2156
2941
|
const TX_LOCK_TIMEOUT_MS = 3e4;
|
|
2157
2942
|
async function acquireTxLock() {
|
|
2158
2943
|
let release;
|
|
@@ -2178,8 +2963,9 @@ function createSqliteProvider() {
|
|
|
2178
2963
|
return release;
|
|
2179
2964
|
}
|
|
2180
2965
|
const ctx = {
|
|
2181
|
-
isConnected: () =>
|
|
2182
|
-
logger: logger4
|
|
2966
|
+
isConnected: () => ready && engine !== null && engine.isOpen(),
|
|
2967
|
+
logger: logger4,
|
|
2968
|
+
operationLog: () => currentConfig2?.operationLog
|
|
2183
2969
|
};
|
|
2184
2970
|
function mapSqliteType(def) {
|
|
2185
2971
|
switch (def.type) {
|
|
@@ -2187,6 +2973,7 @@ function createSqliteProvider() {
|
|
|
2187
2973
|
case "JSON":
|
|
2188
2974
|
return "TEXT";
|
|
2189
2975
|
case "INTEGER":
|
|
2976
|
+
case "BIGINT":
|
|
2190
2977
|
case "BOOLEAN":
|
|
2191
2978
|
return "INTEGER";
|
|
2192
2979
|
case "REAL":
|
|
@@ -2220,71 +3007,59 @@ function createSqliteProvider() {
|
|
|
2220
3007
|
async createTable(name, columns, ifNotExists = true) {
|
|
2221
3008
|
const quotedTable = quoteIdentifier(name);
|
|
2222
3009
|
const sql = buildDefaultCreateTableSql(sqliteBuildColumnSql, quotedTable, columns, ifNotExists);
|
|
2223
|
-
|
|
3010
|
+
await engine.exec(sql);
|
|
2224
3011
|
return ok(void 0);
|
|
2225
3012
|
},
|
|
2226
3013
|
async dropTable(name, ifExists) {
|
|
2227
3014
|
const ifExistsClause = ifExists ? "IF EXISTS " : "";
|
|
2228
|
-
|
|
3015
|
+
await engine.exec(`DROP TABLE ${ifExistsClause}${quoteIdentifier(name)}`);
|
|
2229
3016
|
return ok(void 0);
|
|
2230
3017
|
},
|
|
2231
3018
|
async addColumn(table, column, def) {
|
|
2232
3019
|
const colSql = sqliteBuildColumnSql(column, def);
|
|
2233
|
-
|
|
3020
|
+
await engine.exec(`ALTER TABLE ${quoteIdentifier(table)} ADD COLUMN ${colSql}`);
|
|
2234
3021
|
return ok(void 0);
|
|
2235
3022
|
},
|
|
2236
3023
|
async dropColumn(table, column) {
|
|
2237
|
-
|
|
3024
|
+
await engine.exec(`ALTER TABLE ${quoteIdentifier(table)} DROP COLUMN ${quoteIdentifier(column)}`);
|
|
2238
3025
|
return ok(void 0);
|
|
2239
3026
|
},
|
|
2240
3027
|
async renameTable(oldName, newName) {
|
|
2241
|
-
|
|
3028
|
+
await engine.exec(buildDefaultRenameTableSql(quoteIdentifier(oldName), quoteIdentifier(newName)));
|
|
2242
3029
|
return ok(void 0);
|
|
2243
3030
|
},
|
|
2244
3031
|
async createIndex(table, index, def) {
|
|
2245
3032
|
const sql = buildDefaultCreateIndexSql(quoteIdentifier, quoteIdentifier(table), quoteIdentifier(index), def);
|
|
2246
|
-
|
|
3033
|
+
await engine.exec(sql);
|
|
2247
3034
|
return ok(void 0);
|
|
2248
3035
|
},
|
|
2249
3036
|
async dropIndex(index, ifExists = true) {
|
|
2250
3037
|
const sql = buildDefaultDropIndexSql(quoteIdentifier, index, ifExists);
|
|
2251
|
-
|
|
3038
|
+
await engine.exec(sql);
|
|
2252
3039
|
return ok(void 0);
|
|
2253
3040
|
},
|
|
2254
3041
|
async raw(sql) {
|
|
2255
|
-
|
|
3042
|
+
await engine.exec(sql);
|
|
2256
3043
|
return ok(void 0);
|
|
2257
3044
|
}
|
|
2258
3045
|
};
|
|
2259
3046
|
const rawDml = {
|
|
2260
3047
|
async query(sql, params) {
|
|
2261
|
-
const
|
|
2262
|
-
const rows = params ? stmt.all(...params) : stmt.all();
|
|
3048
|
+
const rows = await engine.all(sql, params);
|
|
2263
3049
|
return ok(rows);
|
|
2264
3050
|
},
|
|
2265
3051
|
async get(sql, params) {
|
|
2266
|
-
const
|
|
2267
|
-
const row = params ? stmt.get(...params) : stmt.get();
|
|
3052
|
+
const row = await engine.get(sql, params);
|
|
2268
3053
|
return ok(row ?? null);
|
|
2269
3054
|
},
|
|
2270
3055
|
async execute(sql, params) {
|
|
2271
|
-
const
|
|
2272
|
-
|
|
2273
|
-
return ok({ changes: result.changes, lastInsertRowid: result.lastInsertRowid });
|
|
3056
|
+
const result = await engine.run(sql, params);
|
|
3057
|
+
return ok(result);
|
|
2274
3058
|
},
|
|
2275
3059
|
async batch(statements) {
|
|
2276
3060
|
const releaseTxLock = await acquireTxLock();
|
|
2277
3061
|
try {
|
|
2278
|
-
|
|
2279
|
-
const transaction = db.transaction(() => {
|
|
2280
|
-
for (const { sql: s, params } of statements) {
|
|
2281
|
-
const stmt = db.prepare(s);
|
|
2282
|
-
if (params)
|
|
2283
|
-
stmt.run(...params);
|
|
2284
|
-
else stmt.run();
|
|
2285
|
-
}
|
|
2286
|
-
});
|
|
2287
|
-
transaction();
|
|
3062
|
+
await engine.batch(statements);
|
|
2288
3063
|
return ok(void 0);
|
|
2289
3064
|
} catch (error) {
|
|
2290
3065
|
return err(HaiReldbError.QUERY_FAILED, reldbM("reldb_sqliteBatchFailed", { params: { error: String(error) } }), error);
|
|
@@ -2294,45 +3069,32 @@ function createSqliteProvider() {
|
|
|
2294
3069
|
},
|
|
2295
3070
|
async queryPage(options) {
|
|
2296
3071
|
const result = await queryPageAsync(
|
|
2297
|
-
|
|
2298
|
-
const stmt = database.prepare(sqlStr);
|
|
2299
|
-
return params ? stmt.all(...params) : stmt.all();
|
|
2300
|
-
},
|
|
3072
|
+
(sqlStr, params) => engine.all(sqlStr, params),
|
|
2301
3073
|
options
|
|
2302
3074
|
);
|
|
2303
3075
|
return ok(result);
|
|
2304
3076
|
}
|
|
2305
3077
|
};
|
|
2306
|
-
function createSqliteTxDmlOps(
|
|
3078
|
+
function createSqliteTxDmlOps(txEngine) {
|
|
2307
3079
|
return {
|
|
2308
3080
|
async query(sql, params) {
|
|
2309
|
-
|
|
2310
|
-
return ok(params ? stmt.all(...params) : stmt.all());
|
|
3081
|
+
return ok(await txEngine.all(sql, params));
|
|
2311
3082
|
},
|
|
2312
3083
|
async get(sql, params) {
|
|
2313
|
-
|
|
2314
|
-
return ok((params ? stmt.get(...params) : stmt.get()) ?? null);
|
|
3084
|
+
return ok(await txEngine.get(sql, params) ?? null);
|
|
2315
3085
|
},
|
|
2316
3086
|
async execute(sql, params) {
|
|
2317
|
-
|
|
2318
|
-
const result = params ? stmt.run(...params) : stmt.run();
|
|
2319
|
-
return ok({ changes: result.changes, lastInsertRowid: result.lastInsertRowid });
|
|
3087
|
+
return ok(await txEngine.run(sql, params));
|
|
2320
3088
|
},
|
|
2321
3089
|
async batch(statements) {
|
|
2322
3090
|
for (const { sql: s, params } of statements) {
|
|
2323
|
-
|
|
2324
|
-
if (params)
|
|
2325
|
-
stmt.run(...params);
|
|
2326
|
-
else stmt.run();
|
|
3091
|
+
await txEngine.run(s, params);
|
|
2327
3092
|
}
|
|
2328
3093
|
return ok(void 0);
|
|
2329
3094
|
},
|
|
2330
3095
|
async queryPage(options) {
|
|
2331
3096
|
const result = await queryPageAsync(
|
|
2332
|
-
|
|
2333
|
-
const stmt = db.prepare(sqlStr);
|
|
2334
|
-
return params ? stmt.all(...params) : stmt.all();
|
|
2335
|
-
},
|
|
3097
|
+
(sqlStr, params) => txEngine.all(sqlStr, params),
|
|
2336
3098
|
options
|
|
2337
3099
|
);
|
|
2338
3100
|
return ok(result);
|
|
@@ -2340,7 +3102,7 @@ function createSqliteProvider() {
|
|
|
2340
3102
|
};
|
|
2341
3103
|
}
|
|
2342
3104
|
async function beginTx() {
|
|
2343
|
-
const
|
|
3105
|
+
const txEngine = engine;
|
|
2344
3106
|
let released = false;
|
|
2345
3107
|
const releaseTxLock = await acquireTxLock();
|
|
2346
3108
|
const finishTransaction = () => {
|
|
@@ -2350,18 +3112,19 @@ function createSqliteProvider() {
|
|
|
2350
3112
|
}
|
|
2351
3113
|
};
|
|
2352
3114
|
try {
|
|
2353
|
-
|
|
3115
|
+
await txEngine.exec("BEGIN TRANSACTION");
|
|
2354
3116
|
} catch (error) {
|
|
2355
3117
|
finishTransaction();
|
|
2356
3118
|
return err(HaiReldbError.TRANSACTION_FAILED, sqliteTxErrorMessage(String(error)), error);
|
|
2357
3119
|
}
|
|
2358
|
-
const txDmlOps = createSqliteTxDmlOps(
|
|
2359
|
-
|
|
3120
|
+
const txDmlOps = createSqliteTxDmlOps(txEngine);
|
|
3121
|
+
const guardedTxDmlOps = createBaseDmlOps({ ...ctx, isConnected: () => txEngine.isOpen() }, txDmlOps);
|
|
3122
|
+
return ok(createTxHandle(guardedTxDmlOps, {
|
|
2360
3123
|
commit: async () => {
|
|
2361
|
-
|
|
3124
|
+
await txEngine.exec("COMMIT");
|
|
2362
3125
|
},
|
|
2363
3126
|
rollback: async () => {
|
|
2364
|
-
|
|
3127
|
+
await txEngine.exec("ROLLBACK");
|
|
2365
3128
|
},
|
|
2366
3129
|
release: () => finishTransaction(),
|
|
2367
3130
|
errorMessage: sqliteTxErrorMessage
|
|
@@ -2376,38 +3139,35 @@ function createSqliteProvider() {
|
|
|
2376
3139
|
if (!config.database) {
|
|
2377
3140
|
return err(HaiReldbError.CONFIG_ERROR, reldbM("reldb_sqliteNeedPath"));
|
|
2378
3141
|
}
|
|
3142
|
+
const previousCleanup = await closeResource();
|
|
3143
|
+
if (!previousCleanup.success)
|
|
3144
|
+
return previousCleanup;
|
|
2379
3145
|
try {
|
|
2380
|
-
const Database = require2("better-sqlite3");
|
|
2381
3146
|
const sqliteOptions = {
|
|
2382
3147
|
walMode: true,
|
|
2383
3148
|
readonly: false,
|
|
3149
|
+
executor: "sync",
|
|
3150
|
+
workerConnectTimeoutMs: 1e4,
|
|
2384
3151
|
...config.sqlite ?? {}
|
|
2385
3152
|
};
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
3153
|
+
const engineOptions = {
|
|
3154
|
+
database: config.database,
|
|
3155
|
+
readonly: sqliteOptions.readonly ?? false,
|
|
3156
|
+
walMode: sqliteOptions.walMode !== false
|
|
3157
|
+
};
|
|
3158
|
+
engine = sqliteOptions.executor === "worker" ? createWorkerSqliteEngine(engineOptions, sqliteOptions.workerConnectTimeoutMs ?? 1e4) : createSyncSqliteEngine(engineOptions);
|
|
3159
|
+
await engine.initialize();
|
|
3160
|
+
ready = true;
|
|
3161
|
+
currentConfig2 = config;
|
|
3162
|
+
logger4.info("Connected to SQLite", { database: config.database, executor: sqliteOptions.executor });
|
|
2393
3163
|
return ok(void 0);
|
|
2394
3164
|
} catch (error) {
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
async close() {
|
|
2399
|
-
if (database) {
|
|
2400
|
-
try {
|
|
2401
|
-
database.close();
|
|
2402
|
-
} catch (error) {
|
|
2403
|
-
database = null;
|
|
2404
|
-
return err(HaiReldbError.CONNECTION_FAILED, reldbM("reldb_sqliteConnectionFailed", { params: { error: String(error) } }), error);
|
|
2405
|
-
}
|
|
2406
|
-
database = null;
|
|
2407
|
-
logger4.info("Disconnected from SQLite");
|
|
3165
|
+
const cleanup = await closeResource();
|
|
3166
|
+
const result = err(HaiReldbError.CONNECTION_FAILED, reldbM("reldb_sqliteConnectionFailed", { params: { error: String(error) } }), error);
|
|
3167
|
+
return cleanup.success || result.success ? result : err({ ...result.error, ext: { cleanupPending: true, cleanupError: cleanup.error } });
|
|
2408
3168
|
}
|
|
2409
|
-
return ok(void 0);
|
|
2410
3169
|
},
|
|
3170
|
+
close: closeResource,
|
|
2411
3171
|
isConnected: () => ctx.isConnected(),
|
|
2412
3172
|
ddl: createBaseDdlOps(ctx, rawDdl),
|
|
2413
3173
|
sql: dmlOps,
|
|
@@ -2422,6 +3182,43 @@ var currentProvider = null;
|
|
|
2422
3182
|
var currentConfig = null;
|
|
2423
3183
|
var currentJsonOps = null;
|
|
2424
3184
|
var initInProgress = false;
|
|
3185
|
+
var pendingProvider = null;
|
|
3186
|
+
var initCompletion = null;
|
|
3187
|
+
var closingPromise = null;
|
|
3188
|
+
async function closeOwnedProvider() {
|
|
3189
|
+
if (currentProvider) {
|
|
3190
|
+
pendingProvider = currentProvider;
|
|
3191
|
+
currentProvider = null;
|
|
3192
|
+
}
|
|
3193
|
+
currentConfig = null;
|
|
3194
|
+
currentJsonOps = null;
|
|
3195
|
+
if (!pendingProvider)
|
|
3196
|
+
return ok(void 0);
|
|
3197
|
+
try {
|
|
3198
|
+
const result = await pendingProvider.close();
|
|
3199
|
+
if (!result.success)
|
|
3200
|
+
return err({ ...result.error, ext: { ...result.error.ext, cleanupPending: true } });
|
|
3201
|
+
pendingProvider = null;
|
|
3202
|
+
return ok(void 0);
|
|
3203
|
+
} catch (error) {
|
|
3204
|
+
logger5.error("Provider cleanup failed; retaining resource for retry", { error });
|
|
3205
|
+
return err({ ...HaiReldbError.CONNECTION_FAILED, message: reldbM("reldb_closeFailed", {
|
|
3206
|
+
params: { error: error instanceof Error ? error.message : String(error) }
|
|
3207
|
+
}), cause: error, ext: { cleanupPending: true } });
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
async function failInitialization(result) {
|
|
3211
|
+
const cleanup = await closeOwnedProvider();
|
|
3212
|
+
if (result.success)
|
|
3213
|
+
return result;
|
|
3214
|
+
if (cleanup.success) {
|
|
3215
|
+
const ext = { ...result.error.ext };
|
|
3216
|
+
delete ext.cleanupPending;
|
|
3217
|
+
delete ext.cleanupError;
|
|
3218
|
+
return err({ ...result.error, ext: Object.keys(ext).length > 0 ? ext : void 0 });
|
|
3219
|
+
}
|
|
3220
|
+
return err({ ...result.error, ext: { ...result.error.ext, cleanupPending: true, cleanupError: cleanup.error } });
|
|
3221
|
+
}
|
|
2425
3222
|
function createProvider(config) {
|
|
2426
3223
|
switch (config.type) {
|
|
2427
3224
|
case "sqlite":
|
|
@@ -2450,18 +3247,29 @@ var reldb = {
|
|
|
2450
3247
|
*
|
|
2451
3248
|
* @param config - 数据库配置(允许部分字段,内部会补齐默认值)
|
|
2452
3249
|
* @returns 初始化结果,失败时包含错误信息
|
|
3250
|
+
* @example
|
|
3251
|
+
* ```ts
|
|
3252
|
+
* import { reldb } from '@h-ai/reldb'
|
|
3253
|
+
*
|
|
3254
|
+
* const result = await reldb.init({ type: 'sqlite', database: ':memory:' })
|
|
3255
|
+
* if (!result.success) throw new Error(result.error.message)
|
|
3256
|
+
* void result.data
|
|
3257
|
+
* ```
|
|
2453
3258
|
*/
|
|
2454
3259
|
async init(config) {
|
|
2455
|
-
if (initInProgress) {
|
|
3260
|
+
if (initInProgress || closingPromise) {
|
|
2456
3261
|
logger5.warn("RelDB module init is already in progress, skipping concurrent call");
|
|
2457
3262
|
return err(HaiReldbError.CONFIG_ERROR, reldbM("reldb_configError", { params: { error: "init already in progress" } }));
|
|
2458
3263
|
}
|
|
2459
3264
|
initInProgress = true;
|
|
3265
|
+
let resolveInit;
|
|
3266
|
+
initCompletion = new Promise((resolve) => {
|
|
3267
|
+
resolveInit = resolve;
|
|
3268
|
+
});
|
|
2460
3269
|
try {
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
}
|
|
3270
|
+
const cleanup = await closeOwnedProvider();
|
|
3271
|
+
if (!cleanup.success)
|
|
3272
|
+
return cleanup;
|
|
2465
3273
|
logger5.info("Initializing RelDB module");
|
|
2466
3274
|
const parseResult = ReldbConfigSchema.safeParse(config);
|
|
2467
3275
|
if (!parseResult.success) {
|
|
@@ -2469,16 +3277,20 @@ var reldb = {
|
|
|
2469
3277
|
return err(HaiReldbError.CONFIG_ERROR, reldbM("reldb_configError", { params: { error: parseResult.error.message } }), parseResult.error);
|
|
2470
3278
|
}
|
|
2471
3279
|
const parsed = parseResult.data;
|
|
3280
|
+
if (core.module.requiresSharedState(process.env) && parsed.type === "sqlite")
|
|
3281
|
+
return err(HaiReldbError.CONFIG_ERROR, reldbM("reldb_sharedBackendRequired"));
|
|
2472
3282
|
try {
|
|
2473
3283
|
const provider = createProvider(parsed);
|
|
3284
|
+
pendingProvider = provider;
|
|
2474
3285
|
const connectResult = await provider.connect(parsed);
|
|
2475
3286
|
if (!connectResult.success) {
|
|
2476
3287
|
logger5.error("RelDB module initialization failed", {
|
|
2477
3288
|
code: connectResult.error.code,
|
|
2478
3289
|
message: connectResult.error.message
|
|
2479
3290
|
});
|
|
2480
|
-
return connectResult;
|
|
3291
|
+
return await failInitialization(connectResult);
|
|
2481
3292
|
}
|
|
3293
|
+
pendingProvider = null;
|
|
2482
3294
|
currentProvider = provider;
|
|
2483
3295
|
currentConfig = parsed;
|
|
2484
3296
|
currentJsonOps = createJsonOps(parsed.type);
|
|
@@ -2486,10 +3298,12 @@ var reldb = {
|
|
|
2486
3298
|
return ok(void 0);
|
|
2487
3299
|
} catch (error) {
|
|
2488
3300
|
logger5.error("RelDB module initialization failed", { error });
|
|
2489
|
-
return err(HaiReldbError.CONNECTION_FAILED, reldbM("reldb_initFailed", { params: { error: error instanceof Error ? error.message : String(error) } }), error);
|
|
3301
|
+
return await failInitialization(err(HaiReldbError.CONNECTION_FAILED, reldbM("reldb_initFailed", { params: { error: error instanceof Error ? error.message : String(error) } }), error));
|
|
2490
3302
|
}
|
|
2491
3303
|
} finally {
|
|
2492
3304
|
initInProgress = false;
|
|
3305
|
+
initCompletion = null;
|
|
3306
|
+
resolveInit();
|
|
2493
3307
|
}
|
|
2494
3308
|
},
|
|
2495
3309
|
/**
|
|
@@ -2521,9 +3335,14 @@ var reldb = {
|
|
|
2521
3335
|
get tx() {
|
|
2522
3336
|
return currentProvider?.tx ?? notInitializedTx;
|
|
2523
3337
|
},
|
|
2524
|
-
/**
|
|
3338
|
+
/** 获取当前脱敏后的配置快照(未初始化时为 null) */
|
|
2525
3339
|
get config() {
|
|
2526
|
-
|
|
3340
|
+
if (!currentConfig)
|
|
3341
|
+
return null;
|
|
3342
|
+
const snapshot = core.sanitize.sanitizeSensitiveFields(currentConfig);
|
|
3343
|
+
if ("ssl" in snapshot && snapshot.ssl && typeof snapshot.ssl === "object" && "key" in snapshot.ssl)
|
|
3344
|
+
snapshot.ssl.key = core.sanitize.defaultSensitiveReplacement;
|
|
3345
|
+
return snapshot;
|
|
2527
3346
|
},
|
|
2528
3347
|
/** 检查是否已初始化 */
|
|
2529
3348
|
get isInitialized() {
|
|
@@ -2569,35 +3388,123 @@ var reldb = {
|
|
|
2569
3388
|
/**
|
|
2570
3389
|
* 关闭数据库连接
|
|
2571
3390
|
*
|
|
2572
|
-
*
|
|
3391
|
+
* 等待初始化和在途事务,停止接收新操作;失败时保留资源供 close/init 重试。
|
|
3392
|
+
* @returns 所有资源释放成功才返回成功;并发关闭共享同一清理过程。
|
|
3393
|
+
* @example
|
|
3394
|
+
* ```ts
|
|
3395
|
+
* import { reldb } from '@h-ai/reldb'
|
|
3396
|
+
*
|
|
3397
|
+
* const result = await reldb.close()
|
|
3398
|
+
* if (!result.success) throw new Error(result.error.message)
|
|
3399
|
+
* void result.data
|
|
3400
|
+
* ```
|
|
2573
3401
|
*/
|
|
2574
3402
|
async close() {
|
|
2575
|
-
if (!
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
const closeResult = await currentProvider.close();
|
|
2584
|
-
if (!closeResult.success) {
|
|
2585
|
-
logger5.error("RelDB module close failed", { code: closeResult.error.code, message: closeResult.error.message });
|
|
2586
|
-
return closeResult;
|
|
2587
|
-
}
|
|
2588
|
-
logger5.info("RelDB module closed");
|
|
2589
|
-
return ok(void 0);
|
|
2590
|
-
} catch (error) {
|
|
2591
|
-
logger5.error("RelDB module close failed", { error });
|
|
2592
|
-
return err(HaiReldbError.CONNECTION_FAILED, reldbM("reldb_closeFailed", { params: { error: error instanceof Error ? error.message : String(error) } }), error);
|
|
2593
|
-
} finally {
|
|
2594
|
-
currentProvider = null;
|
|
2595
|
-
currentConfig = null;
|
|
2596
|
-
currentJsonOps = null;
|
|
3403
|
+
if (!closingPromise) {
|
|
3404
|
+
closingPromise = (async () => {
|
|
3405
|
+
if (initCompletion)
|
|
3406
|
+
await initCompletion;
|
|
3407
|
+
return await closeOwnedProvider();
|
|
3408
|
+
})().finally(() => {
|
|
3409
|
+
closingPromise = null;
|
|
3410
|
+
});
|
|
2597
3411
|
}
|
|
3412
|
+
return await closingPromise;
|
|
2598
3413
|
}
|
|
2599
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 = (cause) => err({ ...HaiCommonError.INIT_FAILED, message: core.i18n.coreM("core_runtimeStartFailed", { params: { module } }), ext: { module, phase: "migration" }, cause });
|
|
3428
|
+
async function inspect(sql) {
|
|
3429
|
+
const columns = await schemaColumns(db, table, sql);
|
|
3430
|
+
if (!columns.success)
|
|
3431
|
+
return failed(columns.error);
|
|
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(row.error);
|
|
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(needed.error);
|
|
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(created.error);
|
|
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(seeded.error);
|
|
3456
|
+
const started = await db.tx.begin();
|
|
3457
|
+
if (!started.success)
|
|
3458
|
+
return failed(started.error);
|
|
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(lock.success ? new Error("Migration lock unavailable") : lock.error);
|
|
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(locked.error);
|
|
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(needed.error);
|
|
3479
|
+
if (needed.data) {
|
|
3480
|
+
const applied = await step.apply(tx);
|
|
3481
|
+
if (!applied.success)
|
|
3482
|
+
return failed(applied.error);
|
|
3483
|
+
const remaining = await step.needed(tx);
|
|
3484
|
+
if (!remaining.success || remaining.data)
|
|
3485
|
+
return failed(remaining.success ? new Error("Migration step did not converge") : remaining.error);
|
|
3486
|
+
}
|
|
3487
|
+
}
|
|
3488
|
+
const recorded = await tx.execute(`UPDATE ${table} SET version=? WHERE module=?`, [targetVersion, module]);
|
|
3489
|
+
if (!recorded.success)
|
|
3490
|
+
return failed(recorded.error);
|
|
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(result.error);
|
|
3498
|
+
} catch (error) {
|
|
3499
|
+
return failed(error);
|
|
3500
|
+
} finally {
|
|
3501
|
+
if (mysqlLock)
|
|
3502
|
+
await tx.get("SELECT RELEASE_LOCK(?)", [`hai-schema:${module}`]);
|
|
3503
|
+
if (!committed)
|
|
3504
|
+
await tx.rollback();
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
2600
3507
|
|
|
2601
|
-
export { BaseReldbCrudRepository, HaiReldbError,
|
|
3508
|
+
export { BaseReldbCrudRepository, HaiReldbError, createJsonOps, escapeSqlString, migrateSchema, parseJsonPath, quoteIdentifier, reldb, schemaColumns, validateIdentifier, validateIdentifiers };
|
|
2602
3509
|
//# sourceMappingURL=index.js.map
|
|
2603
3510
|
//# sourceMappingURL=index.js.map
|