befly 3.10.18 → 3.10.19
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.md +83 -307
- package/dist/befly.config.d.ts +7 -0
- package/{befly.config.ts → dist/befly.config.js} +10 -33
- package/dist/checks/checkApi.d.ts +1 -0
- package/{checks/checkApi.ts → dist/checks/checkApi.js} +11 -28
- package/dist/checks/checkHook.d.ts +1 -0
- package/{checks/checkHook.ts → dist/checks/checkHook.js} +11 -20
- package/dist/checks/checkMenu.d.ts +7 -0
- package/{checks/checkMenu.ts → dist/checks/checkMenu.js} +18 -53
- package/dist/checks/checkPlugin.d.ts +1 -0
- package/{checks/checkPlugin.ts → dist/checks/checkPlugin.js} +11 -20
- package/dist/checks/checkTable.d.ts +6 -0
- package/{checks/checkTable.ts → dist/checks/checkTable.js} +17 -41
- package/dist/configs/presetFields.d.ts +4 -0
- package/{configs/presetFields.ts → dist/configs/presetFields.js} +1 -1
- package/dist/configs/presetRegexp.d.ts +145 -0
- package/{utils/regex.ts → dist/configs/presetRegexp.js} +8 -31
- package/dist/hooks/auth.d.ts +5 -0
- package/{hooks/auth.ts → dist/hooks/auth.js} +6 -10
- package/dist/hooks/cors.d.ts +9 -0
- package/{hooks/cors.ts → dist/hooks/cors.js} +3 -13
- package/dist/hooks/parser.d.ts +12 -0
- package/{hooks/parser.ts → dist/hooks/parser.js} +29 -44
- package/dist/hooks/permission.d.ts +12 -0
- package/{hooks/permission.ts → dist/hooks/permission.js} +14 -25
- package/dist/hooks/validator.d.ts +9 -0
- package/{hooks/validator.ts → dist/hooks/validator.js} +7 -14
- package/dist/lib/asyncContext.d.ts +21 -0
- package/dist/lib/asyncContext.js +27 -0
- package/dist/lib/cacheHelper.d.ts +95 -0
- package/{lib/cacheHelper.ts → dist/lib/cacheHelper.js} +45 -105
- package/dist/lib/cacheKeys.d.ts +23 -0
- package/{lib/cacheKeys.ts → dist/lib/cacheKeys.js} +5 -10
- package/dist/lib/cipher.d.ts +153 -0
- package/{lib/cipher.ts → dist/lib/cipher.js} +23 -44
- package/dist/lib/connect.d.ts +91 -0
- package/{lib/connect.ts → dist/lib/connect.js} +47 -88
- package/dist/lib/dbDialect.d.ts +87 -0
- package/{lib/dbDialect.ts → dist/lib/dbDialect.js} +32 -112
- package/dist/lib/dbHelper.d.ts +204 -0
- package/{lib/dbHelper.ts → dist/lib/dbHelper.js} +83 -240
- package/dist/lib/dbUtils.d.ts +68 -0
- package/{lib/dbUtils.ts → dist/lib/dbUtils.js} +51 -125
- package/dist/lib/jwt.d.ts +13 -0
- package/{lib/jwt.ts → dist/lib/jwt.js} +11 -32
- package/dist/lib/logger.d.ts +32 -0
- package/{lib/logger.ts → dist/lib/logger.js} +202 -279
- package/dist/lib/redisHelper.d.ts +185 -0
- package/{lib/redisHelper.ts → dist/lib/redisHelper.js} +97 -141
- package/dist/lib/sqlBuilder.d.ts +160 -0
- package/{lib/sqlBuilder.ts → dist/lib/sqlBuilder.js} +132 -278
- package/dist/lib/sqlCheck.d.ts +23 -0
- package/{lib/sqlCheck.ts → dist/lib/sqlCheck.js} +24 -41
- package/dist/lib/validator.d.ts +45 -0
- package/{lib/validator.ts → dist/lib/validator.js} +44 -61
- package/dist/loader/loadApis.d.ts +12 -0
- package/{loader/loadApis.ts → dist/loader/loadApis.js} +9 -19
- package/dist/loader/loadHooks.d.ts +8 -0
- package/{loader/loadHooks.ts → dist/loader/loadHooks.js} +7 -21
- package/dist/loader/loadPlugins.d.ts +8 -0
- package/{loader/loadPlugins.ts → dist/loader/loadPlugins.js} +10 -22
- package/dist/main.d.ts +26 -0
- package/{main.ts → dist/main.js} +60 -99
- package/dist/paths.d.ts +93 -0
- package/{paths.ts → dist/paths.js} +6 -19
- package/dist/plugins/cache.d.ts +14 -0
- package/{plugins/cache.ts → dist/plugins/cache.js} +5 -12
- package/dist/plugins/cipher.d.ts +10 -0
- package/{plugins/cipher.ts → dist/plugins/cipher.js} +2 -6
- package/dist/plugins/config.d.ts +10 -0
- package/dist/plugins/config.js +6 -0
- package/dist/plugins/db.d.ts +14 -0
- package/{plugins/db.ts → dist/plugins/db.js} +9 -17
- package/dist/plugins/jwt.d.ts +10 -0
- package/dist/plugins/jwt.js +10 -0
- package/dist/plugins/logger.d.ts +28 -0
- package/{plugins/logger.ts → dist/plugins/logger.js} +3 -8
- package/dist/plugins/redis.d.ts +14 -0
- package/{plugins/redis.ts → dist/plugins/redis.js} +7 -12
- package/dist/plugins/tool.d.ts +79 -0
- package/{plugins/tool.ts → dist/plugins/tool.js} +7 -30
- package/dist/router/api.d.ts +14 -0
- package/dist/router/api.js +107 -0
- package/dist/router/static.d.ts +9 -0
- package/{router/static.ts → dist/router/static.js} +20 -34
- package/dist/scripts/ensureDist.d.ts +1 -0
- package/dist/scripts/ensureDist.js +80 -0
- package/dist/sync/syncApi.d.ts +3 -0
- package/{sync/syncApi.ts → dist/sync/syncApi.js} +34 -54
- package/dist/sync/syncCache.d.ts +2 -0
- package/{sync/syncCache.ts → dist/sync/syncCache.js} +1 -6
- package/dist/sync/syncDev.d.ts +6 -0
- package/{sync/syncDev.ts → dist/sync/syncDev.js} +29 -62
- package/dist/sync/syncMenu.d.ts +14 -0
- package/{sync/syncMenu.ts → dist/sync/syncMenu.js} +65 -125
- package/dist/sync/syncTable.d.ts +151 -0
- package/{sync/syncTable.ts → dist/sync/syncTable.js} +171 -378
- package/{types → dist/types}/api.d.ts +8 -47
- package/dist/types/api.js +4 -0
- package/{types → dist/types}/befly.d.ts +31 -222
- package/dist/types/befly.js +4 -0
- package/{types → dist/types}/cache.d.ts +7 -15
- package/dist/types/cache.js +4 -0
- package/dist/types/cipher.d.ts +27 -0
- package/dist/types/cipher.js +7 -0
- package/{types → dist/types}/common.d.ts +8 -33
- package/dist/types/common.js +5 -0
- package/{types → dist/types}/context.d.ts +2 -4
- package/dist/types/context.js +4 -0
- package/{types → dist/types}/crypto.d.ts +0 -3
- package/dist/types/crypto.js +4 -0
- package/dist/types/database.d.ts +138 -0
- package/dist/types/database.js +4 -0
- package/dist/types/hook.d.ts +15 -0
- package/dist/types/hook.js +6 -0
- package/dist/types/jwt.d.ts +75 -0
- package/dist/types/jwt.js +4 -0
- package/dist/types/logger.d.ts +47 -0
- package/dist/types/logger.js +6 -0
- package/dist/types/plugin.d.ts +14 -0
- package/dist/types/plugin.js +6 -0
- package/dist/types/redis.d.ts +71 -0
- package/dist/types/redis.js +4 -0
- package/{types/roleApisCache.ts → dist/types/roleApisCache.d.ts} +0 -2
- package/dist/types/roleApisCache.js +8 -0
- package/dist/types/sync.d.ts +92 -0
- package/dist/types/sync.js +4 -0
- package/dist/types/table.d.ts +34 -0
- package/dist/types/table.js +4 -0
- package/dist/types/validate.d.ts +67 -0
- package/dist/types/validate.js +4 -0
- package/dist/utils/arrayKeysToCamel.d.ts +13 -0
- package/{utils/arrayKeysToCamel.ts → dist/utils/arrayKeysToCamel.js} +5 -5
- package/dist/utils/calcPerfTime.d.ts +4 -0
- package/{utils/calcPerfTime.ts → dist/utils/calcPerfTime.js} +3 -3
- package/dist/utils/configTypes.d.ts +1 -0
- package/dist/utils/configTypes.js +1 -0
- package/dist/utils/convertBigIntFields.d.ts +11 -0
- package/{utils/convertBigIntFields.ts → dist/utils/convertBigIntFields.js} +5 -9
- package/dist/utils/cors.d.ts +8 -0
- package/{utils/cors.ts → dist/utils/cors.js} +1 -3
- package/dist/utils/disableMenusGlob.d.ts +13 -0
- package/{utils/disableMenusGlob.ts → dist/utils/disableMenusGlob.js} +9 -29
- package/dist/utils/fieldClear.d.ts +11 -0
- package/{utils/fieldClear.ts → dist/utils/fieldClear.js} +15 -33
- package/dist/utils/genShortId.d.ts +10 -0
- package/{utils/genShortId.ts → dist/utils/genShortId.js} +1 -1
- package/dist/utils/getClientIp.d.ts +6 -0
- package/{utils/getClientIp.ts → dist/utils/getClientIp.js} +1 -7
- package/dist/utils/importDefault.d.ts +1 -0
- package/dist/utils/importDefault.js +29 -0
- package/dist/utils/isDirentDirectory.d.ts +2 -0
- package/{utils/isDirentDirectory.ts → dist/utils/isDirentDirectory.js} +3 -8
- package/dist/utils/keysToCamel.d.ts +10 -0
- package/{utils/keysToCamel.ts → dist/utils/keysToCamel.js} +4 -5
- package/dist/utils/keysToSnake.d.ts +10 -0
- package/{utils/keysToSnake.ts → dist/utils/keysToSnake.js} +4 -5
- package/dist/utils/loadMenuConfigs.d.ts +5 -0
- package/{utils/loadMenuConfigs.ts → dist/utils/loadMenuConfigs.js} +24 -51
- package/dist/utils/pickFields.d.ts +4 -0
- package/{utils/pickFields.ts → dist/utils/pickFields.js} +2 -5
- package/dist/utils/process.d.ts +24 -0
- package/{utils/process.ts → dist/utils/process.js} +2 -18
- package/dist/utils/processFields.d.ts +4 -0
- package/{utils/processFields.ts → dist/utils/processFields.js} +5 -9
- package/dist/utils/regex.d.ts +145 -0
- package/{configs/presetRegexp.ts → dist/utils/regex.js} +8 -31
- package/dist/utils/response.d.ts +20 -0
- package/{utils/response.ts → dist/utils/response.js} +28 -49
- package/dist/utils/scanAddons.d.ts +17 -0
- package/{utils/scanAddons.ts → dist/utils/scanAddons.js} +6 -40
- package/dist/utils/scanConfig.d.ts +26 -0
- package/{utils/scanConfig.ts → dist/utils/scanConfig.js} +22 -59
- package/dist/utils/scanFiles.d.ts +30 -0
- package/{utils/scanFiles.ts → dist/utils/scanFiles.js} +26 -66
- package/dist/utils/scanSources.d.ts +10 -0
- package/dist/utils/scanSources.js +41 -0
- package/dist/utils/sortModules.d.ts +28 -0
- package/{utils/sortModules.ts → dist/utils/sortModules.js} +25 -65
- package/dist/utils/sqlLog.d.ts +14 -0
- package/{utils/sqlLog.ts → dist/utils/sqlLog.js} +2 -14
- package/package.json +14 -28
- package/.gitignore +0 -0
- package/bunfig.toml +0 -3
- package/docs/README.md +0 -98
- package/docs/api/api.md +0 -1921
- package/docs/guide/examples.md +0 -926
- package/docs/guide/quickstart.md +0 -354
- package/docs/hooks/auth.md +0 -38
- package/docs/hooks/cors.md +0 -28
- package/docs/hooks/hook.md +0 -838
- package/docs/hooks/parser.md +0 -19
- package/docs/hooks/rateLimit.md +0 -47
- package/docs/infra/redis.md +0 -628
- package/docs/plugins/cipher.md +0 -61
- package/docs/plugins/database.md +0 -189
- package/docs/plugins/plugin.md +0 -986
- package/docs/reference/addon.md +0 -510
- package/docs/reference/config.md +0 -573
- package/docs/reference/logger.md +0 -495
- package/docs/reference/sync.md +0 -478
- package/docs/reference/table.md +0 -763
- package/docs/reference/validator.md +0 -620
- package/lib/asyncContext.ts +0 -43
- package/plugins/config.ts +0 -13
- package/plugins/jwt.ts +0 -15
- package/router/api.ts +0 -130
- package/tsconfig.json +0 -8
- package/types/database.d.ts +0 -541
- package/types/hook.d.ts +0 -25
- package/types/jwt.d.ts +0 -118
- package/types/logger.d.ts +0 -65
- package/types/plugin.d.ts +0 -19
- package/types/redis.d.ts +0 -83
- package/types/sync.d.ts +0 -398
- package/types/table.d.ts +0 -216
- package/types/validate.d.ts +0 -69
- package/utils/configTypes.ts +0 -3
- package/utils/importDefault.ts +0 -21
- package/utils/scanSources.ts +0 -64
|
@@ -5,104 +5,20 @@
|
|
|
5
5
|
* - 历史上该能力拆分在 packages/core/sync/syncTable/* 多个模块中
|
|
6
6
|
* - 现在按项目要求,将所有实现合并到本文件(目录 packages/core/sync/syncTable/ 已删除)
|
|
7
7
|
*/
|
|
8
|
-
|
|
9
|
-
import type { DbDialectName } from "../lib/dbDialect.ts";
|
|
10
|
-
import type { BeflyContext } from "../types/befly.ts";
|
|
11
|
-
import type { DbResult, SqlInfo } from "../types/database.ts";
|
|
12
|
-
import type { ColumnInfo, FieldChange, IndexInfo, TablePlan } from "../types/sync.ts";
|
|
13
|
-
import type { FieldDefinition } from "../types/validate.ts";
|
|
14
|
-
import type { ScanFileResult } from "../utils/scanFiles.ts";
|
|
15
|
-
|
|
16
8
|
import { snakeCase } from "es-toolkit/string";
|
|
17
|
-
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import { Logger } from "../lib/logger.ts";
|
|
21
|
-
|
|
22
|
-
type SqlExecutor = {
|
|
23
|
-
unsafe<T = any>(sqlStr: string, params?: unknown[]): Promise<DbResult<T, SqlInfo>>;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
type MySqlTableExistsRow = { count: number };
|
|
27
|
-
type MySqlColumnInfoRow = {
|
|
28
|
-
COLUMN_NAME: string;
|
|
29
|
-
DATA_TYPE: string;
|
|
30
|
-
COLUMN_TYPE: string;
|
|
31
|
-
CHARACTER_MAXIMUM_LENGTH: number | null;
|
|
32
|
-
IS_NULLABLE: "YES" | "NO";
|
|
33
|
-
COLUMN_DEFAULT: any;
|
|
34
|
-
COLUMN_COMMENT: string | null;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
type PgTableExistsRow = { count: number };
|
|
38
|
-
type PgColumnInfoRow = {
|
|
39
|
-
column_name: string;
|
|
40
|
-
data_type: string;
|
|
41
|
-
character_maximum_length: number | null;
|
|
42
|
-
is_nullable: string;
|
|
43
|
-
column_default: any;
|
|
44
|
-
};
|
|
45
|
-
type PgColumnCommentRow = {
|
|
46
|
-
column_name: string;
|
|
47
|
-
column_comment: string;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
type MySqlIndexRow = {
|
|
51
|
-
INDEX_NAME: string;
|
|
52
|
-
COLUMN_NAME: string;
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
type PgIndexRow = {
|
|
56
|
-
indexname: string;
|
|
57
|
-
indexdef: string;
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
type SqliteTableInfoRow = {
|
|
61
|
-
name: string;
|
|
62
|
-
type: string;
|
|
63
|
-
notnull: number;
|
|
64
|
-
dflt_value: any;
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
type SqliteIndexListRow = { name: string };
|
|
68
|
-
type SqliteIndexInfoRow = { name: string };
|
|
69
|
-
|
|
70
|
-
type DbDialect = DbDialectName;
|
|
71
|
-
|
|
72
|
-
/* ========================================================================== */
|
|
73
|
-
/* 对外导出面
|
|
74
|
-
*
|
|
75
|
-
* 约束:本文件仅导出一个函数:syncTable。
|
|
76
|
-
* - 生产代码:通过 await syncTable(ctx, items) 执行同步。
|
|
77
|
-
* - 测试:通过 syncTable.TestKit 访问纯函数/常量(不再导出零散函数)。
|
|
78
|
-
*/
|
|
79
|
-
/* ========================================================================== */
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* 文件导航(推荐阅读顺序)
|
|
83
|
-
* 1) syncTable(ctx, items) 入口(本段下方)
|
|
84
|
-
* 2) 版本/常量/方言判断(DB_VERSION_REQUIREMENTS 等)
|
|
85
|
-
* 3) 通用 DDL 工具(quote/type/default/ddl/index SQL)
|
|
86
|
-
* 4) Runtime I/O(只读元信息:表/列/索引/版本)
|
|
87
|
-
* 5) plan/apply(写变更:建表/改表/SQLite 重建)
|
|
88
|
-
*/
|
|
89
|
-
|
|
90
|
-
type SyncTableFn = ((ctx: BeflyContext, items: ScanFileResult[]) => Promise<void>) & {
|
|
91
|
-
TestKit: typeof SYNC_TABLE_TEST_KIT;
|
|
92
|
-
};
|
|
93
|
-
|
|
9
|
+
import { CacheKeys } from "../lib/cacheKeys.js";
|
|
10
|
+
import { getDialectByName, getSyncTableColumnsInfoQuery, getSyncTableIndexesQuery } from "../lib/dbDialect.js";
|
|
11
|
+
import { Logger } from "../lib/logger.js";
|
|
94
12
|
/**
|
|
95
13
|
* 数据库同步命令入口(函数模式)
|
|
96
14
|
*/
|
|
97
|
-
export const syncTable = (async (ctx
|
|
15
|
+
export const syncTable = (async (ctx, items) => {
|
|
98
16
|
try {
|
|
99
17
|
// 记录处理过的表名(用于清理缓存)
|
|
100
|
-
const processedTables
|
|
101
|
-
|
|
18
|
+
const processedTables = [];
|
|
102
19
|
if (!Array.isArray(items)) {
|
|
103
20
|
throw new Error("syncTable(items) 参数必须是数组");
|
|
104
21
|
}
|
|
105
|
-
|
|
106
22
|
if (!ctx) {
|
|
107
23
|
throw new Error("syncTable(ctx, items) 缺少 ctx");
|
|
108
24
|
}
|
|
@@ -115,7 +31,6 @@ export const syncTable = (async (ctx: BeflyContext, items: ScanFileResult[]): Pr
|
|
|
115
31
|
if (!ctx.config) {
|
|
116
32
|
throw new Error("syncTable(ctx, items) 缺少 ctx.config");
|
|
117
33
|
}
|
|
118
|
-
|
|
119
34
|
// DbDialect 归一化(允许值与映射关系):
|
|
120
35
|
//
|
|
121
36
|
// | ctx.config.db.type 输入 | 归一化 dbDialect |
|
|
@@ -128,39 +43,34 @@ export const syncTable = (async (ctx: BeflyContext, items: ScanFileResult[]): Pr
|
|
|
128
43
|
// - 这里的归一化
|
|
129
44
|
// - ensureDbVersion / runtime I/O / DDL 分支
|
|
130
45
|
const dbType = String(ctx.config.db?.type || "mysql").toLowerCase();
|
|
131
|
-
let dbDialect
|
|
46
|
+
let dbDialect = "mysql";
|
|
132
47
|
if (dbType === "postgres" || dbType === "postgresql") {
|
|
133
48
|
dbDialect = "postgresql";
|
|
134
|
-
}
|
|
49
|
+
}
|
|
50
|
+
else if (dbType === "sqlite") {
|
|
135
51
|
dbDialect = "sqlite";
|
|
136
52
|
}
|
|
137
|
-
|
|
138
53
|
// 检查数据库版本(复用 ctx.db 的现有连接/事务)
|
|
139
54
|
await ensureDbVersion(dbDialect, ctx.db);
|
|
140
|
-
|
|
141
55
|
const databaseName = ctx.config.db?.database || "";
|
|
142
|
-
const runtime
|
|
56
|
+
const runtime = {
|
|
143
57
|
dbDialect: dbDialect,
|
|
144
58
|
db: ctx.db,
|
|
145
59
|
dbName: databaseName
|
|
146
60
|
};
|
|
147
|
-
|
|
148
61
|
// 处理传入的 tables 数据(来自 scanSources)
|
|
149
62
|
for (const item of items) {
|
|
150
63
|
if (!item || item.type !== "table") {
|
|
151
64
|
continue;
|
|
152
65
|
}
|
|
153
|
-
|
|
154
66
|
if (item.source !== "app" && item.source !== "addon" && item.source !== "core") {
|
|
155
67
|
Logger.warn(`syncTable 跳过未知来源表定义: source=${String(item.source)} fileName=${String(item.fileName)}`);
|
|
156
68
|
continue;
|
|
157
69
|
}
|
|
158
|
-
|
|
159
70
|
// 确定表名:
|
|
160
71
|
// - addon 表:addon_{addonName}_{fileName}
|
|
161
72
|
// - app/core 表:{fileName}
|
|
162
73
|
const baseTableName = snakeCase(item.fileName);
|
|
163
|
-
|
|
164
74
|
let tableName = baseTableName;
|
|
165
75
|
if (item.source === "addon") {
|
|
166
76
|
if (!item.addonName || String(item.addonName).trim() === "") {
|
|
@@ -168,45 +78,39 @@ export const syncTable = (async (ctx: BeflyContext, items: ScanFileResult[]): Pr
|
|
|
168
78
|
}
|
|
169
79
|
tableName = `addon_${snakeCase(item.addonName)}_${baseTableName}`;
|
|
170
80
|
}
|
|
171
|
-
|
|
172
81
|
const tableDefinition = item.content;
|
|
173
82
|
if (!tableDefinition || typeof tableDefinition !== "object") {
|
|
174
83
|
throw new Error(`syncTable 表定义无效: table=${tableName}`);
|
|
175
84
|
}
|
|
176
|
-
|
|
177
85
|
// 为字段属性设置默认值:表定义来自 JSON/扫描结果,字段可能缺省。
|
|
178
86
|
// 缺省会让 diff/DDL 生成出现 undefined vs null 等差异,导致错误的变更判断。
|
|
179
87
|
for (const fieldDef of Object.values(tableDefinition)) {
|
|
180
88
|
applyFieldDefaults(fieldDef);
|
|
181
89
|
}
|
|
182
|
-
|
|
183
90
|
const existsTable = await tableExistsRuntime(runtime, tableName);
|
|
184
|
-
|
|
185
91
|
if (existsTable) {
|
|
186
|
-
await modifyTableRuntime(runtime, tableName, tableDefinition
|
|
187
|
-
}
|
|
188
|
-
|
|
92
|
+
await modifyTableRuntime(runtime, tableName, tableDefinition);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
await createTable(runtime, tableName, tableDefinition);
|
|
189
96
|
}
|
|
190
|
-
|
|
191
97
|
// 记录处理过的表名(用于清理缓存)
|
|
192
98
|
processedTables.push(tableName);
|
|
193
99
|
}
|
|
194
|
-
|
|
195
100
|
// 清理 Redis 缓存(如果有表被处理)
|
|
196
101
|
if (processedTables.length > 0) {
|
|
197
102
|
const cacheKeys = processedTables.map((tableName) => CacheKeys.tableColumns(tableName));
|
|
198
103
|
await ctx.redis.delBatch(cacheKeys);
|
|
199
104
|
}
|
|
200
|
-
}
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
201
107
|
Logger.error({ err: error }, "数据库同步失败");
|
|
202
108
|
throw error;
|
|
203
109
|
}
|
|
204
|
-
})
|
|
205
|
-
|
|
110
|
+
});
|
|
206
111
|
/* ========================================================================== */
|
|
207
112
|
/* 版本/常量/运行时方言状态 */
|
|
208
113
|
/* ========================================================================== */
|
|
209
|
-
|
|
210
114
|
/**
|
|
211
115
|
* 数据库版本要求
|
|
212
116
|
*/
|
|
@@ -215,8 +119,7 @@ const DB_VERSION_REQUIREMENTS = {
|
|
|
215
119
|
POSTGRES_MIN_MAJOR: 17,
|
|
216
120
|
SQLITE_MIN_VERSION: "3.50.0",
|
|
217
121
|
SQLITE_MIN_VERSION_NUM: 35000 // 3 * 10000 + 50 * 100
|
|
218
|
-
}
|
|
219
|
-
|
|
122
|
+
};
|
|
220
123
|
/**
|
|
221
124
|
* 字段变更类型的中文标签映射
|
|
222
125
|
*/
|
|
@@ -227,8 +130,7 @@ const CHANGE_TYPE_LABELS = {
|
|
|
227
130
|
default: "默认值",
|
|
228
131
|
nullable: "可空约束",
|
|
229
132
|
unique: "唯一约束"
|
|
230
|
-
}
|
|
231
|
-
|
|
133
|
+
};
|
|
232
134
|
/**
|
|
233
135
|
* MySQL 表配置
|
|
234
136
|
*/
|
|
@@ -236,24 +138,14 @@ const MYSQL_TABLE_CONFIG = {
|
|
|
236
138
|
ENGINE: "InnoDB",
|
|
237
139
|
CHARSET: "utf8mb4",
|
|
238
140
|
COLLATE: "utf8mb4_0900_ai_ci"
|
|
239
|
-
} as const;
|
|
240
|
-
|
|
241
|
-
type SystemFieldMeta = {
|
|
242
|
-
name: "id" | "created_at" | "updated_at" | "deleted_at" | "state";
|
|
243
|
-
comment: string;
|
|
244
|
-
needsIndex: boolean;
|
|
245
|
-
mysqlDdl: string;
|
|
246
|
-
pgDdl: string;
|
|
247
|
-
sqliteDdl: string;
|
|
248
141
|
};
|
|
249
|
-
|
|
250
142
|
/**
|
|
251
143
|
* 系统字段定义:三处会用到
|
|
252
144
|
* - createTable:建表时追加系统字段列定义
|
|
253
145
|
* - modifyTable:对已存在的表补齐缺失的系统字段
|
|
254
146
|
* - SYSTEM_INDEX_FIELDS:从 needsIndex 派生默认系统索引集合
|
|
255
147
|
*/
|
|
256
|
-
const SYSTEM_FIELDS
|
|
148
|
+
const SYSTEM_FIELDS = [
|
|
257
149
|
{
|
|
258
150
|
name: "id",
|
|
259
151
|
comment: "主键ID",
|
|
@@ -295,23 +187,19 @@ const SYSTEM_FIELDS: ReadonlyArray<SystemFieldMeta> = [
|
|
|
295
187
|
sqliteDdl: "INTEGER NOT NULL DEFAULT 1"
|
|
296
188
|
}
|
|
297
189
|
];
|
|
298
|
-
|
|
299
190
|
/**
|
|
300
191
|
* 需要创建索引的系统字段
|
|
301
192
|
*/
|
|
302
|
-
const SYSTEM_INDEX_FIELDS
|
|
303
|
-
|
|
304
|
-
const SYSTEM_FIELD_META_MAP: Record<string, SystemFieldMeta> = {};
|
|
193
|
+
const SYSTEM_INDEX_FIELDS = SYSTEM_FIELDS.filter((f) => f.needsIndex).map((f) => f.name);
|
|
194
|
+
const SYSTEM_FIELD_META_MAP = {};
|
|
305
195
|
for (const f of SYSTEM_FIELDS) {
|
|
306
196
|
SYSTEM_FIELD_META_MAP[f.name] = f;
|
|
307
197
|
}
|
|
308
|
-
|
|
309
198
|
const SYNC_TABLE_TEST_KIT = {
|
|
310
199
|
DB_VERSION_REQUIREMENTS: DB_VERSION_REQUIREMENTS,
|
|
311
200
|
CHANGE_TYPE_LABELS: CHANGE_TYPE_LABELS,
|
|
312
201
|
MYSQL_TABLE_CONFIG: MYSQL_TABLE_CONFIG,
|
|
313
202
|
SYSTEM_INDEX_FIELDS: SYSTEM_INDEX_FIELDS,
|
|
314
|
-
|
|
315
203
|
getTypeMapping: getTypeMapping,
|
|
316
204
|
quoteIdentifier: quoteIdentifier,
|
|
317
205
|
escapeComment: escapeComment,
|
|
@@ -326,12 +214,10 @@ const SYNC_TABLE_TEST_KIT = {
|
|
|
326
214
|
generateDDLClause: generateDDLClause,
|
|
327
215
|
isCompatibleTypeChange: isCompatibleTypeChange,
|
|
328
216
|
compareFieldDefinition: compareFieldDefinition,
|
|
329
|
-
|
|
330
217
|
tableExistsRuntime: tableExistsRuntime,
|
|
331
218
|
getTableColumnsRuntime: getTableColumnsRuntime,
|
|
332
219
|
getTableIndexesRuntime: getTableIndexesRuntime,
|
|
333
|
-
|
|
334
|
-
createRuntime: (dbDialect: DbDialect, db: SqlExecutor, dbName: string = ""): SyncRuntime => {
|
|
220
|
+
createRuntime: (dbDialect, db, dbName = "") => {
|
|
335
221
|
return {
|
|
336
222
|
dbDialect: dbDialect,
|
|
337
223
|
db: db,
|
|
@@ -339,10 +225,8 @@ const SYNC_TABLE_TEST_KIT = {
|
|
|
339
225
|
};
|
|
340
226
|
}
|
|
341
227
|
};
|
|
342
|
-
|
|
343
228
|
// 测试能力挂载(避免导出零散函数,同时确保运行时存在)
|
|
344
229
|
syncTable.TestKit = SYNC_TABLE_TEST_KIT;
|
|
345
|
-
|
|
346
230
|
// 防御性:避免运行时被误覆盖(只读),但仍保持可枚举/可访问。
|
|
347
231
|
Object.defineProperty(syncTable, "TestKit", {
|
|
348
232
|
value: SYNC_TABLE_TEST_KIT,
|
|
@@ -350,11 +234,10 @@ Object.defineProperty(syncTable, "TestKit", {
|
|
|
350
234
|
enumerable: true,
|
|
351
235
|
configurable: false
|
|
352
236
|
});
|
|
353
|
-
|
|
354
237
|
/**
|
|
355
238
|
* 获取字段类型映射(根据当前数据库类型)
|
|
356
239
|
*/
|
|
357
|
-
function getTypeMapping(dbDialect
|
|
240
|
+
function getTypeMapping(dbDialect) {
|
|
358
241
|
return {
|
|
359
242
|
number: dbDialect === "sqlite" ? "INTEGER" : dbDialect === "postgresql" ? "BIGINT" : "BIGINT",
|
|
360
243
|
string: dbDialect === "sqlite" ? "TEXT" : dbDialect === "postgresql" ? "character varying" : "VARCHAR",
|
|
@@ -365,32 +248,27 @@ function getTypeMapping(dbDialect: DbDialect): Record<string, string> {
|
|
|
365
248
|
array_number_text: dbDialect === "mysql" ? "MEDIUMTEXT" : "TEXT"
|
|
366
249
|
};
|
|
367
250
|
}
|
|
368
|
-
|
|
369
251
|
/* ========================================================================== */
|
|
370
252
|
/* 通用工具与 DDL 片段生成 */
|
|
371
253
|
/* ========================================================================== */
|
|
372
|
-
|
|
373
254
|
/**
|
|
374
255
|
* 根据数据库类型引用标识符
|
|
375
256
|
*/
|
|
376
|
-
function quoteIdentifier(dbDialect
|
|
257
|
+
function quoteIdentifier(dbDialect, identifier) {
|
|
377
258
|
return getDialectByName(dbDialect).quoteIdent(identifier);
|
|
378
259
|
}
|
|
379
|
-
|
|
380
260
|
/**
|
|
381
261
|
* 转义 SQL 注释中的双引号
|
|
382
262
|
*/
|
|
383
|
-
function escapeComment(str
|
|
263
|
+
function escapeComment(str) {
|
|
384
264
|
return String(str).replace(/"/g, '\\"');
|
|
385
265
|
}
|
|
386
|
-
|
|
387
266
|
// 注意:这里刻意不封装“logFieldChange/formatFieldList”之类的一次性工具函数,
|
|
388
267
|
// 以减少抽象层级(按项目要求:能直写就直写)。
|
|
389
|
-
|
|
390
268
|
/**
|
|
391
269
|
* 为字段定义应用默认值
|
|
392
270
|
*/
|
|
393
|
-
function applyFieldDefaults(fieldDef
|
|
271
|
+
function applyFieldDefaults(fieldDef) {
|
|
394
272
|
fieldDef.detail = fieldDef.detail ?? "";
|
|
395
273
|
fieldDef.min = fieldDef.min ?? 0;
|
|
396
274
|
fieldDef.max = fieldDef.max ?? (fieldDef.type === "number" ? Number.MAX_SAFE_INTEGER : 100);
|
|
@@ -401,18 +279,16 @@ function applyFieldDefaults(fieldDef: any): void {
|
|
|
401
279
|
fieldDef.unsigned = fieldDef.unsigned ?? true;
|
|
402
280
|
fieldDef.regexp = fieldDef.regexp ?? null;
|
|
403
281
|
}
|
|
404
|
-
|
|
405
282
|
/**
|
|
406
283
|
* 判断是否为字符串或数组类型(需要长度参数)
|
|
407
284
|
*/
|
|
408
|
-
function isStringOrArrayType(fieldType
|
|
285
|
+
function isStringOrArrayType(fieldType) {
|
|
409
286
|
return fieldType === "string" || fieldType === "array_string" || fieldType === "array_number_string";
|
|
410
287
|
}
|
|
411
|
-
|
|
412
288
|
/**
|
|
413
289
|
* 获取 SQL 数据类型
|
|
414
290
|
*/
|
|
415
|
-
function getSqlType(dbDialect
|
|
291
|
+
function getSqlType(dbDialect, fieldType, fieldMax, unsigned = false) {
|
|
416
292
|
const typeMapping = getTypeMapping(dbDialect);
|
|
417
293
|
if (isStringOrArrayType(fieldType)) {
|
|
418
294
|
return `${typeMapping[fieldType]}(${fieldMax})`;
|
|
@@ -423,15 +299,13 @@ function getSqlType(dbDialect: DbDialect, fieldType: string, fieldMax: number |
|
|
|
423
299
|
}
|
|
424
300
|
return baseType;
|
|
425
301
|
}
|
|
426
|
-
|
|
427
302
|
/**
|
|
428
303
|
* 处理默认值:将 null 或 'null' 字符串转换为对应类型的默认值
|
|
429
304
|
*/
|
|
430
|
-
function resolveDefaultValue(fieldDefault
|
|
305
|
+
function resolveDefaultValue(fieldDefault, fieldType) {
|
|
431
306
|
if (fieldDefault !== null && fieldDefault !== "null") {
|
|
432
307
|
return fieldDefault;
|
|
433
308
|
}
|
|
434
|
-
|
|
435
309
|
switch (fieldType) {
|
|
436
310
|
case "number":
|
|
437
311
|
return 0;
|
|
@@ -448,31 +322,28 @@ function resolveDefaultValue(fieldDefault: any, fieldType: string): any {
|
|
|
448
322
|
return fieldDefault;
|
|
449
323
|
}
|
|
450
324
|
}
|
|
451
|
-
|
|
452
325
|
/**
|
|
453
326
|
* 生成 SQL DEFAULT 子句
|
|
454
327
|
*/
|
|
455
|
-
function generateDefaultSql(actualDefault
|
|
328
|
+
function generateDefaultSql(actualDefault, fieldType) {
|
|
456
329
|
if (fieldType === "text" || fieldType === "array_text" || actualDefault === "null") {
|
|
457
330
|
return "";
|
|
458
331
|
}
|
|
459
|
-
|
|
460
332
|
if (fieldType === "number" || fieldType === "string" || fieldType === "array_string" || fieldType === "array_number_string") {
|
|
461
333
|
if (typeof actualDefault === "number" && !Number.isNaN(actualDefault)) {
|
|
462
334
|
return ` DEFAULT ${actualDefault}`;
|
|
463
|
-
}
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
464
337
|
const escaped = String(actualDefault).replace(/'/g, "''");
|
|
465
338
|
return ` DEFAULT '${escaped}'`;
|
|
466
339
|
}
|
|
467
340
|
}
|
|
468
|
-
|
|
469
341
|
return "";
|
|
470
342
|
}
|
|
471
|
-
|
|
472
343
|
/**
|
|
473
344
|
* 构建索引操作 SQL(统一使用在线策略)
|
|
474
345
|
*/
|
|
475
|
-
function buildIndexSQL(dbDialect
|
|
346
|
+
function buildIndexSQL(dbDialect, tableName, indexName, fieldName, action) {
|
|
476
347
|
// 说明(策略取舍):
|
|
477
348
|
// - MySQL:通过 ALTER TABLE 在线添加/删除索引;配合 ALGORITHM/LOCK 以降低阻塞。
|
|
478
349
|
// - PostgreSQL:CREATE/DROP INDEX CONCURRENTLY 尽量减少锁表(代价是执行更慢/有并发限制)。
|
|
@@ -480,123 +351,108 @@ function buildIndexSQL(dbDialect: DbDialect, tableName: string, indexName: strin
|
|
|
480
351
|
const tableQuoted = quoteIdentifier(dbDialect, tableName);
|
|
481
352
|
const indexQuoted = quoteIdentifier(dbDialect, indexName);
|
|
482
353
|
const fieldQuoted = quoteIdentifier(dbDialect, fieldName);
|
|
483
|
-
|
|
484
354
|
if (dbDialect === "mysql") {
|
|
485
|
-
const parts
|
|
355
|
+
const parts = [];
|
|
486
356
|
if (action === "create") {
|
|
487
357
|
parts.push(`ADD INDEX ${indexQuoted} (${fieldQuoted})`);
|
|
488
|
-
}
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
489
360
|
parts.push(`DROP INDEX ${indexQuoted}`);
|
|
490
361
|
}
|
|
491
362
|
return `ALTER TABLE ${tableQuoted} ALGORITHM=INPLACE, LOCK=NONE, ${parts.join(", ")}`;
|
|
492
363
|
}
|
|
493
|
-
|
|
494
364
|
if (dbDialect === "postgresql") {
|
|
495
365
|
if (action === "create") {
|
|
496
366
|
return `CREATE INDEX CONCURRENTLY IF NOT EXISTS ${indexQuoted} ON ${tableQuoted}(${fieldQuoted})`;
|
|
497
367
|
}
|
|
498
368
|
return `DROP INDEX CONCURRENTLY IF EXISTS ${indexQuoted}`;
|
|
499
369
|
}
|
|
500
|
-
|
|
501
370
|
if (action === "create") {
|
|
502
371
|
return `CREATE INDEX IF NOT EXISTS ${indexQuoted} ON ${tableQuoted}(${fieldQuoted})`;
|
|
503
372
|
}
|
|
504
373
|
return `DROP INDEX IF EXISTS ${indexQuoted}`;
|
|
505
374
|
}
|
|
506
|
-
|
|
507
375
|
/**
|
|
508
376
|
* 获取单个系统字段的列定义(用于 ADD COLUMN 或 CREATE TABLE)
|
|
509
377
|
*/
|
|
510
|
-
function getSystemColumnDef(dbDialect
|
|
378
|
+
function getSystemColumnDef(dbDialect, fieldName) {
|
|
511
379
|
const meta = SYSTEM_FIELD_META_MAP[fieldName];
|
|
512
|
-
if (!meta)
|
|
513
|
-
|
|
380
|
+
if (!meta)
|
|
381
|
+
return null;
|
|
514
382
|
const colQuoted = quoteIdentifier(dbDialect, meta.name);
|
|
515
383
|
if (dbDialect === "mysql") {
|
|
516
384
|
return `${colQuoted} ${meta.mysqlDdl} COMMENT "${escapeComment(meta.comment)}"`;
|
|
517
385
|
}
|
|
518
|
-
|
|
519
386
|
if (dbDialect === "postgresql") {
|
|
520
387
|
return `${colQuoted} ${meta.pgDdl}`;
|
|
521
388
|
}
|
|
522
|
-
|
|
523
389
|
return `${colQuoted} ${meta.sqliteDdl}`;
|
|
524
390
|
}
|
|
525
|
-
|
|
526
391
|
/**
|
|
527
392
|
* 构建系统字段列定义
|
|
528
393
|
*/
|
|
529
|
-
function buildSystemColumnDefs(dbDialect
|
|
530
|
-
const defs
|
|
394
|
+
function buildSystemColumnDefs(dbDialect) {
|
|
395
|
+
const defs = [];
|
|
531
396
|
for (const f of SYSTEM_FIELDS) {
|
|
532
397
|
const d = getSystemColumnDef(dbDialect, f.name);
|
|
533
|
-
if (d)
|
|
398
|
+
if (d)
|
|
399
|
+
defs.push(d);
|
|
534
400
|
}
|
|
535
401
|
return defs;
|
|
536
402
|
}
|
|
537
|
-
|
|
538
403
|
/**
|
|
539
404
|
* 构建业务字段列定义
|
|
540
405
|
*/
|
|
541
|
-
function buildBusinessColumnDefs(dbDialect
|
|
542
|
-
const colDefs
|
|
543
|
-
|
|
406
|
+
function buildBusinessColumnDefs(dbDialect, fields) {
|
|
407
|
+
const colDefs = [];
|
|
544
408
|
for (const [fieldKey, fieldDef] of Object.entries(fields)) {
|
|
545
409
|
const dbFieldName = snakeCase(fieldKey);
|
|
546
410
|
const colQuoted = quoteIdentifier(dbDialect, dbFieldName);
|
|
547
|
-
|
|
548
411
|
const sqlType = getSqlType(dbDialect, fieldDef.type, fieldDef.max, fieldDef.unsigned);
|
|
549
|
-
|
|
550
412
|
const actualDefault = resolveDefaultValue(fieldDef.default, fieldDef.type);
|
|
551
413
|
const defaultSql = generateDefaultSql(actualDefault, fieldDef.type);
|
|
552
|
-
|
|
553
414
|
const uniqueSql = fieldDef.unique ? " UNIQUE" : "";
|
|
554
415
|
const nullableSql = fieldDef.nullable ? " NULL" : " NOT NULL";
|
|
555
|
-
|
|
556
416
|
if (dbDialect === "mysql") {
|
|
557
417
|
colDefs.push(`${colQuoted} ${sqlType}${uniqueSql}${nullableSql}${defaultSql} COMMENT "${escapeComment(fieldDef.name)}"`);
|
|
558
|
-
}
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
559
420
|
colDefs.push(`${colQuoted} ${sqlType}${uniqueSql}${nullableSql}${defaultSql}`);
|
|
560
421
|
}
|
|
561
422
|
}
|
|
562
|
-
|
|
563
423
|
return colDefs;
|
|
564
424
|
}
|
|
565
|
-
|
|
566
425
|
/**
|
|
567
426
|
* 生成字段 DDL 子句(不含 ALTER TABLE 前缀)
|
|
568
427
|
*/
|
|
569
|
-
function generateDDLClause(dbDialect
|
|
428
|
+
function generateDDLClause(dbDialect, fieldKey, fieldDef, isAdd = false) {
|
|
570
429
|
// 说明(策略取舍):
|
|
571
430
|
// - MySQL:ADD/MODIFY 一条子句内可同时表达类型/可空/默认值/注释(同步成本低)。
|
|
572
431
|
// - PostgreSQL:modify 场景这里仅生成 TYPE 变更;默认值/注释等由其他子句或 commentActions 处理。
|
|
573
432
|
// - SQLite:不支持标准化的 MODIFY COLUMN,这里仅提供 ADD COLUMN;复杂变更通过 rebuildSqliteTable 完成。
|
|
574
433
|
const dbFieldName = snakeCase(fieldKey);
|
|
575
434
|
const colQuoted = quoteIdentifier(dbDialect, dbFieldName);
|
|
576
|
-
|
|
577
435
|
const sqlType = getSqlType(dbDialect, fieldDef.type, fieldDef.max, fieldDef.unsigned);
|
|
578
|
-
|
|
579
436
|
const actualDefault = resolveDefaultValue(fieldDef.default, fieldDef.type);
|
|
580
437
|
const defaultSql = generateDefaultSql(actualDefault, fieldDef.type);
|
|
581
|
-
|
|
582
438
|
const uniqueSql = fieldDef.unique ? " UNIQUE" : "";
|
|
583
439
|
const nullableSql = fieldDef.nullable ? " NULL" : " NOT NULL";
|
|
584
|
-
|
|
585
440
|
if (dbDialect === "mysql") {
|
|
586
441
|
return `${isAdd ? "ADD COLUMN" : "MODIFY COLUMN"} ${colQuoted} ${sqlType}${uniqueSql}${nullableSql}${defaultSql} COMMENT "${escapeComment(fieldDef.name)}"`;
|
|
587
442
|
}
|
|
588
443
|
if (dbDialect === "postgresql") {
|
|
589
|
-
if (isAdd)
|
|
444
|
+
if (isAdd)
|
|
445
|
+
return `ADD COLUMN IF NOT EXISTS ${colQuoted} ${sqlType}${uniqueSql}${nullableSql}${defaultSql}`;
|
|
590
446
|
return `ALTER COLUMN ${colQuoted} TYPE ${sqlType}`;
|
|
591
447
|
}
|
|
592
|
-
if (isAdd)
|
|
448
|
+
if (isAdd)
|
|
449
|
+
return `ADD COLUMN IF NOT EXISTS ${colQuoted} ${sqlType}${uniqueSql}${nullableSql}${defaultSql}`;
|
|
593
450
|
return "";
|
|
594
451
|
}
|
|
595
|
-
|
|
596
452
|
/**
|
|
597
453
|
* 安全执行 DDL 语句(MySQL 降级策略)
|
|
598
454
|
*/
|
|
599
|
-
async function executeDDLSafely(db
|
|
455
|
+
async function executeDDLSafely(db, stmt) {
|
|
600
456
|
// MySQL DDL 兼容性/可用性兜底:
|
|
601
457
|
// - 优先执行原语句(通常含 ALGORITHM=INSTANT)。
|
|
602
458
|
// - 若 INSTANT 不可用(版本/表结构限制),降级为 INPLACE 再试。
|
|
@@ -604,16 +460,17 @@ async function executeDDLSafely(db: SqlExecutor, stmt: string): Promise<boolean>
|
|
|
604
460
|
try {
|
|
605
461
|
await db.unsafe(stmt);
|
|
606
462
|
return true;
|
|
607
|
-
}
|
|
463
|
+
}
|
|
464
|
+
catch (error) {
|
|
608
465
|
if (stmt.includes("ALGORITHM=INSTANT")) {
|
|
609
466
|
const inplaceSql = stmt.replace(/ALGORITHM=INSTANT/g, "ALGORITHM=INPLACE");
|
|
610
467
|
try {
|
|
611
468
|
await db.unsafe(inplaceSql);
|
|
612
469
|
return true;
|
|
613
|
-
}
|
|
470
|
+
}
|
|
471
|
+
catch {
|
|
614
472
|
let traditionSql = stmt;
|
|
615
473
|
traditionSql = traditionSql.replace(/\bALGORITHM\s*=\s*(INPLACE|INSTANT)\b\s*,?\s*/g, "").replace(/\bLOCK\s*=\s*(NONE|SHARED|EXCLUSIVE)\b\s*,?\s*/g, "");
|
|
616
|
-
|
|
617
474
|
traditionSql = traditionSql
|
|
618
475
|
.replace(/,\s*,/g, ", ")
|
|
619
476
|
.replace(/,\s*$/g, "")
|
|
@@ -622,16 +479,16 @@ async function executeDDLSafely(db: SqlExecutor, stmt: string): Promise<boolean>
|
|
|
622
479
|
await db.unsafe(traditionSql);
|
|
623
480
|
return true;
|
|
624
481
|
}
|
|
625
|
-
}
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
626
484
|
throw error;
|
|
627
485
|
}
|
|
628
486
|
}
|
|
629
487
|
}
|
|
630
|
-
|
|
631
488
|
/**
|
|
632
489
|
* 判断是否为兼容的类型变更(宽化型变更,无数据丢失风险)
|
|
633
490
|
*/
|
|
634
|
-
function isCompatibleTypeChange(currentType
|
|
491
|
+
function isCompatibleTypeChange(currentType, newType) {
|
|
635
492
|
// 说明:该函数用于“自动同步”里的安全阈值判断。
|
|
636
493
|
// - 允许:宽化型变更(不收缩、不改变语义大类),例如:
|
|
637
494
|
// - INT -> BIGINT(或 tinyint/smallint/mediumint -> 更宽的整型)
|
|
@@ -640,50 +497,28 @@ function isCompatibleTypeChange(currentType: string, newType: string): boolean {
|
|
|
640
497
|
// - 禁止:收缩型变更(BIGINT -> INT、TEXT -> VARCHAR)以及跨大类变更(需人工评估/迁移)。
|
|
641
498
|
const c = String(currentType || "").toLowerCase();
|
|
642
499
|
const n = String(newType || "").toLowerCase();
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
500
|
+
if (c === n)
|
|
501
|
+
return false;
|
|
646
502
|
const cBase = c
|
|
647
503
|
.replace(/\s*unsigned/gi, "")
|
|
648
504
|
.replace(/\([^)]*\)/g, "")
|
|
649
505
|
.trim();
|
|
650
|
-
|
|
651
506
|
const nBase = n
|
|
652
507
|
.replace(/\s*unsigned/gi, "")
|
|
653
508
|
.replace(/\([^)]*\)/g, "")
|
|
654
509
|
.trim();
|
|
655
|
-
|
|
656
510
|
const intTypes = ["tinyint", "smallint", "mediumint", "int", "integer", "bigint"];
|
|
657
511
|
const cIntIdx = intTypes.indexOf(cBase);
|
|
658
512
|
const nIntIdx = intTypes.indexOf(nBase);
|
|
659
513
|
if (cIntIdx !== -1 && nIntIdx !== -1 && nIntIdx > cIntIdx) {
|
|
660
514
|
return true;
|
|
661
515
|
}
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
if (cBase === "character varying" && nBase === "text")
|
|
665
|
-
|
|
516
|
+
if (cBase === "varchar" && (nBase === "text" || nBase === "mediumtext" || nBase === "longtext"))
|
|
517
|
+
return true;
|
|
518
|
+
if (cBase === "character varying" && nBase === "text")
|
|
519
|
+
return true;
|
|
666
520
|
return false;
|
|
667
521
|
}
|
|
668
|
-
|
|
669
|
-
type SyncRuntime = {
|
|
670
|
-
/**
|
|
671
|
-
* 当前数据库方言(mysql/postgresql/sqlite),决定 SQL 片段与元信息查询方式。
|
|
672
|
-
* 约束:必须与 ctx.config.db.type 一致(经归一化)。
|
|
673
|
-
*/
|
|
674
|
-
dbDialect: DbDialect;
|
|
675
|
-
/**
|
|
676
|
-
* SQL 执行器:必须复用 ctx.db。
|
|
677
|
-
* 约束:syncTable 内部禁止新建 DB 连接/事务;runtime 仅保存引用,不拥有生命周期。
|
|
678
|
-
*/
|
|
679
|
-
db: SqlExecutor;
|
|
680
|
-
/**
|
|
681
|
-
* 数据库名:主要用于 MySQL information_schema 查询。
|
|
682
|
-
* 约束:PG/SQLite 可以传空字符串;不要在非 MySQL 方言依赖该值。
|
|
683
|
-
*/
|
|
684
|
-
dbName: string;
|
|
685
|
-
};
|
|
686
|
-
|
|
687
522
|
/* ========================================================================== */
|
|
688
523
|
/* runtime I/O(只读:读库/元信息查询)
|
|
689
524
|
*
|
|
@@ -693,43 +528,41 @@ type SyncRuntime = {
|
|
|
693
528
|
* - 对外不再保留 dbDialect/db/dbName 形式的 wrapper;统一使用 runtime 形态(更直写)。
|
|
694
529
|
*/
|
|
695
530
|
/* ========================================================================== */
|
|
696
|
-
|
|
697
531
|
// ---------------------------------------------------------------------------
|
|
698
532
|
// 读:表是否存在
|
|
699
533
|
// ---------------------------------------------------------------------------
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
534
|
+
async function tableExistsRuntime(runtime, tableName) {
|
|
535
|
+
if (!runtime.db)
|
|
536
|
+
throw new Error("SQL 执行器未初始化");
|
|
703
537
|
try {
|
|
704
538
|
// 统一交由方言层构造 SQL;syncTable 仅决定“要查哪个 schema/db”。
|
|
705
539
|
// - MySQL:传 runtime.dbName(information_schema.table_schema)
|
|
706
540
|
// - PostgreSQL:固定 public(项目约定)
|
|
707
541
|
// - SQLite:忽略 schema
|
|
708
|
-
let schema
|
|
542
|
+
let schema = undefined;
|
|
709
543
|
if (runtime.dbDialect === "mysql") {
|
|
710
544
|
schema = runtime.dbName;
|
|
711
|
-
}
|
|
545
|
+
}
|
|
546
|
+
else if (runtime.dbDialect === "postgresql") {
|
|
712
547
|
schema = "public";
|
|
713
548
|
}
|
|
714
|
-
|
|
715
549
|
const q = getDialectByName(runtime.dbDialect).tableExistsQuery(tableName, schema);
|
|
716
|
-
const res = await runtime.db.unsafe
|
|
550
|
+
const res = await runtime.db.unsafe(q.sql, q.params);
|
|
717
551
|
return (res.data?.[0]?.count || 0) > 0;
|
|
718
|
-
}
|
|
552
|
+
}
|
|
553
|
+
catch (error) {
|
|
719
554
|
const errMsg = String(error?.message || error);
|
|
720
|
-
const outErr
|
|
721
|
-
if (error?.sqlInfo)
|
|
555
|
+
const outErr = new Error(`runtime I/O 失败: op=tableExists table=${tableName} err=${errMsg}`);
|
|
556
|
+
if (error?.sqlInfo)
|
|
557
|
+
outErr.sqlInfo = error.sqlInfo;
|
|
722
558
|
throw outErr;
|
|
723
559
|
}
|
|
724
560
|
}
|
|
725
|
-
|
|
726
561
|
// ---------------------------------------------------------------------------
|
|
727
562
|
// 读:列信息
|
|
728
563
|
// ---------------------------------------------------------------------------
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
const columns: { [key: string]: ColumnInfo } = {};
|
|
732
|
-
|
|
564
|
+
async function getTableColumnsRuntime(runtime, tableName) {
|
|
565
|
+
const columns = {};
|
|
733
566
|
try {
|
|
734
567
|
// 方言差异说明:
|
|
735
568
|
// - MySQL:information_schema.columns 最完整,包含 COLUMN_TYPE 与 COLUMN_COMMENT。
|
|
@@ -737,10 +570,9 @@ async function getTableColumnsRuntime(runtime: SyncRuntime, tableName: string):
|
|
|
737
570
|
// - SQLite:PRAGMA table_info 仅提供 type/notnull/default 等有限信息,无列注释。
|
|
738
571
|
if (runtime.dbDialect === "mysql") {
|
|
739
572
|
const q = getSyncTableColumnsInfoQuery({ dialect: "mysql", table: tableName, dbName: runtime.dbName });
|
|
740
|
-
const result = await runtime.db.unsafe
|
|
573
|
+
const result = await runtime.db.unsafe(q.columns.sql, q.columns.params);
|
|
741
574
|
for (const row of result.data) {
|
|
742
575
|
const defaultValue = row.COLUMN_DEFAULT;
|
|
743
|
-
|
|
744
576
|
columns[row.COLUMN_NAME] = {
|
|
745
577
|
type: row.DATA_TYPE,
|
|
746
578
|
columnType: row.COLUMN_TYPE,
|
|
@@ -751,13 +583,14 @@ async function getTableColumnsRuntime(runtime: SyncRuntime, tableName: string):
|
|
|
751
583
|
comment: row.COLUMN_COMMENT
|
|
752
584
|
};
|
|
753
585
|
}
|
|
754
|
-
}
|
|
586
|
+
}
|
|
587
|
+
else if (runtime.dbDialect === "postgresql") {
|
|
755
588
|
const q = getSyncTableColumnsInfoQuery({ dialect: "postgresql", table: tableName, dbName: runtime.dbName });
|
|
756
|
-
const result = await runtime.db.unsafe
|
|
757
|
-
const comments = q.comments ? (await runtime.db.unsafe
|
|
758
|
-
const commentMap
|
|
759
|
-
for (const r of comments)
|
|
760
|
-
|
|
589
|
+
const result = await runtime.db.unsafe(q.columns.sql, q.columns.params);
|
|
590
|
+
const comments = q.comments ? (await runtime.db.unsafe(q.comments.sql, q.comments.params)).data : [];
|
|
591
|
+
const commentMap = {};
|
|
592
|
+
for (const r of comments)
|
|
593
|
+
commentMap[r.column_name] = r.column_comment;
|
|
761
594
|
for (const row of result.data) {
|
|
762
595
|
columns[row.column_name] = {
|
|
763
596
|
type: row.data_type,
|
|
@@ -769,9 +602,10 @@ async function getTableColumnsRuntime(runtime: SyncRuntime, tableName: string):
|
|
|
769
602
|
comment: commentMap[row.column_name] ?? null
|
|
770
603
|
};
|
|
771
604
|
}
|
|
772
|
-
}
|
|
605
|
+
}
|
|
606
|
+
else if (runtime.dbDialect === "sqlite") {
|
|
773
607
|
const q = getSyncTableColumnsInfoQuery({ dialect: "sqlite", table: tableName, dbName: runtime.dbName });
|
|
774
|
-
const result = await runtime.db.unsafe
|
|
608
|
+
const result = await runtime.db.unsafe(q.columns.sql, q.columns.params);
|
|
775
609
|
for (const row of result.data) {
|
|
776
610
|
let baseType = String(row.type || "").toUpperCase();
|
|
777
611
|
let max = null;
|
|
@@ -791,23 +625,21 @@ async function getTableColumnsRuntime(runtime: SyncRuntime, tableName: string):
|
|
|
791
625
|
};
|
|
792
626
|
}
|
|
793
627
|
}
|
|
794
|
-
|
|
795
628
|
return columns;
|
|
796
|
-
}
|
|
629
|
+
}
|
|
630
|
+
catch (error) {
|
|
797
631
|
const errMsg = String(error?.message || error);
|
|
798
|
-
const outErr
|
|
799
|
-
if (error?.sqlInfo)
|
|
632
|
+
const outErr = new Error(`runtime I/O 失败: op=getTableColumns table=${tableName} err=${errMsg}`);
|
|
633
|
+
if (error?.sqlInfo)
|
|
634
|
+
outErr.sqlInfo = error.sqlInfo;
|
|
800
635
|
throw outErr;
|
|
801
636
|
}
|
|
802
637
|
}
|
|
803
|
-
|
|
804
638
|
// ---------------------------------------------------------------------------
|
|
805
639
|
// 读:索引信息(单列索引)
|
|
806
640
|
// ---------------------------------------------------------------------------
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
const indexes: IndexInfo = {};
|
|
810
|
-
|
|
641
|
+
async function getTableIndexesRuntime(runtime, tableName) {
|
|
642
|
+
const indexes = {};
|
|
811
643
|
try {
|
|
812
644
|
// 方言差异说明:
|
|
813
645
|
// - MySQL:information_schema.statistics 直接给出 index -> column 映射。
|
|
@@ -815,14 +647,16 @@ async function getTableIndexesRuntime(runtime: SyncRuntime, tableName: string):
|
|
|
815
647
|
// - SQLite:PRAGMA index_list + index_info;同样仅收集单列索引,避免多列索引误判。
|
|
816
648
|
if (runtime.dbDialect === "mysql") {
|
|
817
649
|
const q = getSyncTableIndexesQuery({ dialect: "mysql", table: tableName, dbName: runtime.dbName });
|
|
818
|
-
const result = await runtime.db.unsafe
|
|
650
|
+
const result = await runtime.db.unsafe(q.sql, q.params);
|
|
819
651
|
for (const row of result.data) {
|
|
820
|
-
if (!indexes[row.INDEX_NAME])
|
|
652
|
+
if (!indexes[row.INDEX_NAME])
|
|
653
|
+
indexes[row.INDEX_NAME] = [];
|
|
821
654
|
indexes[row.INDEX_NAME].push(row.COLUMN_NAME);
|
|
822
655
|
}
|
|
823
|
-
}
|
|
656
|
+
}
|
|
657
|
+
else if (runtime.dbDialect === "postgresql") {
|
|
824
658
|
const q = getSyncTableIndexesQuery({ dialect: "postgresql", table: tableName, dbName: runtime.dbName });
|
|
825
|
-
const result = await runtime.db.unsafe
|
|
659
|
+
const result = await runtime.db.unsafe(q.sql, q.params);
|
|
826
660
|
for (const row of result.data) {
|
|
827
661
|
const m = /\(([^)]+)\)/.exec(row.indexdef);
|
|
828
662
|
if (m) {
|
|
@@ -830,38 +664,39 @@ async function getTableIndexesRuntime(runtime: SyncRuntime, tableName: string):
|
|
|
830
664
|
indexes[row.indexname] = [col];
|
|
831
665
|
}
|
|
832
666
|
}
|
|
833
|
-
}
|
|
667
|
+
}
|
|
668
|
+
else if (runtime.dbDialect === "sqlite") {
|
|
834
669
|
const quotedTable = quoteIdentifier("sqlite", tableName);
|
|
835
|
-
const list = await runtime.db.unsafe
|
|
670
|
+
const list = await runtime.db.unsafe(`PRAGMA index_list(${quotedTable})`);
|
|
836
671
|
for (const idx of list.data) {
|
|
837
672
|
const quotedIndex = quoteIdentifier("sqlite", idx.name);
|
|
838
|
-
const info = await runtime.db.unsafe
|
|
673
|
+
const info = await runtime.db.unsafe(`PRAGMA index_info(${quotedIndex})`);
|
|
839
674
|
const cols = info.data.map((r) => r.name);
|
|
840
|
-
if (cols.length === 1)
|
|
675
|
+
if (cols.length === 1)
|
|
676
|
+
indexes[idx.name] = cols;
|
|
841
677
|
}
|
|
842
678
|
}
|
|
843
|
-
|
|
844
679
|
return indexes;
|
|
845
|
-
}
|
|
680
|
+
}
|
|
681
|
+
catch (error) {
|
|
846
682
|
const errMsg = String(error?.message || error);
|
|
847
|
-
const outErr
|
|
848
|
-
if (error?.sqlInfo)
|
|
683
|
+
const outErr = new Error(`runtime I/O 失败: op=getTableIndexes table=${tableName} err=${errMsg}`);
|
|
684
|
+
if (error?.sqlInfo)
|
|
685
|
+
outErr.sqlInfo = error.sqlInfo;
|
|
849
686
|
throw outErr;
|
|
850
687
|
}
|
|
851
688
|
}
|
|
852
|
-
|
|
853
689
|
// ---------------------------------------------------------------------------
|
|
854
690
|
// 读:数据库版本
|
|
855
691
|
// ---------------------------------------------------------------------------
|
|
856
|
-
|
|
857
692
|
/**
|
|
858
693
|
* 数据库版本检查(按方言)
|
|
859
694
|
*/
|
|
860
|
-
async function ensureDbVersion(dbDialect
|
|
861
|
-
if (!db)
|
|
862
|
-
|
|
695
|
+
async function ensureDbVersion(dbDialect, db) {
|
|
696
|
+
if (!db)
|
|
697
|
+
throw new Error("SQL 执行器未初始化");
|
|
863
698
|
if (dbDialect === "mysql") {
|
|
864
|
-
const r = await db.unsafe
|
|
699
|
+
const r = await db.unsafe("SELECT VERSION() AS version");
|
|
865
700
|
if (!r.data || r.data.length === 0 || !r.data[0]?.version) {
|
|
866
701
|
throw new Error("无法获取 MySQL 版本信息");
|
|
867
702
|
}
|
|
@@ -872,9 +707,8 @@ async function ensureDbVersion(dbDialect: DbDialect, db: SqlExecutor): Promise<v
|
|
|
872
707
|
}
|
|
873
708
|
return;
|
|
874
709
|
}
|
|
875
|
-
|
|
876
710
|
if (dbDialect === "postgresql") {
|
|
877
|
-
const r = await db.unsafe
|
|
711
|
+
const r = await db.unsafe("SELECT version() AS version");
|
|
878
712
|
if (!r.data || r.data.length === 0 || !r.data[0]?.version) {
|
|
879
713
|
throw new Error("无法获取 PostgreSQL 版本信息");
|
|
880
714
|
}
|
|
@@ -886,9 +720,8 @@ async function ensureDbVersion(dbDialect: DbDialect, db: SqlExecutor): Promise<v
|
|
|
886
720
|
}
|
|
887
721
|
return;
|
|
888
722
|
}
|
|
889
|
-
|
|
890
723
|
if (dbDialect === "sqlite") {
|
|
891
|
-
const r = await db.unsafe
|
|
724
|
+
const r = await db.unsafe("SELECT sqlite_version() AS version");
|
|
892
725
|
if (!r.data || r.data.length === 0 || !r.data[0]?.version) {
|
|
893
726
|
throw new Error("无法获取 SQLite 版本信息");
|
|
894
727
|
}
|
|
@@ -903,13 +736,11 @@ async function ensureDbVersion(dbDialect: DbDialect, db: SqlExecutor): Promise<v
|
|
|
903
736
|
return;
|
|
904
737
|
}
|
|
905
738
|
}
|
|
906
|
-
|
|
907
739
|
/**
|
|
908
740
|
* 比较字段定义变化
|
|
909
741
|
*/
|
|
910
|
-
function compareFieldDefinition(dbDialect
|
|
911
|
-
const changes
|
|
912
|
-
|
|
742
|
+
function compareFieldDefinition(dbDialect, existingColumn, fieldDef) {
|
|
743
|
+
const changes = [];
|
|
913
744
|
// SQLite 元信息能力较弱:
|
|
914
745
|
// - 列注释:sqlite 无 information_schema 注释,PRAGMA table_info 也不提供 comment
|
|
915
746
|
// - 字符串长度:sqlite 类型系统宽松,长度/类型信息不稳定(易产生误报)
|
|
@@ -923,7 +754,6 @@ function compareFieldDefinition(dbDialect: DbDialect, existingColumn: ColumnInfo
|
|
|
923
754
|
});
|
|
924
755
|
}
|
|
925
756
|
}
|
|
926
|
-
|
|
927
757
|
if (dbDialect !== "sqlite") {
|
|
928
758
|
const currentComment = existingColumn.comment || "";
|
|
929
759
|
if (currentComment !== fieldDef.name) {
|
|
@@ -934,11 +764,9 @@ function compareFieldDefinition(dbDialect: DbDialect, existingColumn: ColumnInfo
|
|
|
934
764
|
});
|
|
935
765
|
}
|
|
936
766
|
}
|
|
937
|
-
|
|
938
767
|
const typeMapping = getTypeMapping(dbDialect);
|
|
939
768
|
const expectedType = typeMapping[fieldDef.type].toLowerCase();
|
|
940
769
|
const currentType = existingColumn.type.toLowerCase();
|
|
941
|
-
|
|
942
770
|
if (currentType !== expectedType) {
|
|
943
771
|
changes.push({
|
|
944
772
|
type: "datatype",
|
|
@@ -946,7 +774,6 @@ function compareFieldDefinition(dbDialect: DbDialect, existingColumn: ColumnInfo
|
|
|
946
774
|
expected: expectedType
|
|
947
775
|
});
|
|
948
776
|
}
|
|
949
|
-
|
|
950
777
|
const expectedNullable = fieldDef.nullable;
|
|
951
778
|
if (existingColumn.nullable !== expectedNullable) {
|
|
952
779
|
changes.push({
|
|
@@ -955,7 +782,6 @@ function compareFieldDefinition(dbDialect: DbDialect, existingColumn: ColumnInfo
|
|
|
955
782
|
expected: expectedNullable
|
|
956
783
|
});
|
|
957
784
|
}
|
|
958
|
-
|
|
959
785
|
const expectedDefault = resolveDefaultValue(fieldDef.default, fieldDef.type);
|
|
960
786
|
if (String(existingColumn.defaultValue) !== String(expectedDefault)) {
|
|
961
787
|
changes.push({
|
|
@@ -964,18 +790,15 @@ function compareFieldDefinition(dbDialect: DbDialect, existingColumn: ColumnInfo
|
|
|
964
790
|
expected: expectedDefault
|
|
965
791
|
});
|
|
966
792
|
}
|
|
967
|
-
|
|
968
793
|
return changes;
|
|
969
794
|
}
|
|
970
|
-
|
|
971
795
|
/* ========================================================================== */
|
|
972
796
|
/* plan/apply & 建表/改表(核心同步逻辑) */
|
|
973
797
|
/* ========================================================================== */
|
|
974
|
-
|
|
975
798
|
/**
|
|
976
799
|
* SQLite 重建表迁移(简化版)
|
|
977
800
|
*/
|
|
978
|
-
async function rebuildSqliteTable(runtime
|
|
801
|
+
async function rebuildSqliteTable(runtime, tableName, fields) {
|
|
979
802
|
// 说明:SQLite ALTER TABLE 能力有限(尤其是修改列类型/默认值/约束)。
|
|
980
803
|
// 策略:创建临时表 -> 复制“交集列”数据 -> 删除旧表 -> 临时表改名。
|
|
981
804
|
// - 只复制 targetCols 与 existingCols 的交集,避免因新增列/删除列导致 INSERT 失败。
|
|
@@ -983,66 +806,66 @@ async function rebuildSqliteTable(runtime: SyncRuntime, tableName: string, field
|
|
|
983
806
|
if (runtime.dbDialect !== "sqlite") {
|
|
984
807
|
throw new Error(`rebuildSqliteTable 仅支持 sqlite 方言,当前: ${String(runtime.dbDialect)}`);
|
|
985
808
|
}
|
|
986
|
-
|
|
987
809
|
const quotedSourceTable = quoteIdentifier("sqlite", tableName);
|
|
988
|
-
const info = await runtime.db.unsafe
|
|
810
|
+
const info = await runtime.db.unsafe(`PRAGMA table_info(${quotedSourceTable})`);
|
|
989
811
|
const existingCols = info.data.map((r) => r.name);
|
|
990
812
|
const businessCols = Object.keys(fields).map((k) => snakeCase(k));
|
|
991
813
|
const targetCols = ["id", "created_at", "updated_at", "deleted_at", "state", ...businessCols];
|
|
992
814
|
const tmpTable = `${tableName}__tmp__${Date.now()}`;
|
|
993
|
-
|
|
994
815
|
await createTable(runtime, tmpTable, fields);
|
|
995
|
-
|
|
996
816
|
const commonCols = targetCols.filter((c) => existingCols.includes(c));
|
|
997
817
|
if (commonCols.length > 0) {
|
|
998
818
|
const colsSql = commonCols.map((c) => quoteIdentifier("sqlite", c)).join(", ");
|
|
999
819
|
const quotedTmpTable = quoteIdentifier("sqlite", tmpTable);
|
|
1000
820
|
await runtime.db.unsafe(`INSERT INTO ${quotedTmpTable} (${colsSql}) SELECT ${colsSql} FROM ${quotedSourceTable}`);
|
|
1001
821
|
}
|
|
1002
|
-
|
|
1003
822
|
await runtime.db.unsafe(`DROP TABLE ${quotedSourceTable}`);
|
|
1004
823
|
const quotedTmpTable = quoteIdentifier("sqlite", tmpTable);
|
|
1005
824
|
await runtime.db.unsafe(`ALTER TABLE ${quotedTmpTable} RENAME TO ${quotedSourceTable}`);
|
|
1006
825
|
}
|
|
1007
|
-
|
|
1008
826
|
/**
|
|
1009
827
|
* 将表结构计划应用到数据库
|
|
1010
828
|
*/
|
|
1011
|
-
async function applyTablePlan(runtime
|
|
1012
|
-
if (!plan || !plan.changed)
|
|
1013
|
-
|
|
829
|
+
async function applyTablePlan(runtime, tableName, fields, plan) {
|
|
830
|
+
if (!plan || !plan.changed)
|
|
831
|
+
return;
|
|
1014
832
|
// A) 结构变更(ADD/MODIFY):SQLite 走重建表;其余方言走 ALTER TABLE
|
|
1015
833
|
if (runtime.dbDialect === "sqlite") {
|
|
1016
834
|
if (plan.modifyClauses.length > 0 || plan.defaultClauses.length > 0) {
|
|
1017
835
|
await rebuildSqliteTable(runtime, tableName, fields);
|
|
1018
|
-
}
|
|
836
|
+
}
|
|
837
|
+
else {
|
|
1019
838
|
for (const c of plan.addClauses) {
|
|
1020
839
|
const stmt = `ALTER TABLE ${quoteIdentifier(runtime.dbDialect, tableName)} ${c}`;
|
|
1021
840
|
await runtime.db.unsafe(stmt);
|
|
1022
841
|
}
|
|
1023
842
|
}
|
|
1024
|
-
}
|
|
843
|
+
}
|
|
844
|
+
else {
|
|
1025
845
|
const clauses = [...plan.addClauses, ...plan.modifyClauses];
|
|
1026
846
|
if (clauses.length > 0) {
|
|
1027
847
|
const tableQuoted = quoteIdentifier(runtime.dbDialect, tableName);
|
|
1028
848
|
const stmt = runtime.dbDialect === "mysql" ? `ALTER TABLE ${tableQuoted} ALGORITHM=INSTANT, LOCK=NONE, ${clauses.join(", ")}` : `ALTER TABLE ${tableQuoted} ${clauses.join(", ")}`;
|
|
1029
|
-
if (runtime.dbDialect === "mysql")
|
|
1030
|
-
|
|
849
|
+
if (runtime.dbDialect === "mysql")
|
|
850
|
+
await executeDDLSafely(runtime.db, stmt);
|
|
851
|
+
else
|
|
852
|
+
await runtime.db.unsafe(stmt);
|
|
1031
853
|
}
|
|
1032
854
|
}
|
|
1033
|
-
|
|
1034
855
|
// B) 默认值变更:SQLite 不支持在线修改默认值(需要重建表),其余方言按子句执行
|
|
1035
856
|
if (plan.defaultClauses.length > 0) {
|
|
1036
857
|
if (runtime.dbDialect === "sqlite") {
|
|
1037
858
|
Logger.warn(`SQLite 不支持修改默认值,表 ${tableName} 的默认值变更已跳过`);
|
|
1038
|
-
}
|
|
859
|
+
}
|
|
860
|
+
else {
|
|
1039
861
|
const tableQuoted = quoteIdentifier(runtime.dbDialect, tableName);
|
|
1040
862
|
const stmt = runtime.dbDialect === "mysql" ? `ALTER TABLE ${tableQuoted} ALGORITHM=INSTANT, LOCK=NONE, ${plan.defaultClauses.join(", ")}` : `ALTER TABLE ${tableQuoted} ${plan.defaultClauses.join(", ")}`;
|
|
1041
|
-
if (runtime.dbDialect === "mysql")
|
|
1042
|
-
|
|
863
|
+
if (runtime.dbDialect === "mysql")
|
|
864
|
+
await executeDDLSafely(runtime.db, stmt);
|
|
865
|
+
else
|
|
866
|
+
await runtime.db.unsafe(stmt);
|
|
1043
867
|
}
|
|
1044
868
|
}
|
|
1045
|
-
|
|
1046
869
|
// C) 索引动作:不同方言的 DDL 策略由 buildIndexSQL 统一生成
|
|
1047
870
|
for (const act of plan.indexActions) {
|
|
1048
871
|
const stmt = buildIndexSQL(runtime.dbDialect, tableName, act.indexName, act.fieldName, act.action);
|
|
@@ -1050,15 +873,16 @@ async function applyTablePlan(runtime: SyncRuntime, tableName: string, fields: R
|
|
|
1050
873
|
await runtime.db.unsafe(stmt);
|
|
1051
874
|
if (act.action === "create") {
|
|
1052
875
|
Logger.debug(`[索引变化] 新建索引 ${tableName}.${act.indexName} (${act.fieldName})`);
|
|
1053
|
-
}
|
|
876
|
+
}
|
|
877
|
+
else {
|
|
1054
878
|
Logger.debug(`[索引变化] 删除索引 ${tableName}.${act.indexName} (${act.fieldName})`);
|
|
1055
879
|
}
|
|
1056
|
-
}
|
|
880
|
+
}
|
|
881
|
+
catch (error) {
|
|
1057
882
|
Logger.error({ err: error, table: tableName, index: act.indexName, field: act.fieldName }, `${act.action === "create" ? "创建" : "删除"}索引失败`);
|
|
1058
883
|
throw error;
|
|
1059
884
|
}
|
|
1060
885
|
}
|
|
1061
|
-
|
|
1062
886
|
// D) PG 列注释:独立 SQL 执行(COMMENT ON COLUMN)
|
|
1063
887
|
if (runtime.dbDialect === "postgresql" && plan.commentActions && plan.commentActions.length > 0) {
|
|
1064
888
|
for (const stmt of plan.commentActions) {
|
|
@@ -1066,20 +890,16 @@ async function applyTablePlan(runtime: SyncRuntime, tableName: string, fields: R
|
|
|
1066
890
|
}
|
|
1067
891
|
}
|
|
1068
892
|
}
|
|
1069
|
-
|
|
1070
893
|
/**
|
|
1071
894
|
* 创建表(包含系统字段和业务字段)
|
|
1072
895
|
*/
|
|
1073
|
-
async function createTable(runtime
|
|
896
|
+
async function createTable(runtime, tableName, fields, systemIndexFields = SYSTEM_INDEX_FIELDS) {
|
|
1074
897
|
const colDefs = [...buildSystemColumnDefs(runtime.dbDialect), ...buildBusinessColumnDefs(runtime.dbDialect, fields)];
|
|
1075
|
-
|
|
1076
898
|
const cols = colDefs.join(",\n ");
|
|
1077
899
|
const tableQuoted = quoteIdentifier(runtime.dbDialect, tableName);
|
|
1078
900
|
const { ENGINE, CHARSET, COLLATE } = MYSQL_TABLE_CONFIG;
|
|
1079
901
|
const createSQL = runtime.dbDialect === "mysql" ? `CREATE TABLE ${tableQuoted} (\n ${cols}\n ) ENGINE=${ENGINE} DEFAULT CHARSET=${CHARSET} COLLATE=${COLLATE}` : `CREATE TABLE ${tableQuoted} (\n ${cols}\n )`;
|
|
1080
|
-
|
|
1081
902
|
await runtime.db.unsafe(createSQL);
|
|
1082
|
-
|
|
1083
903
|
if (runtime.dbDialect === "postgresql") {
|
|
1084
904
|
for (const f of SYSTEM_FIELDS) {
|
|
1085
905
|
const escaped = String(f.comment).replace(/'/g, "''");
|
|
@@ -1087,11 +907,9 @@ async function createTable(runtime: SyncRuntime, tableName: string, fields: Reco
|
|
|
1087
907
|
const stmt = `COMMENT ON COLUMN ${tableQuoted}.${colQuoted} IS '${escaped}'`;
|
|
1088
908
|
await runtime.db.unsafe(stmt);
|
|
1089
909
|
}
|
|
1090
|
-
|
|
1091
910
|
for (const [fieldKey, fieldDef] of Object.entries(fields)) {
|
|
1092
911
|
const dbFieldName = snakeCase(fieldKey);
|
|
1093
912
|
const colQuoted = quoteIdentifier(runtime.dbDialect, dbFieldName);
|
|
1094
|
-
|
|
1095
913
|
const fieldName = fieldDef.name && fieldDef.name !== "null" ? String(fieldDef.name) : "";
|
|
1096
914
|
const escaped = fieldName.replace(/'/g, "''");
|
|
1097
915
|
const valueSql = fieldName ? `'${escaped}'` : "NULL";
|
|
@@ -1099,14 +917,11 @@ async function createTable(runtime: SyncRuntime, tableName: string, fields: Reco
|
|
|
1099
917
|
await runtime.db.unsafe(stmt);
|
|
1100
918
|
}
|
|
1101
919
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
let existingIndexes: Record<string, string[]> = {};
|
|
920
|
+
const indexTasks = [];
|
|
921
|
+
let existingIndexes = {};
|
|
1106
922
|
if (runtime.dbDialect === "mysql") {
|
|
1107
923
|
existingIndexes = await getTableIndexesRuntime(runtime, tableName);
|
|
1108
924
|
}
|
|
1109
|
-
|
|
1110
925
|
for (const sysField of systemIndexFields) {
|
|
1111
926
|
const indexName = `idx_${sysField}`;
|
|
1112
927
|
if (runtime.dbDialect === "mysql" && existingIndexes[indexName]) {
|
|
@@ -1115,10 +930,8 @@ async function createTable(runtime: SyncRuntime, tableName: string, fields: Reco
|
|
|
1115
930
|
const stmt = buildIndexSQL(runtime.dbDialect, tableName, indexName, sysField, "create");
|
|
1116
931
|
indexTasks.push(runtime.db.unsafe(stmt));
|
|
1117
932
|
}
|
|
1118
|
-
|
|
1119
933
|
for (const [fieldKey, fieldDef] of Object.entries(fields)) {
|
|
1120
934
|
const dbFieldName = snakeCase(fieldKey);
|
|
1121
|
-
|
|
1122
935
|
if (fieldDef.index === true && fieldDef.unique !== true) {
|
|
1123
936
|
const indexName = `idx_${dbFieldName}`;
|
|
1124
937
|
if (runtime.dbDialect === "mysql" && existingIndexes[indexName]) {
|
|
@@ -1128,75 +941,63 @@ async function createTable(runtime: SyncRuntime, tableName: string, fields: Reco
|
|
|
1128
941
|
indexTasks.push(runtime.db.unsafe(stmt));
|
|
1129
942
|
}
|
|
1130
943
|
}
|
|
1131
|
-
|
|
1132
944
|
if (indexTasks.length > 0) {
|
|
1133
945
|
await Promise.all(indexTasks);
|
|
1134
946
|
}
|
|
1135
947
|
}
|
|
1136
|
-
|
|
1137
|
-
async function modifyTableRuntime(runtime: SyncRuntime, tableName: string, fields: Record<string, FieldDefinition>): Promise<TablePlan> {
|
|
948
|
+
async function modifyTableRuntime(runtime, tableName, fields) {
|
|
1138
949
|
// 1) 读取现有元信息(列/索引)
|
|
1139
950
|
const existingColumns = await getTableColumnsRuntime(runtime, tableName);
|
|
1140
951
|
const existingIndexes = await getTableIndexesRuntime(runtime, tableName);
|
|
1141
|
-
|
|
1142
952
|
// 2) 规划变更(先 plan,后统一 apply)
|
|
1143
953
|
let changed = false;
|
|
1144
|
-
|
|
1145
|
-
const
|
|
1146
|
-
const
|
|
1147
|
-
const
|
|
1148
|
-
const indexActions: Array<{ action: "create" | "drop"; indexName: string; fieldName: string }> = [];
|
|
1149
|
-
|
|
954
|
+
const addClauses = [];
|
|
955
|
+
const modifyClauses = [];
|
|
956
|
+
const defaultClauses = [];
|
|
957
|
+
const indexActions = [];
|
|
1150
958
|
// 3) 对比业务字段:新增/变更(类型/长度/可空/默认值/注释)
|
|
1151
959
|
for (const [fieldKey, fieldDef] of Object.entries(fields)) {
|
|
1152
960
|
const dbFieldName = snakeCase(fieldKey);
|
|
1153
|
-
|
|
1154
961
|
if (existingColumns[dbFieldName]) {
|
|
1155
962
|
const comparison = compareFieldDefinition(runtime.dbDialect, existingColumns[dbFieldName], fieldDef);
|
|
1156
963
|
if (comparison.length > 0) {
|
|
1157
964
|
for (const c of comparison) {
|
|
1158
|
-
const changeLabel = CHANGE_TYPE_LABELS[c.type
|
|
965
|
+
const changeLabel = CHANGE_TYPE_LABELS[c.type] || "未知";
|
|
1159
966
|
Logger.debug(` ~ 修改 ${dbFieldName} ${changeLabel}: ${c.current} -> ${c.expected}`);
|
|
1160
967
|
}
|
|
1161
|
-
|
|
1162
968
|
if (isStringOrArrayType(fieldDef.type) && existingColumns[dbFieldName].max && fieldDef.max !== null) {
|
|
1163
|
-
if (existingColumns[dbFieldName].max
|
|
969
|
+
if (existingColumns[dbFieldName].max > fieldDef.max) {
|
|
1164
970
|
Logger.warn(`[跳过危险变更] ${tableName}.${dbFieldName} 长度收缩 ${existingColumns[dbFieldName].max} -> ${fieldDef.max} 已被跳过(需手动处理)`);
|
|
1165
971
|
}
|
|
1166
972
|
}
|
|
1167
|
-
|
|
1168
973
|
const hasTypeChange = comparison.some((c) => c.type === "datatype");
|
|
1169
974
|
const hasLengthChange = comparison.some((c) => c.type === "length");
|
|
1170
975
|
const onlyDefaultChanged = comparison.every((c) => c.type === "default");
|
|
1171
976
|
const defaultChanged = comparison.some((c) => c.type === "default");
|
|
1172
|
-
|
|
1173
977
|
if (hasTypeChange) {
|
|
1174
978
|
const typeChange = comparison.find((c) => c.type === "datatype");
|
|
1175
979
|
const currentType = String(typeChange?.current || "").toLowerCase();
|
|
1176
980
|
const typeMapping = getTypeMapping(runtime.dbDialect);
|
|
1177
981
|
const expectedType = typeMapping[fieldDef.type]?.toLowerCase() || "";
|
|
1178
|
-
|
|
1179
982
|
if (!isCompatibleTypeChange(currentType, expectedType)) {
|
|
1180
983
|
const errorMsg = [`禁止字段类型变更: ${tableName}.${dbFieldName}`, `当前类型: ${typeChange?.current}`, `目标类型: ${typeChange?.expected}`, "说明: 仅允许宽化型变更(如 INT->BIGINT, VARCHAR->TEXT),其他类型变更需要手动处理"].join("\n");
|
|
1181
984
|
throw new Error(errorMsg);
|
|
1182
985
|
}
|
|
1183
986
|
Logger.debug(`[兼容类型变更] ${tableName}.${dbFieldName} ${currentType} -> ${expectedType}`);
|
|
1184
987
|
}
|
|
1185
|
-
|
|
1186
988
|
if (defaultChanged) {
|
|
1187
989
|
const actualDefault = resolveDefaultValue(fieldDef.default, fieldDef.type);
|
|
1188
|
-
|
|
1189
|
-
let v: string | null = null;
|
|
990
|
+
let v = null;
|
|
1190
991
|
if (actualDefault !== "null") {
|
|
1191
992
|
const defaultSql = generateDefaultSql(actualDefault, fieldDef.type);
|
|
1192
993
|
v = defaultSql.trim().replace(/^DEFAULT\s+/, "");
|
|
1193
994
|
}
|
|
1194
|
-
|
|
1195
995
|
if (v !== null && v !== "") {
|
|
1196
996
|
if (runtime.dbDialect === "postgresql") {
|
|
1197
997
|
const colQuoted = quoteIdentifier(runtime.dbDialect, dbFieldName);
|
|
1198
998
|
defaultClauses.push(`ALTER COLUMN ${colQuoted} SET DEFAULT ${v}`);
|
|
1199
|
-
}
|
|
999
|
+
}
|
|
1000
|
+
else if (runtime.dbDialect === "mysql" && onlyDefaultChanged) {
|
|
1200
1001
|
if (fieldDef.type !== "text") {
|
|
1201
1002
|
const colQuoted = quoteIdentifier(runtime.dbDialect, dbFieldName);
|
|
1202
1003
|
defaultClauses.push(`ALTER COLUMN ${colQuoted} SET DEFAULT ${v}`);
|
|
@@ -1204,26 +1005,26 @@ async function modifyTableRuntime(runtime: SyncRuntime, tableName: string, field
|
|
|
1204
1005
|
}
|
|
1205
1006
|
}
|
|
1206
1007
|
}
|
|
1207
|
-
|
|
1208
1008
|
if (!onlyDefaultChanged) {
|
|
1209
1009
|
let skipModify = false;
|
|
1210
1010
|
if (hasLengthChange && isStringOrArrayType(fieldDef.type) && existingColumns[dbFieldName].max && fieldDef.max !== null) {
|
|
1211
|
-
const isShrink = existingColumns[dbFieldName].max
|
|
1212
|
-
if (isShrink)
|
|
1011
|
+
const isShrink = existingColumns[dbFieldName].max > fieldDef.max;
|
|
1012
|
+
if (isShrink)
|
|
1013
|
+
skipModify = true;
|
|
1213
1014
|
}
|
|
1214
|
-
|
|
1215
|
-
|
|
1015
|
+
if (!skipModify)
|
|
1016
|
+
modifyClauses.push(generateDDLClause(runtime.dbDialect, fieldKey, fieldDef, false));
|
|
1216
1017
|
}
|
|
1217
1018
|
changed = true;
|
|
1218
1019
|
}
|
|
1219
|
-
}
|
|
1020
|
+
}
|
|
1021
|
+
else {
|
|
1220
1022
|
addClauses.push(generateDDLClause(runtime.dbDialect, fieldKey, fieldDef, true));
|
|
1221
1023
|
changed = true;
|
|
1222
1024
|
}
|
|
1223
1025
|
}
|
|
1224
|
-
|
|
1225
1026
|
// 4) 补齐系统字段(除 id 外)
|
|
1226
|
-
const systemFieldNames = SYSTEM_FIELDS.filter((f) => f.name !== "id").map((f) => f.name)
|
|
1027
|
+
const systemFieldNames = SYSTEM_FIELDS.filter((f) => f.name !== "id").map((f) => f.name);
|
|
1227
1028
|
for (const sysFieldName of systemFieldNames) {
|
|
1228
1029
|
if (!existingColumns[sysFieldName]) {
|
|
1229
1030
|
const colDef = getSystemColumnDef(runtime.dbDialect, sysFieldName);
|
|
@@ -1234,7 +1035,6 @@ async function modifyTableRuntime(runtime: SyncRuntime, tableName: string, field
|
|
|
1234
1035
|
}
|
|
1235
1036
|
}
|
|
1236
1037
|
}
|
|
1237
|
-
|
|
1238
1038
|
// 5) 索引动作:系统字段索引 + 业务字段单列索引
|
|
1239
1039
|
for (const sysField of SYSTEM_INDEX_FIELDS) {
|
|
1240
1040
|
const idxName = `idx_${sysField}`;
|
|
@@ -1244,28 +1044,25 @@ async function modifyTableRuntime(runtime: SyncRuntime, tableName: string, field
|
|
|
1244
1044
|
changed = true;
|
|
1245
1045
|
}
|
|
1246
1046
|
}
|
|
1247
|
-
|
|
1248
1047
|
for (const [fieldKey, fieldDef] of Object.entries(fields)) {
|
|
1249
1048
|
const dbFieldName = snakeCase(fieldKey);
|
|
1250
|
-
|
|
1251
1049
|
const indexName = `idx_${dbFieldName}`;
|
|
1252
1050
|
if (fieldDef.index && !fieldDef.unique && !existingIndexes[indexName]) {
|
|
1253
1051
|
indexActions.push({ action: "create", indexName: indexName, fieldName: dbFieldName });
|
|
1254
1052
|
changed = true;
|
|
1255
|
-
}
|
|
1053
|
+
}
|
|
1054
|
+
else if (!fieldDef.index && existingIndexes[indexName] && existingIndexes[indexName].length === 1) {
|
|
1256
1055
|
indexActions.push({ action: "drop", indexName: indexName, fieldName: dbFieldName });
|
|
1257
1056
|
changed = true;
|
|
1258
1057
|
}
|
|
1259
1058
|
}
|
|
1260
|
-
|
|
1261
1059
|
// 6) PG 注释动作(MySQL 在列定义里带 COMMENT;SQLite 无列注释)
|
|
1262
|
-
const commentActions
|
|
1060
|
+
const commentActions = [];
|
|
1263
1061
|
if (runtime.dbDialect === "postgresql") {
|
|
1264
1062
|
const tableQuoted = quoteIdentifier(runtime.dbDialect, tableName);
|
|
1265
1063
|
for (const [fieldKey, fieldDef] of Object.entries(fields)) {
|
|
1266
1064
|
const dbFieldName = snakeCase(fieldKey);
|
|
1267
1065
|
const colQuoted = quoteIdentifier(runtime.dbDialect, dbFieldName);
|
|
1268
|
-
|
|
1269
1066
|
if (existingColumns[dbFieldName]) {
|
|
1270
1067
|
const curr = existingColumns[dbFieldName].comment || "";
|
|
1271
1068
|
const want = fieldDef.name && fieldDef.name !== "null" ? String(fieldDef.name) : "";
|
|
@@ -1277,11 +1074,9 @@ async function modifyTableRuntime(runtime: SyncRuntime, tableName: string, field
|
|
|
1277
1074
|
}
|
|
1278
1075
|
}
|
|
1279
1076
|
}
|
|
1280
|
-
|
|
1281
1077
|
// 7) 汇总计划并应用
|
|
1282
1078
|
changed = addClauses.length > 0 || modifyClauses.length > 0 || defaultClauses.length > 0 || indexActions.length > 0 || commentActions.length > 0;
|
|
1283
|
-
|
|
1284
|
-
const plan: TablePlan = {
|
|
1079
|
+
const plan = {
|
|
1285
1080
|
changed: changed,
|
|
1286
1081
|
addClauses: addClauses,
|
|
1287
1082
|
modifyClauses: modifyClauses,
|
|
@@ -1289,10 +1084,8 @@ async function modifyTableRuntime(runtime: SyncRuntime, tableName: string, field
|
|
|
1289
1084
|
indexActions: indexActions,
|
|
1290
1085
|
commentActions: commentActions
|
|
1291
1086
|
};
|
|
1292
|
-
|
|
1293
1087
|
if (plan.changed) {
|
|
1294
1088
|
await applyTablePlan(runtime, tableName, fields, plan);
|
|
1295
1089
|
}
|
|
1296
|
-
|
|
1297
1090
|
return plan;
|
|
1298
1091
|
}
|