@h-ai/reldb 1.0.0-beta.2 → 1.0.0-beta.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +295 -143
- package/README.ja.md +295 -143
- package/README.md +295 -143
- package/dist/{chunk-SDS2ZT3V.js → chunk-7OE27ZH3.js} +18 -6
- package/dist/chunk-7OE27ZH3.js.map +1 -0
- package/dist/index.d.ts +56 -54
- package/dist/index.js +392 -159
- package/dist/index.js.map +1 -1
- package/dist/providers/sqlite/reldb-sqlite-worker.mjs +23 -6
- package/dist/schemas.d.ts +2 -1
- package/dist/schemas.js +1 -1
- package/package.json +10 -10
- package/dist/chunk-SDS2ZT3V.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ReldbConfigSchema, MysqlPoolConfigSchema } from './chunk-
|
|
2
|
-
export { MysqlConfigSchema, MysqlOptionsSchema, MysqlPoolConfigSchema, MysqlSslConfigSchema, PoolConfigSchema, PostgresqlConfigSchema, ReldbConfigSchema, ReldbOperationLogConfigSchema, ReldbTypeSchema, SqliteConfigSchema, SqliteOptionsSchema, SslConfigSchema } from './chunk-
|
|
1
|
+
import { ReldbConfigSchema, MysqlPoolConfigSchema } from './chunk-7OE27ZH3.js';
|
|
2
|
+
export { MysqlConfigSchema, MysqlOptionsSchema, MysqlPoolConfigSchema, MysqlSslConfigSchema, PoolConfigSchema, PostgresqlConfigSchema, ReldbConfigSchema, ReldbOperationLogConfigSchema, ReldbTypeSchema, SqliteConfigSchema, SqliteOptionsSchema, SslConfigSchema } from './chunk-7OE27ZH3.js';
|
|
3
3
|
import { core, ok, err, HaiCommonError } from '@h-ai/core';
|
|
4
4
|
import process from 'process';
|
|
5
5
|
import { createRequire } from 'module';
|
|
@@ -253,7 +253,10 @@ var BaseReldbCrudRepository = class {
|
|
|
253
253
|
* @returns 表结构定义
|
|
254
254
|
*/
|
|
255
255
|
buildReldbTableDef(fields) {
|
|
256
|
-
const entries = fields.map((field) => [
|
|
256
|
+
const entries = fields.map((field) => [
|
|
257
|
+
field.columnName,
|
|
258
|
+
this.normalizeReldbColumnDef(field.def)
|
|
259
|
+
]);
|
|
257
260
|
return Object.fromEntries(entries);
|
|
258
261
|
}
|
|
259
262
|
/**
|
|
@@ -599,7 +602,10 @@ var BaseReldbCrudRepository = class {
|
|
|
599
602
|
} else {
|
|
600
603
|
sql = `INSERT INTO ${this.table} (${insertColumns.join(", ")}) VALUES (${placeholders}) ON CONFLICT(${this.idColumn}) DO UPDATE SET ${updateSet}`;
|
|
601
604
|
}
|
|
602
|
-
return this.sql(tx).execute(sql, [
|
|
605
|
+
return this.sql(tx).execute(sql, [
|
|
606
|
+
...insertValues,
|
|
607
|
+
...updateValues
|
|
608
|
+
]);
|
|
603
609
|
}
|
|
604
610
|
/**
|
|
605
611
|
* 根据主键查找单条记录
|
|
@@ -879,13 +885,19 @@ function createSqliteJsonOps() {
|
|
|
879
885
|
set(column, path, value) {
|
|
880
886
|
return {
|
|
881
887
|
sql: `json_set(${column}, ?, json(?))`,
|
|
882
|
-
params: [
|
|
888
|
+
params: [
|
|
889
|
+
path,
|
|
890
|
+
JSON.stringify(value)
|
|
891
|
+
]
|
|
883
892
|
};
|
|
884
893
|
},
|
|
885
894
|
insert(column, path, value) {
|
|
886
895
|
return {
|
|
887
896
|
sql: `json_insert(${column}, ?, json(?))`,
|
|
888
|
-
params: [
|
|
897
|
+
params: [
|
|
898
|
+
path,
|
|
899
|
+
JSON.stringify(value)
|
|
900
|
+
]
|
|
889
901
|
};
|
|
890
902
|
},
|
|
891
903
|
remove(column, path) {
|
|
@@ -897,7 +909,9 @@ function createSqliteJsonOps() {
|
|
|
897
909
|
merge(column, patch) {
|
|
898
910
|
return {
|
|
899
911
|
sql: `json_patch(${column}, ?)`,
|
|
900
|
-
params: [
|
|
912
|
+
params: [
|
|
913
|
+
JSON.stringify(patch)
|
|
914
|
+
]
|
|
901
915
|
};
|
|
902
916
|
}
|
|
903
917
|
};
|
|
@@ -907,14 +921,25 @@ function buildPostgresMergePatch(column, patch) {
|
|
|
907
921
|
if (!normalized || typeof normalized !== "object" || Array.isArray(normalized))
|
|
908
922
|
throw new TypeError(reldbM("reldb_jsonPatchObjectRequired"));
|
|
909
923
|
const steps = [];
|
|
910
|
-
const pending = Object.entries(normalized).reverse().map(([key, value]) => ({
|
|
924
|
+
const pending = Object.entries(normalized).reverse().map(([key, value]) => ({
|
|
925
|
+
path: [key],
|
|
926
|
+
value
|
|
927
|
+
}));
|
|
911
928
|
while (pending.length > 0) {
|
|
912
929
|
const entry = pending.pop();
|
|
913
930
|
const object = entry.value !== null && typeof entry.value === "object" && !Array.isArray(entry.value);
|
|
914
|
-
steps.push({
|
|
931
|
+
steps.push({
|
|
932
|
+
path: entry.path,
|
|
933
|
+
value: object ? {} : entry.value,
|
|
934
|
+
object
|
|
935
|
+
});
|
|
915
936
|
if (object) {
|
|
916
|
-
for (const [key, value] of Object.entries(entry.value).reverse())
|
|
917
|
-
pending.push({
|
|
937
|
+
for (const [key, value] of Object.entries(entry.value).reverse()) {
|
|
938
|
+
pending.push({
|
|
939
|
+
path: [...entry.path, key],
|
|
940
|
+
value
|
|
941
|
+
});
|
|
942
|
+
}
|
|
918
943
|
}
|
|
919
944
|
}
|
|
920
945
|
return {
|
|
@@ -939,7 +964,9 @@ function buildPostgresMergePatch(column, patch) {
|
|
|
939
964
|
FROM merged JOIN patch_steps ON patch_steps.step = merged.step + 1
|
|
940
965
|
)
|
|
941
966
|
SELECT document FROM merged ORDER BY step DESC LIMIT 1)`,
|
|
942
|
-
params: [
|
|
967
|
+
params: [
|
|
968
|
+
JSON.stringify(steps)
|
|
969
|
+
]
|
|
943
970
|
};
|
|
944
971
|
}
|
|
945
972
|
function createPostgresJsonOps() {
|
|
@@ -955,14 +982,20 @@ function createPostgresJsonOps() {
|
|
|
955
982
|
const segments = parseJsonPath(path);
|
|
956
983
|
return {
|
|
957
984
|
sql: `jsonb_set(${column}, ?::text[], ?::jsonb)`,
|
|
958
|
-
params: [
|
|
985
|
+
params: [
|
|
986
|
+
segments,
|
|
987
|
+
JSON.stringify(value)
|
|
988
|
+
]
|
|
959
989
|
};
|
|
960
990
|
},
|
|
961
991
|
insert(column, path, value) {
|
|
962
992
|
const segments = parseJsonPath(path);
|
|
963
993
|
return {
|
|
964
994
|
sql: `jsonb_insert(${column}, ?::text[], ?::jsonb)`,
|
|
965
|
-
params: [
|
|
995
|
+
params: [
|
|
996
|
+
segments,
|
|
997
|
+
JSON.stringify(value)
|
|
998
|
+
]
|
|
966
999
|
};
|
|
967
1000
|
},
|
|
968
1001
|
remove(column, path) {
|
|
@@ -988,13 +1021,19 @@ function createMysqlJsonOps() {
|
|
|
988
1021
|
set(column, path, value) {
|
|
989
1022
|
return {
|
|
990
1023
|
sql: `JSON_SET(${column}, ?, CAST(? AS JSON))`,
|
|
991
|
-
params: [
|
|
1024
|
+
params: [
|
|
1025
|
+
path,
|
|
1026
|
+
JSON.stringify(value)
|
|
1027
|
+
]
|
|
992
1028
|
};
|
|
993
1029
|
},
|
|
994
1030
|
insert(column, path, value) {
|
|
995
1031
|
return {
|
|
996
1032
|
sql: `JSON_INSERT(${column}, ?, CAST(? AS JSON))`,
|
|
997
|
-
params: [
|
|
1033
|
+
params: [
|
|
1034
|
+
path,
|
|
1035
|
+
JSON.stringify(value)
|
|
1036
|
+
]
|
|
998
1037
|
};
|
|
999
1038
|
},
|
|
1000
1039
|
remove(column, path) {
|
|
@@ -1006,7 +1045,9 @@ function createMysqlJsonOps() {
|
|
|
1006
1045
|
merge(column, patch) {
|
|
1007
1046
|
return {
|
|
1008
1047
|
sql: `JSON_MERGE_PATCH(${column}, CAST(? AS JSON))`,
|
|
1009
|
-
params: [
|
|
1048
|
+
params: [
|
|
1049
|
+
JSON.stringify(patch)
|
|
1050
|
+
]
|
|
1010
1051
|
};
|
|
1011
1052
|
}
|
|
1012
1053
|
};
|
|
@@ -1088,7 +1129,9 @@ function validateIdentifier(name) {
|
|
|
1088
1129
|
if (VALID_IDENTIFIER_RE.test(name)) {
|
|
1089
1130
|
return ok(name);
|
|
1090
1131
|
}
|
|
1091
|
-
return err(HaiReldbError.CONFIG_ERROR, reldbM("reldb_invalidIdentifier", {
|
|
1132
|
+
return err(HaiReldbError.CONFIG_ERROR, reldbM("reldb_invalidIdentifier", {
|
|
1133
|
+
params: { name }
|
|
1134
|
+
}));
|
|
1092
1135
|
}
|
|
1093
1136
|
function validateIdentifiers(names) {
|
|
1094
1137
|
for (const name of names) {
|
|
@@ -1331,7 +1374,11 @@ function createCrud(ops, config) {
|
|
|
1331
1374
|
return findResult;
|
|
1332
1375
|
}
|
|
1333
1376
|
if (findResult.data === null) {
|
|
1334
|
-
return err(HaiReldbError.RECORD_NOT_FOUND, reldbM("reldb_crudRecordNotFound", {
|
|
1377
|
+
return err(HaiReldbError.RECORD_NOT_FOUND, reldbM("reldb_crudRecordNotFound", {
|
|
1378
|
+
params: {
|
|
1379
|
+
id: String(id)
|
|
1380
|
+
}
|
|
1381
|
+
}));
|
|
1335
1382
|
}
|
|
1336
1383
|
return ok(findResult.data);
|
|
1337
1384
|
},
|
|
@@ -1404,7 +1451,10 @@ function createCrud(ops, config) {
|
|
|
1404
1451
|
}
|
|
1405
1452
|
const setClause = filtered.map((column) => `${column} = ?`).join(", ");
|
|
1406
1453
|
const sql = `UPDATE ${table} SET ${setClause} WHERE ${idColumn} = ?`;
|
|
1407
|
-
const params = [
|
|
1454
|
+
const params = [
|
|
1455
|
+
...filtered.map((column) => data[column]),
|
|
1456
|
+
id
|
|
1457
|
+
];
|
|
1408
1458
|
return resolveOps(tx).execute(sql, params);
|
|
1409
1459
|
},
|
|
1410
1460
|
/**
|
|
@@ -1517,12 +1567,11 @@ function createTxHandle(baseDmlOps, callbacks) {
|
|
|
1517
1567
|
const check = ensureActive();
|
|
1518
1568
|
if (!check.success)
|
|
1519
1569
|
return check;
|
|
1570
|
+
active = false;
|
|
1520
1571
|
try {
|
|
1521
1572
|
await callbacks.commit();
|
|
1522
|
-
active = false;
|
|
1523
1573
|
return ok(void 0);
|
|
1524
1574
|
} catch (error) {
|
|
1525
|
-
active = false;
|
|
1526
1575
|
return err(HaiReldbError.TRANSACTION_FAILED, callbacks.errorMessage(String(error)), error);
|
|
1527
1576
|
} finally {
|
|
1528
1577
|
callbacks.release();
|
|
@@ -1532,12 +1581,11 @@ function createTxHandle(baseDmlOps, callbacks) {
|
|
|
1532
1581
|
const check = ensureActive();
|
|
1533
1582
|
if (!check.success)
|
|
1534
1583
|
return check;
|
|
1584
|
+
active = false;
|
|
1535
1585
|
try {
|
|
1536
1586
|
await callbacks.rollback();
|
|
1537
|
-
active = false;
|
|
1538
1587
|
return ok(void 0);
|
|
1539
1588
|
} catch (error) {
|
|
1540
|
-
active = false;
|
|
1541
1589
|
return err(HaiReldbError.TRANSACTION_FAILED, callbacks.errorMessage(String(error)), error);
|
|
1542
1590
|
} finally {
|
|
1543
1591
|
callbacks.release();
|
|
@@ -1559,7 +1607,11 @@ function createTxWrap(beginTx) {
|
|
|
1559
1607
|
return ok(result);
|
|
1560
1608
|
} catch (error) {
|
|
1561
1609
|
await txResult.data.rollback();
|
|
1562
|
-
return err(HaiReldbError.TRANSACTION_FAILED, reldbM("reldb_txFailed", {
|
|
1610
|
+
return err(HaiReldbError.TRANSACTION_FAILED, reldbM("reldb_txFailed", {
|
|
1611
|
+
params: {
|
|
1612
|
+
error: String(error)
|
|
1613
|
+
}
|
|
1614
|
+
}), error);
|
|
1563
1615
|
}
|
|
1564
1616
|
};
|
|
1565
1617
|
}
|
|
@@ -1599,8 +1651,14 @@ async function wrapOp(ctx, fn, errorDef, errorLabel, errorMeta) {
|
|
|
1599
1651
|
let definition = original ?? errorDef;
|
|
1600
1652
|
const traceId = core.context.get().traceId ?? core.context.get().requestId ?? original?.traceId ?? core.id.uuid();
|
|
1601
1653
|
const fields = error && typeof error === "object" ? error : {};
|
|
1602
|
-
if (definition.code === HaiReldbError.QUERY_FAILED.code && ([
|
|
1654
|
+
if (definition.code === HaiReldbError.QUERY_FAILED.code && ([
|
|
1655
|
+
"23505",
|
|
1656
|
+
"ER_DUP_ENTRY",
|
|
1657
|
+
"SQLITE_CONSTRAINT_UNIQUE",
|
|
1658
|
+
"SQLITE_CONSTRAINT_PRIMARYKEY"
|
|
1659
|
+
].includes(String(fields.code)) || fields.errno === 1062)) {
|
|
1603
1660
|
definition = HaiReldbError.DUPLICATE_ENTRY;
|
|
1661
|
+
}
|
|
1604
1662
|
const secrets = /* @__PURE__ */ new Set();
|
|
1605
1663
|
const seen = /* @__PURE__ */ new WeakSet();
|
|
1606
1664
|
const collect = (value) => {
|
|
@@ -1614,17 +1672,44 @@ async function wrapOp(ctx, fn, errorDef, errorLabel, errorMeta) {
|
|
|
1614
1672
|
}
|
|
1615
1673
|
};
|
|
1616
1674
|
collect(errorMeta?.params);
|
|
1617
|
-
const payloads = [
|
|
1675
|
+
const payloads = [
|
|
1676
|
+
errorMeta,
|
|
1677
|
+
...Array.isArray(errorMeta?.statements) ? errorMeta.statements : []
|
|
1678
|
+
];
|
|
1618
1679
|
for (const payload of payloads) {
|
|
1619
1680
|
collect(payload?.params);
|
|
1620
1681
|
for (const match of (typeof payload?.sql === "string" ? payload.sql : "").matchAll(/'((?:''|[^'])*)'/g))
|
|
1621
1682
|
secrets.add(match[1].replaceAll("''", "'"));
|
|
1622
1683
|
}
|
|
1623
1684
|
const diagnostic = {};
|
|
1624
|
-
for (const key2 of [
|
|
1685
|
+
for (const key2 of [
|
|
1686
|
+
"name",
|
|
1687
|
+
"message",
|
|
1688
|
+
"stack",
|
|
1689
|
+
"code",
|
|
1690
|
+
"errno",
|
|
1691
|
+
"sqlState",
|
|
1692
|
+
"detail",
|
|
1693
|
+
"hint",
|
|
1694
|
+
"constraint",
|
|
1695
|
+
"schema",
|
|
1696
|
+
"routine",
|
|
1697
|
+
"severity",
|
|
1698
|
+
"table",
|
|
1699
|
+
"column"
|
|
1700
|
+
]) {
|
|
1625
1701
|
const value = error instanceof Error && key2 === "message" ? error.message : error instanceof Error && key2 === "stack" ? error.stack : fields[key2];
|
|
1626
1702
|
if (typeof value === "string") {
|
|
1627
|
-
if ([
|
|
1703
|
+
if ([
|
|
1704
|
+
"code",
|
|
1705
|
+
"sqlState",
|
|
1706
|
+
"schema",
|
|
1707
|
+
"routine",
|
|
1708
|
+
"severity",
|
|
1709
|
+
"constraint",
|
|
1710
|
+
"table",
|
|
1711
|
+
"column"
|
|
1712
|
+
].includes(key2)) {
|
|
1628
1713
|
diagnostic[key2] = core.sanitize.sanitizeText(value);
|
|
1629
1714
|
} else {
|
|
1630
1715
|
const frames = key2 === "stack" ? value.search(/^\s+at\s/m) : -1;
|
|
@@ -1637,9 +1722,21 @@ async function wrapOp(ctx, fn, errorDef, errorLabel, errorMeta) {
|
|
|
1637
1722
|
}
|
|
1638
1723
|
if (!Object.keys(diagnostic).length)
|
|
1639
1724
|
diagnostic.message = core.sanitize.sanitizeText(String(error), [...secrets]);
|
|
1640
|
-
ctx.logger.error(errorLabel, {
|
|
1725
|
+
ctx.logger.error(errorLabel, {
|
|
1726
|
+
...summarizeOperationPayload(errorMeta),
|
|
1727
|
+
table: errorMeta?.tableName,
|
|
1728
|
+
column: errorMeta?.columnName,
|
|
1729
|
+
errorCode: definition.code,
|
|
1730
|
+
traceId,
|
|
1731
|
+
error: diagnostic
|
|
1732
|
+
});
|
|
1641
1733
|
const key = definition.code === HaiReldbError.DDL_FAILED.code ? "reldb_ddlFailed" : definition.code === HaiReldbError.TRANSACTION_FAILED.code ? "reldb_txFailed" : "reldb_queryFailed";
|
|
1642
|
-
return err({
|
|
1734
|
+
return err({
|
|
1735
|
+
...definition,
|
|
1736
|
+
traceId,
|
|
1737
|
+
message: definition.code === HaiReldbError.DUPLICATE_ENTRY.code ? reldbM("reldb_duplicateEntry") : original ? core.sanitize.sanitizeText(original.message, [...secrets]) : reldbM(key),
|
|
1738
|
+
cause: diagnostic
|
|
1739
|
+
});
|
|
1643
1740
|
};
|
|
1644
1741
|
try {
|
|
1645
1742
|
const result = await fn();
|
|
@@ -1783,14 +1880,20 @@ async function queryPageAsync(queryRows, options) {
|
|
|
1783
1880
|
const countRows = await queryRows(countSql, options.params);
|
|
1784
1881
|
const total = countRows.length > 0 ? parseCount(countRows[0]) : 0;
|
|
1785
1882
|
const dataSql = `${options.sql} LIMIT ? OFFSET ?`;
|
|
1786
|
-
const dataParams = [
|
|
1883
|
+
const dataParams = [
|
|
1884
|
+
...options.params ?? [],
|
|
1885
|
+
pagination2.limit,
|
|
1886
|
+
pagination2.offset
|
|
1887
|
+
];
|
|
1787
1888
|
const rows = await queryRows(dataSql, dataParams);
|
|
1788
1889
|
return buildPaginatedResult(rows, total, pagination2);
|
|
1789
1890
|
}
|
|
1790
1891
|
|
|
1791
1892
|
// src/providers/reldb-ddl-builder.ts
|
|
1792
1893
|
function buildColumnSqlBase(name, def, options) {
|
|
1793
|
-
const parts = [
|
|
1894
|
+
const parts = [
|
|
1895
|
+
options.quoteId(name)
|
|
1896
|
+
];
|
|
1794
1897
|
parts.push(options.mapType(def));
|
|
1795
1898
|
if (options.inlinePrimaryKey && def.primaryKey) {
|
|
1796
1899
|
parts.push("PRIMARY KEY");
|
|
@@ -1934,10 +2037,18 @@ function createMysqlProvider() {
|
|
|
1934
2037
|
}));
|
|
1935
2038
|
}, (error) => {
|
|
1936
2039
|
releaseSlot();
|
|
1937
|
-
finish(err(HaiReldbError.CONNECTION_FAILED, reldbM("reldb_mysqlConnectionFailed", {
|
|
2040
|
+
finish(err(HaiReldbError.CONNECTION_FAILED, reldbM("reldb_mysqlConnectionFailed", {
|
|
2041
|
+
params: {
|
|
2042
|
+
error: String(error)
|
|
2043
|
+
}
|
|
2044
|
+
}), error));
|
|
1938
2045
|
}).catch((error) => {
|
|
1939
2046
|
ready = false;
|
|
1940
|
-
finish(err(HaiReldbError.CONNECTION_FAILED, reldbM("reldb_mysqlConnectionFailed", {
|
|
2047
|
+
finish(err(HaiReldbError.CONNECTION_FAILED, reldbM("reldb_mysqlConnectionFailed", {
|
|
2048
|
+
params: {
|
|
2049
|
+
error: reldbM("reldb_mysqlPoolReleaseFailed")
|
|
2050
|
+
}
|
|
2051
|
+
}), error));
|
|
1941
2052
|
logger2.warn("MySQL connection release failed; pool requires cleanup");
|
|
1942
2053
|
});
|
|
1943
2054
|
}
|
|
@@ -1986,7 +2097,11 @@ function createMysqlProvider() {
|
|
|
1986
2097
|
poolConfig = null;
|
|
1987
2098
|
return ok(void 0);
|
|
1988
2099
|
} catch (error) {
|
|
1989
|
-
return err(HaiReldbError.CONNECTION_FAILED, reldbM("reldb_mysqlConnectionFailed", {
|
|
2100
|
+
return err(HaiReldbError.CONNECTION_FAILED, reldbM("reldb_mysqlConnectionFailed", {
|
|
2101
|
+
params: {
|
|
2102
|
+
error: String(error)
|
|
2103
|
+
}
|
|
2104
|
+
}), error);
|
|
1990
2105
|
}
|
|
1991
2106
|
})().finally(() => {
|
|
1992
2107
|
closing = null;
|
|
@@ -2002,7 +2117,9 @@ function createMysqlProvider() {
|
|
|
2002
2117
|
return `\`${name}\``;
|
|
2003
2118
|
}
|
|
2004
2119
|
function mysqlTxErrorMessage(detail) {
|
|
2005
|
-
return reldbM("reldb_mysqlTxFailed", {
|
|
2120
|
+
return reldbM("reldb_mysqlTxFailed", {
|
|
2121
|
+
params: { error: detail }
|
|
2122
|
+
});
|
|
2006
2123
|
}
|
|
2007
2124
|
async function findIndexTableName(indexName) {
|
|
2008
2125
|
return withConnection(async (connection) => {
|
|
@@ -2129,7 +2246,9 @@ function createMysqlProvider() {
|
|
|
2129
2246
|
if (!tableResult.data) {
|
|
2130
2247
|
if (ifExists)
|
|
2131
2248
|
return ok(void 0);
|
|
2132
|
-
return err(HaiReldbError.DDL_FAILED, reldbM("reldb_ddlFailed", {
|
|
2249
|
+
return err(HaiReldbError.DDL_FAILED, reldbM("reldb_ddlFailed", {
|
|
2250
|
+
params: { error: `index not found: ${index}` }
|
|
2251
|
+
}));
|
|
2133
2252
|
}
|
|
2134
2253
|
return executeDdl(`DROP INDEX ${mysqlQuoteId(index)} ON ${mysqlQuoteId(tableResult.data)}`);
|
|
2135
2254
|
},
|
|
@@ -2168,7 +2287,11 @@ function createMysqlProvider() {
|
|
|
2168
2287
|
} catch (error) {
|
|
2169
2288
|
await connection.rollback().catch(() => {
|
|
2170
2289
|
});
|
|
2171
|
-
return err(HaiReldbError.QUERY_FAILED, reldbM("reldb_batchFailed", {
|
|
2290
|
+
return err(HaiReldbError.QUERY_FAILED, reldbM("reldb_batchFailed", {
|
|
2291
|
+
params: {
|
|
2292
|
+
error: String(error)
|
|
2293
|
+
}
|
|
2294
|
+
}), error);
|
|
2172
2295
|
}
|
|
2173
2296
|
});
|
|
2174
2297
|
},
|
|
@@ -2233,7 +2356,10 @@ function createMysqlProvider() {
|
|
|
2233
2356
|
return err(HaiReldbError.TRANSACTION_FAILED, mysqlTxErrorMessage(String(error)), error);
|
|
2234
2357
|
}
|
|
2235
2358
|
const txDmlOps = createMysqlTxDmlOps(connection);
|
|
2236
|
-
const guardedTxDmlOps = createBaseDmlOps({
|
|
2359
|
+
const guardedTxDmlOps = createBaseDmlOps({
|
|
2360
|
+
...ctx,
|
|
2361
|
+
isConnected: () => connection !== null
|
|
2362
|
+
}, txDmlOps);
|
|
2237
2363
|
return ok(createTxHandle(guardedTxDmlOps, {
|
|
2238
2364
|
commit: async () => {
|
|
2239
2365
|
await connection.commit();
|
|
@@ -2275,7 +2401,13 @@ function createMysqlProvider() {
|
|
|
2275
2401
|
poolConfig = config.pool ?? MysqlPoolConfigSchema.parse({});
|
|
2276
2402
|
pool = mysql.createPool({
|
|
2277
2403
|
// mysql2 会用非空的分字段覆盖 URI;URL 模式不能传 schema 默认 host 或冲突字段。
|
|
2278
|
-
...config.url ? { uri: config.url } : {
|
|
2404
|
+
...config.url ? { uri: config.url } : {
|
|
2405
|
+
host: config.host,
|
|
2406
|
+
port: config.port,
|
|
2407
|
+
database: config.database,
|
|
2408
|
+
user: config.user,
|
|
2409
|
+
password: config.password
|
|
2410
|
+
},
|
|
2279
2411
|
ssl: config.ssl,
|
|
2280
2412
|
connectionLimit: poolConfig.max,
|
|
2281
2413
|
// 排队由本 Provider 管理,超时项可直接移除;驱动绝不保留取消后的等待者。
|
|
@@ -2286,12 +2418,21 @@ function createMysqlProvider() {
|
|
|
2286
2418
|
await pool.query("SELECT 1");
|
|
2287
2419
|
ready = true;
|
|
2288
2420
|
currentConfig2 = config;
|
|
2289
|
-
logger2.info("Connected to MySQL", config.url ? {
|
|
2421
|
+
logger2.info("Connected to MySQL", config.url ? {
|
|
2422
|
+
target: core.sanitize.sanitizeUrl(config.url)
|
|
2423
|
+
} : { host: config.host, port: config.port, database: config.database });
|
|
2290
2424
|
return ok(void 0);
|
|
2291
2425
|
} catch (error) {
|
|
2292
2426
|
const cleanup = await closeResource();
|
|
2293
|
-
const result = err(HaiReldbError.CONNECTION_FAILED, reldbM("reldb_mysqlConnectionFailed", {
|
|
2294
|
-
|
|
2427
|
+
const result = err(HaiReldbError.CONNECTION_FAILED, reldbM("reldb_mysqlConnectionFailed", {
|
|
2428
|
+
params: {
|
|
2429
|
+
error: String(error)
|
|
2430
|
+
}
|
|
2431
|
+
}), error);
|
|
2432
|
+
return cleanup.success || result.success ? result : err({
|
|
2433
|
+
...result.error,
|
|
2434
|
+
ext: { cleanupPending: true, cleanupError: cleanup.error }
|
|
2435
|
+
});
|
|
2295
2436
|
}
|
|
2296
2437
|
},
|
|
2297
2438
|
close: closeResource,
|
|
@@ -2339,7 +2480,11 @@ function createPostgresProvider() {
|
|
|
2339
2480
|
pool = null;
|
|
2340
2481
|
return ok(void 0);
|
|
2341
2482
|
} catch (error) {
|
|
2342
|
-
return err(HaiReldbError.CONNECTION_FAILED, reldbM("reldb_postgresConnectionFailed", {
|
|
2483
|
+
return err(HaiReldbError.CONNECTION_FAILED, reldbM("reldb_postgresConnectionFailed", {
|
|
2484
|
+
params: {
|
|
2485
|
+
error: String(error)
|
|
2486
|
+
}
|
|
2487
|
+
}), error);
|
|
2343
2488
|
}
|
|
2344
2489
|
})().finally(() => {
|
|
2345
2490
|
closing = null;
|
|
@@ -2442,7 +2587,9 @@ function createPostgresProvider() {
|
|
|
2442
2587
|
return out;
|
|
2443
2588
|
}
|
|
2444
2589
|
function pgTxErrorMessage(detail) {
|
|
2445
|
-
return reldbM("reldb_postgresTxFailed", {
|
|
2590
|
+
return reldbM("reldb_postgresTxFailed", {
|
|
2591
|
+
params: { error: detail }
|
|
2592
|
+
});
|
|
2446
2593
|
}
|
|
2447
2594
|
function mapPgType(def) {
|
|
2448
2595
|
switch (def.type) {
|
|
@@ -2565,7 +2712,11 @@ function createPostgresProvider() {
|
|
|
2565
2712
|
await client.query("ROLLBACK").catch(() => {
|
|
2566
2713
|
});
|
|
2567
2714
|
}
|
|
2568
|
-
return err(HaiReldbError.QUERY_FAILED, reldbM("reldb_batchFailed", {
|
|
2715
|
+
return err(HaiReldbError.QUERY_FAILED, reldbM("reldb_batchFailed", {
|
|
2716
|
+
params: {
|
|
2717
|
+
error: String(error)
|
|
2718
|
+
}
|
|
2719
|
+
}), error);
|
|
2569
2720
|
} finally {
|
|
2570
2721
|
if (client) {
|
|
2571
2722
|
client.release();
|
|
@@ -2631,7 +2782,10 @@ function createPostgresProvider() {
|
|
|
2631
2782
|
return err(HaiReldbError.TRANSACTION_FAILED, pgTxErrorMessage(String(error)), error);
|
|
2632
2783
|
}
|
|
2633
2784
|
const txDmlOps = createPgTxDmlOps(client);
|
|
2634
|
-
const guardedTxDmlOps = createBaseDmlOps({
|
|
2785
|
+
const guardedTxDmlOps = createBaseDmlOps({
|
|
2786
|
+
...ctx,
|
|
2787
|
+
isConnected: () => client !== null
|
|
2788
|
+
}, txDmlOps);
|
|
2635
2789
|
return ok(createTxHandle(guardedTxDmlOps, {
|
|
2636
2790
|
commit: async () => {
|
|
2637
2791
|
await client.query("COMMIT");
|
|
@@ -2697,8 +2851,15 @@ function createPostgresProvider() {
|
|
|
2697
2851
|
return ok(void 0);
|
|
2698
2852
|
} catch (error) {
|
|
2699
2853
|
const cleanup = await closeResource();
|
|
2700
|
-
const result = err(HaiReldbError.CONNECTION_FAILED, reldbM("reldb_postgresConnectionFailed", {
|
|
2701
|
-
|
|
2854
|
+
const result = err(HaiReldbError.CONNECTION_FAILED, reldbM("reldb_postgresConnectionFailed", {
|
|
2855
|
+
params: {
|
|
2856
|
+
error: String(error)
|
|
2857
|
+
}
|
|
2858
|
+
}), error);
|
|
2859
|
+
return cleanup.success || result.success ? result : err({
|
|
2860
|
+
...result.error,
|
|
2861
|
+
ext: { cleanupPending: true, cleanupError: cleanup.error }
|
|
2862
|
+
});
|
|
2702
2863
|
}
|
|
2703
2864
|
},
|
|
2704
2865
|
close: closeResource,
|
|
@@ -2931,7 +3092,11 @@ function createSqliteProvider() {
|
|
|
2931
3092
|
engine = null;
|
|
2932
3093
|
return ok(void 0);
|
|
2933
3094
|
} catch (error) {
|
|
2934
|
-
return err(HaiReldbError.CONNECTION_FAILED, reldbM("reldb_sqliteConnectionFailed", {
|
|
3095
|
+
return err(HaiReldbError.CONNECTION_FAILED, reldbM("reldb_sqliteConnectionFailed", {
|
|
3096
|
+
params: {
|
|
3097
|
+
error: String(error)
|
|
3098
|
+
}
|
|
3099
|
+
}), error);
|
|
2935
3100
|
}
|
|
2936
3101
|
})().finally(() => {
|
|
2937
3102
|
closing = null;
|
|
@@ -3001,7 +3166,9 @@ function createSqliteProvider() {
|
|
|
3001
3166
|
});
|
|
3002
3167
|
}
|
|
3003
3168
|
function sqliteTxErrorMessage(detail) {
|
|
3004
|
-
return reldbM("reldb_sqliteTxFailed", {
|
|
3169
|
+
return reldbM("reldb_sqliteTxFailed", {
|
|
3170
|
+
params: { error: detail }
|
|
3171
|
+
});
|
|
3005
3172
|
}
|
|
3006
3173
|
const rawDdl = {
|
|
3007
3174
|
async createTable(name, columns, ifNotExists = true) {
|
|
@@ -3062,7 +3229,11 @@ function createSqliteProvider() {
|
|
|
3062
3229
|
await engine.batch(statements);
|
|
3063
3230
|
return ok(void 0);
|
|
3064
3231
|
} catch (error) {
|
|
3065
|
-
return err(HaiReldbError.QUERY_FAILED, reldbM("reldb_sqliteBatchFailed", {
|
|
3232
|
+
return err(HaiReldbError.QUERY_FAILED, reldbM("reldb_sqliteBatchFailed", {
|
|
3233
|
+
params: {
|
|
3234
|
+
error: String(error)
|
|
3235
|
+
}
|
|
3236
|
+
}), error);
|
|
3066
3237
|
} finally {
|
|
3067
3238
|
releaseTxLock();
|
|
3068
3239
|
}
|
|
@@ -3118,7 +3289,10 @@ function createSqliteProvider() {
|
|
|
3118
3289
|
return err(HaiReldbError.TRANSACTION_FAILED, sqliteTxErrorMessage(String(error)), error);
|
|
3119
3290
|
}
|
|
3120
3291
|
const txDmlOps = createSqliteTxDmlOps(txEngine);
|
|
3121
|
-
const guardedTxDmlOps = createBaseDmlOps({
|
|
3292
|
+
const guardedTxDmlOps = createBaseDmlOps({
|
|
3293
|
+
...ctx,
|
|
3294
|
+
isConnected: () => txEngine.isOpen()
|
|
3295
|
+
}, txDmlOps);
|
|
3122
3296
|
return ok(createTxHandle(guardedTxDmlOps, {
|
|
3123
3297
|
commit: async () => {
|
|
3124
3298
|
await txEngine.exec("COMMIT");
|
|
@@ -3163,8 +3337,15 @@ function createSqliteProvider() {
|
|
|
3163
3337
|
return ok(void 0);
|
|
3164
3338
|
} catch (error) {
|
|
3165
3339
|
const cleanup = await closeResource();
|
|
3166
|
-
const result = err(HaiReldbError.CONNECTION_FAILED, reldbM("reldb_sqliteConnectionFailed", {
|
|
3167
|
-
|
|
3340
|
+
const result = err(HaiReldbError.CONNECTION_FAILED, reldbM("reldb_sqliteConnectionFailed", {
|
|
3341
|
+
params: {
|
|
3342
|
+
error: String(error)
|
|
3343
|
+
}
|
|
3344
|
+
}), error);
|
|
3345
|
+
return cleanup.success || result.success ? result : err({
|
|
3346
|
+
...result.error,
|
|
3347
|
+
ext: { cleanupPending: true, cleanupError: cleanup.error }
|
|
3348
|
+
});
|
|
3168
3349
|
}
|
|
3169
3350
|
},
|
|
3170
3351
|
close: closeResource,
|
|
@@ -3175,6 +3356,113 @@ function createSqliteProvider() {
|
|
|
3175
3356
|
tx: createBaseTxManager(ctx, beginTx)
|
|
3176
3357
|
};
|
|
3177
3358
|
}
|
|
3359
|
+
async function schemaColumns(db, table, sql = db.sql) {
|
|
3360
|
+
if (!/^[a-z][a-z0-9_]{0,62}$/.test(table))
|
|
3361
|
+
return err(HaiCommonError.VALIDATION_ERROR, core.i18n.coreM("core_errorValidation"));
|
|
3362
|
+
const type = db.config?.type;
|
|
3363
|
+
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]);
|
|
3364
|
+
return found.success ? ok(found.data.map((row) => row.name)) : found;
|
|
3365
|
+
}
|
|
3366
|
+
async function migrateSchema(db, module, steps, options = {}) {
|
|
3367
|
+
if (!db.isInitialized || !/^[a-z][a-z0-9-]{0,62}$/.test(module) || steps.some((step, index) => step.version !== index + 1))
|
|
3368
|
+
return err(HaiCommonError.VALIDATION_ERROR, core.i18n.coreM("core_errorValidation"));
|
|
3369
|
+
const table = "hai_schema_migrations";
|
|
3370
|
+
const targetVersion = steps.length;
|
|
3371
|
+
const failed = (cause) => err({
|
|
3372
|
+
...HaiCommonError.INIT_FAILED,
|
|
3373
|
+
message: core.i18n.coreM("core_runtimeStartFailed", {
|
|
3374
|
+
params: { module }
|
|
3375
|
+
}),
|
|
3376
|
+
ext: { module, phase: "migration" },
|
|
3377
|
+
cause
|
|
3378
|
+
});
|
|
3379
|
+
async function inspect(sql) {
|
|
3380
|
+
const columns = await schemaColumns(db, table, sql);
|
|
3381
|
+
if (!columns.success)
|
|
3382
|
+
return failed(columns.error);
|
|
3383
|
+
const row = columns.data.length ? await sql.get(`SELECT version FROM ${table} WHERE module=?`, [module]) : ok(null);
|
|
3384
|
+
if (!row.success)
|
|
3385
|
+
return failed(row.error);
|
|
3386
|
+
const currentVersion = Number(row.data?.version ?? 0);
|
|
3387
|
+
if (currentVersion > targetVersion)
|
|
3388
|
+
return failed();
|
|
3389
|
+
const changes = [];
|
|
3390
|
+
for (const step of steps) {
|
|
3391
|
+
const needed = await step.needed(sql);
|
|
3392
|
+
if (!needed.success)
|
|
3393
|
+
return failed(needed.error);
|
|
3394
|
+
if (needed.data)
|
|
3395
|
+
changes.push(step.name);
|
|
3396
|
+
}
|
|
3397
|
+
return ok({
|
|
3398
|
+
module,
|
|
3399
|
+
currentVersion,
|
|
3400
|
+
targetVersion,
|
|
3401
|
+
changes,
|
|
3402
|
+
requiresExclusiveAccess: changes.length > 0
|
|
3403
|
+
});
|
|
3404
|
+
}
|
|
3405
|
+
if (options.dryRun)
|
|
3406
|
+
return inspect(db.sql);
|
|
3407
|
+
const created = await db.sql.execute(`CREATE TABLE IF NOT EXISTS ${table} (module VARCHAR(64) PRIMARY KEY, version INTEGER NOT NULL)`);
|
|
3408
|
+
if (!created.success)
|
|
3409
|
+
return failed(created.error);
|
|
3410
|
+
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]);
|
|
3411
|
+
if (!seeded.success)
|
|
3412
|
+
return failed(seeded.error);
|
|
3413
|
+
const started = await db.tx.begin();
|
|
3414
|
+
if (!started.success)
|
|
3415
|
+
return failed(started.error);
|
|
3416
|
+
const tx = started.data;
|
|
3417
|
+
let mysqlLock = false;
|
|
3418
|
+
let committed = false;
|
|
3419
|
+
try {
|
|
3420
|
+
if (db.config?.type === "mysql") {
|
|
3421
|
+
const lock = await tx.get("SELECT GET_LOCK(?,30) AS acquired", [`hai-schema:${module}`]);
|
|
3422
|
+
if (!lock.success || Number(lock.data?.acquired) !== 1) {
|
|
3423
|
+
return failed(lock.success ? new Error("Migration lock unavailable") : lock.error);
|
|
3424
|
+
}
|
|
3425
|
+
mysqlLock = true;
|
|
3426
|
+
}
|
|
3427
|
+
const locked = await tx.execute(`UPDATE ${table} SET version=version WHERE module=?`, [module]);
|
|
3428
|
+
if (!locked.success)
|
|
3429
|
+
return failed(locked.error);
|
|
3430
|
+
const plan = await inspect(tx);
|
|
3431
|
+
if (!plan.success)
|
|
3432
|
+
return plan;
|
|
3433
|
+
for (const step of steps) {
|
|
3434
|
+
const needed = await step.needed(tx);
|
|
3435
|
+
if (!needed.success)
|
|
3436
|
+
return failed(needed.error);
|
|
3437
|
+
if (needed.data) {
|
|
3438
|
+
const applied = await step.apply(tx);
|
|
3439
|
+
if (!applied.success)
|
|
3440
|
+
return failed(applied.error);
|
|
3441
|
+
const remaining = await step.needed(tx);
|
|
3442
|
+
if (!remaining.success || remaining.data) {
|
|
3443
|
+
return failed(remaining.success ? new Error("Migration step did not converge") : remaining.error);
|
|
3444
|
+
}
|
|
3445
|
+
}
|
|
3446
|
+
}
|
|
3447
|
+
const recorded = await tx.execute(`UPDATE ${table} SET version=? WHERE module=?`, [targetVersion, module]);
|
|
3448
|
+
if (!recorded.success)
|
|
3449
|
+
return failed(recorded.error);
|
|
3450
|
+
if (mysqlLock) {
|
|
3451
|
+
await tx.get("SELECT RELEASE_LOCK(?)", [`hai-schema:${module}`]);
|
|
3452
|
+
mysqlLock = false;
|
|
3453
|
+
}
|
|
3454
|
+
const result = await tx.commit();
|
|
3455
|
+
committed = result.success;
|
|
3456
|
+
return result.success ? plan : failed(result.error);
|
|
3457
|
+
} catch (error) {
|
|
3458
|
+
return failed(error);
|
|
3459
|
+
} finally {
|
|
3460
|
+
if (mysqlLock)
|
|
3461
|
+
await tx.get("SELECT RELEASE_LOCK(?)", [`hai-schema:${module}`]);
|
|
3462
|
+
if (!committed)
|
|
3463
|
+
await tx.rollback();
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3178
3466
|
|
|
3179
3467
|
// src/reldb-main.ts
|
|
3180
3468
|
var logger5 = core.logger.child({ module: "reldb", scope: "main" });
|
|
@@ -3196,15 +3484,26 @@ async function closeOwnedProvider() {
|
|
|
3196
3484
|
return ok(void 0);
|
|
3197
3485
|
try {
|
|
3198
3486
|
const result = await pendingProvider.close();
|
|
3199
|
-
if (!result.success)
|
|
3200
|
-
return err({
|
|
3487
|
+
if (!result.success) {
|
|
3488
|
+
return err({
|
|
3489
|
+
...result.error,
|
|
3490
|
+
ext: { ...result.error.ext, cleanupPending: true }
|
|
3491
|
+
});
|
|
3492
|
+
}
|
|
3201
3493
|
pendingProvider = null;
|
|
3202
3494
|
return ok(void 0);
|
|
3203
3495
|
} catch (error) {
|
|
3204
3496
|
logger5.error("Provider cleanup failed; retaining resource for retry", { error });
|
|
3205
|
-
return err({
|
|
3206
|
-
|
|
3207
|
-
|
|
3497
|
+
return err({
|
|
3498
|
+
...HaiReldbError.CONNECTION_FAILED,
|
|
3499
|
+
message: reldbM("reldb_closeFailed", {
|
|
3500
|
+
params: {
|
|
3501
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3502
|
+
}
|
|
3503
|
+
}),
|
|
3504
|
+
cause: error,
|
|
3505
|
+
ext: { cleanupPending: true }
|
|
3506
|
+
});
|
|
3208
3507
|
}
|
|
3209
3508
|
}
|
|
3210
3509
|
async function failInitialization(result) {
|
|
@@ -3215,9 +3514,15 @@ async function failInitialization(result) {
|
|
|
3215
3514
|
const ext = { ...result.error.ext };
|
|
3216
3515
|
delete ext.cleanupPending;
|
|
3217
3516
|
delete ext.cleanupError;
|
|
3218
|
-
return err({
|
|
3517
|
+
return err({
|
|
3518
|
+
...result.error,
|
|
3519
|
+
ext: Object.keys(ext).length > 0 ? ext : void 0
|
|
3520
|
+
});
|
|
3219
3521
|
}
|
|
3220
|
-
return err({
|
|
3522
|
+
return err({
|
|
3523
|
+
...result.error,
|
|
3524
|
+
ext: { ...result.error.ext, cleanupPending: true, cleanupError: cleanup.error }
|
|
3525
|
+
});
|
|
3221
3526
|
}
|
|
3222
3527
|
function createProvider(config) {
|
|
3223
3528
|
switch (config.type) {
|
|
@@ -3259,7 +3564,9 @@ var reldb = {
|
|
|
3259
3564
|
async init(config) {
|
|
3260
3565
|
if (initInProgress || closingPromise) {
|
|
3261
3566
|
logger5.warn("RelDB module init is already in progress, skipping concurrent call");
|
|
3262
|
-
return err(HaiReldbError.CONFIG_ERROR, reldbM("reldb_configError", {
|
|
3567
|
+
return err(HaiReldbError.CONFIG_ERROR, reldbM("reldb_configError", {
|
|
3568
|
+
params: { error: "init already in progress" }
|
|
3569
|
+
}));
|
|
3263
3570
|
}
|
|
3264
3571
|
initInProgress = true;
|
|
3265
3572
|
let resolveInit;
|
|
@@ -3274,7 +3581,9 @@ var reldb = {
|
|
|
3274
3581
|
const parseResult = ReldbConfigSchema.safeParse(config);
|
|
3275
3582
|
if (!parseResult.success) {
|
|
3276
3583
|
logger5.error("RelDB config validation failed", { error: parseResult.error.message });
|
|
3277
|
-
return err(HaiReldbError.CONFIG_ERROR, reldbM("reldb_configError", {
|
|
3584
|
+
return err(HaiReldbError.CONFIG_ERROR, reldbM("reldb_configError", {
|
|
3585
|
+
params: { error: parseResult.error.message }
|
|
3586
|
+
}), parseResult.error);
|
|
3278
3587
|
}
|
|
3279
3588
|
const parsed = parseResult.data;
|
|
3280
3589
|
if (core.module.requiresSharedState(process.env) && parsed.type === "sqlite")
|
|
@@ -3298,7 +3607,11 @@ var reldb = {
|
|
|
3298
3607
|
return ok(void 0);
|
|
3299
3608
|
} catch (error) {
|
|
3300
3609
|
logger5.error("RelDB module initialization failed", { error });
|
|
3301
|
-
return await failInitialization(err(HaiReldbError.CONNECTION_FAILED, reldbM("reldb_initFailed", {
|
|
3610
|
+
return await failInitialization(err(HaiReldbError.CONNECTION_FAILED, reldbM("reldb_initFailed", {
|
|
3611
|
+
params: {
|
|
3612
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3613
|
+
}
|
|
3614
|
+
}), error));
|
|
3302
3615
|
}
|
|
3303
3616
|
} finally {
|
|
3304
3617
|
initInProgress = false;
|
|
@@ -3335,6 +3648,18 @@ var reldb = {
|
|
|
3335
3648
|
get tx() {
|
|
3336
3649
|
return currentProvider?.tx ?? notInitializedTx;
|
|
3337
3650
|
},
|
|
3651
|
+
/** 迁移操作绑定当前实例,调用方只提供模块自己的步骤。 */
|
|
3652
|
+
migrateSchema(module, steps, options) {
|
|
3653
|
+
if (!currentProvider)
|
|
3654
|
+
return Promise.resolve(notInitialized.result());
|
|
3655
|
+
return migrateSchema(reldb, module, steps, options);
|
|
3656
|
+
},
|
|
3657
|
+
/** 列检查复用当前连接或显式传入的事务。 */
|
|
3658
|
+
schemaColumns(table, sql) {
|
|
3659
|
+
if (!currentProvider)
|
|
3660
|
+
return Promise.resolve(notInitialized.result());
|
|
3661
|
+
return schemaColumns(reldb, table, sql);
|
|
3662
|
+
},
|
|
3338
3663
|
/** 获取当前脱敏后的配置快照(未初始化时为 null) */
|
|
3339
3664
|
get config() {
|
|
3340
3665
|
if (!currentConfig)
|
|
@@ -3412,99 +3737,7 @@ var reldb = {
|
|
|
3412
3737
|
return await closingPromise;
|
|
3413
3738
|
}
|
|
3414
3739
|
};
|
|
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
|
-
}
|
|
3507
3740
|
|
|
3508
|
-
export { BaseReldbCrudRepository, HaiReldbError, createJsonOps, escapeSqlString,
|
|
3741
|
+
export { BaseReldbCrudRepository, HaiReldbError, createJsonOps, escapeSqlString, parseJsonPath, quoteIdentifier, reldb, validateIdentifier, validateIdentifiers };
|
|
3509
3742
|
//# sourceMappingURL=index.js.map
|
|
3510
3743
|
//# sourceMappingURL=index.js.map
|