befly 2.2.1 → 2.3.0
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/checks/table.js +44 -8
- package/config/env.js +6 -1
- package/package.json +1 -1
- package/scripts/syncDb.js +282 -41
- package/tables/tool.json +1 -1
package/checks/table.js
CHANGED
|
@@ -110,14 +110,41 @@ export const checkTable = async () => {
|
|
|
110
110
|
continue;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
// 第4
|
|
114
|
-
if (type === '
|
|
113
|
+
// 第4个值与类型联动校验
|
|
114
|
+
if (type === 'text') {
|
|
115
|
+
// text 类型:不允许设置最小/最大长度与默认值(均需为 null)
|
|
116
|
+
if (minStr !== 'null') {
|
|
117
|
+
Logger.error(`${fileType}表 ${fileName} 文件 ${fieldName} 的 text 类型最小值必须为 null,当前为 "${minStr}"`);
|
|
118
|
+
fileValid = false;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (maxStr !== 'null') {
|
|
122
|
+
Logger.error(`${fileType}表 ${fileName} 文件 ${fieldName} 的 text 类型最大长度必须为 null,当前为 "${maxStr}"`);
|
|
123
|
+
fileValid = false;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
} else if (type === 'string') {
|
|
127
|
+
// string:最大长度必为具体数字,且 1..65535
|
|
128
|
+
if (maxStr === 'null' || !validateMinMax(maxStr)) {
|
|
129
|
+
Logger.error(`${fileType}表 ${fileName} 文件 ${fieldName} 最大长度 "${maxStr}" 格式错误,string 类型必须为具体数字`);
|
|
130
|
+
fileValid = false;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const maxVal = parseInt(maxStr, 10);
|
|
134
|
+
if (!(maxVal > 0 && maxVal <= 65535)) {
|
|
135
|
+
Logger.error(`${fileType}表 ${fileName} 文件 ${fieldName} 最大长度 ${maxStr} 越界,string 类型长度必须在 1..65535 范围内`);
|
|
136
|
+
fileValid = false;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
} else if (type === 'array') {
|
|
140
|
+
// array:最大长度必为数字(用于JSON字符串长度限制)
|
|
115
141
|
if (maxStr === 'null' || !validateMinMax(maxStr)) {
|
|
116
|
-
Logger.error(`${fileType}表 ${fileName} 文件 ${fieldName} 最大长度 "${maxStr}" 格式错误,
|
|
142
|
+
Logger.error(`${fileType}表 ${fileName} 文件 ${fieldName} 最大长度 "${maxStr}" 格式错误,array 类型必须为具体数字`);
|
|
117
143
|
fileValid = false;
|
|
118
144
|
continue;
|
|
119
145
|
}
|
|
120
146
|
} else {
|
|
147
|
+
// number 等其他:允许 null 或数字
|
|
121
148
|
if (!validateMinMax(maxStr)) {
|
|
122
149
|
Logger.error(`${fileType}表 ${fileName} 文件 ${fieldName} 最大值 "${maxStr}" 格式错误,必须为null或数字`);
|
|
123
150
|
fileValid = false;
|
|
@@ -125,11 +152,20 @@ export const checkTable = async () => {
|
|
|
125
152
|
}
|
|
126
153
|
}
|
|
127
154
|
|
|
128
|
-
// 第5
|
|
129
|
-
if (
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
155
|
+
// 第5个值:默认值校验
|
|
156
|
+
if (type === 'text') {
|
|
157
|
+
// text 不允许默认值
|
|
158
|
+
if (defaultValue !== 'null') {
|
|
159
|
+
Logger.error(`${fileType}表 ${fileName} 文件 ${fieldName} 为 text 类型,默认值必须为 null,当前为 "${defaultValue}"`);
|
|
160
|
+
fileValid = false;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
if (!validateDefaultValue(defaultValue)) {
|
|
165
|
+
Logger.error(`${fileType}表 ${fileName} 文件 ${fieldName} 默认值 "${defaultValue}" 格式错误,必须为null、字符串或数字`);
|
|
166
|
+
fileValid = false;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
133
169
|
}
|
|
134
170
|
|
|
135
171
|
// 第6个值:是否创建索引必须为0或1
|
package/config/env.js
CHANGED
|
@@ -53,5 +53,10 @@ export const Env = {
|
|
|
53
53
|
MAIL_USER: process.env.MAIL_USER,
|
|
54
54
|
MAIL_PASS: process.env.MAIL_PASS,
|
|
55
55
|
MAIL_SENDER: process.env.MAIL_SENDER,
|
|
56
|
-
MAIL_ADDRESS: process.env.MAIL_ADDRESS
|
|
56
|
+
MAIL_ADDRESS: process.env.MAIL_ADDRESS,
|
|
57
|
+
// 同步脚本开关(用于 core/scripts/syncDb.js)
|
|
58
|
+
SYNC_MERGE_ALTER: process.env.SYNC_MERGE_ALTER,
|
|
59
|
+
SYNC_ONLINE_INDEX: process.env.SYNC_ONLINE_INDEX,
|
|
60
|
+
SYNC_DISALLOW_SHRINK: process.env.SYNC_DISALLOW_SHRINK,
|
|
61
|
+
SYNC_ALLOW_TYPE_CHANGE: process.env.SYNC_ALLOW_TYPE_CHANGE
|
|
57
62
|
};
|
package/package.json
CHANGED
package/scripts/syncDb.js
CHANGED
|
@@ -16,6 +16,52 @@ const typeMapping = {
|
|
|
16
16
|
array: 'VARCHAR'
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
+
// 环境开关读取(支持未在 Env 显式声明的变量,默认值兜底)
|
|
20
|
+
const getFlag = (val, def = 0) => {
|
|
21
|
+
if (val === undefined || val === null || val === '') return !!def;
|
|
22
|
+
const n = Number(val);
|
|
23
|
+
if (!Number.isNaN(n)) return n !== 0;
|
|
24
|
+
const s = String(val).toLowerCase();
|
|
25
|
+
return s === 'true' || s === 'on' || s === 'yes';
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// 命令行参数
|
|
29
|
+
const ARGV = Array.isArray(process.argv) ? process.argv : [];
|
|
30
|
+
const CLI = {
|
|
31
|
+
DRY_RUN: ARGV.includes('--dry-run')
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const FLAGS = {
|
|
35
|
+
// DRY-RUN 改为命令行参数控制,忽略环境变量
|
|
36
|
+
DRY_RUN: CLI.DRY_RUN, // 仅打印计划,不执行
|
|
37
|
+
MERGE_ALTER: getFlag(Env.SYNC_MERGE_ALTER, 1), // 合并每表多项 DDL
|
|
38
|
+
ONLINE_INDEX: getFlag(Env.SYNC_ONLINE_INDEX, 1), // 索引操作使用在线算法
|
|
39
|
+
DISALLOW_SHRINK: getFlag(Env.SYNC_DISALLOW_SHRINK, 1), // 禁止长度收缩
|
|
40
|
+
ALLOW_TYPE_CHANGE: getFlag(Env.SYNC_ALLOW_TYPE_CHANGE, 0) // 允许类型变更
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// 计算期望的默认值(与 information_schema 返回的值对齐)
|
|
44
|
+
// 规则:当默认值为 'null' 时按类型提供默认值:number→0,string→"",array→"[]";text 永不设置默认值
|
|
45
|
+
const getExpectedDefault = (fieldType, fieldDefaultValue) => {
|
|
46
|
+
if (fieldType === 'text') return null; // TEXT 不设置默认
|
|
47
|
+
if (fieldDefaultValue !== undefined && fieldDefaultValue !== null && fieldDefaultValue !== 'null') {
|
|
48
|
+
return fieldDefaultValue; // 保留显式默认值(数字或字符串,包含空字符串)
|
|
49
|
+
}
|
|
50
|
+
// 规则为 'null' 时的内置默认
|
|
51
|
+
switch (fieldType) {
|
|
52
|
+
case 'number':
|
|
53
|
+
return 0;
|
|
54
|
+
case 'string':
|
|
55
|
+
return '';
|
|
56
|
+
case 'array':
|
|
57
|
+
return '[]';
|
|
58
|
+
default:
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const normalizeDefault = (val) => (val === null || val === undefined ? null : String(val));
|
|
64
|
+
|
|
19
65
|
// 获取字段的SQL定义
|
|
20
66
|
const getColumnDefinition = (fieldName, rule) => {
|
|
21
67
|
const [fieldDisplayName, fieldType, fieldMin, fieldMaxLength, fieldDefaultValue, fieldHasIndex] = parseFieldRule(rule);
|
|
@@ -29,21 +75,19 @@ const getColumnDefinition = (fieldName, rule) => {
|
|
|
29
75
|
sqlType = `VARCHAR(${maxLength})`;
|
|
30
76
|
}
|
|
31
77
|
|
|
78
|
+
// 统一强制 NOT NULL
|
|
32
79
|
let columnDef = `\`${fieldName}\` ${sqlType} NOT NULL`;
|
|
33
80
|
|
|
34
|
-
//
|
|
35
|
-
|
|
81
|
+
// 设置默认值:类型非 text 时总是设置(显式默认或内置默认)
|
|
82
|
+
const expectedDefault = getExpectedDefault(fieldType, fieldDefaultValue);
|
|
83
|
+
if (fieldType !== 'text' && expectedDefault !== null) {
|
|
36
84
|
if (fieldType === 'number') {
|
|
37
|
-
columnDef += ` DEFAULT ${
|
|
85
|
+
columnDef += ` DEFAULT ${expectedDefault}`;
|
|
38
86
|
} else {
|
|
39
|
-
columnDef += ` DEFAULT "${
|
|
87
|
+
columnDef += ` DEFAULT \"${String(expectedDefault).replace(/\"/g, '\\"')}\"`;
|
|
40
88
|
}
|
|
41
|
-
} else if (fieldType === 'string' || fieldType === 'array') {
|
|
42
|
-
columnDef += ` DEFAULT ""`;
|
|
43
|
-
} else if (fieldType === 'number') {
|
|
44
|
-
columnDef += ` DEFAULT 0`;
|
|
45
89
|
}
|
|
46
|
-
// text类型不设置默认值
|
|
90
|
+
// text 类型不设置默认值
|
|
47
91
|
|
|
48
92
|
// 添加字段注释(使用第1个属性作为字段显示名称)
|
|
49
93
|
if (fieldDisplayName && fieldDisplayName !== 'null') {
|
|
@@ -101,17 +145,19 @@ const getTableIndexes = async (client, tableName) => {
|
|
|
101
145
|
return indexes;
|
|
102
146
|
};
|
|
103
147
|
|
|
104
|
-
//
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
} catch (error) {
|
|
112
|
-
Logger.error(`${action === 'create' ? '创建' : '删除'}索引失败: ${error.message}`);
|
|
113
|
-
throw error;
|
|
148
|
+
// 构建索引操作 SQL(统一使用 ALTER TABLE 并尽量在线)
|
|
149
|
+
const buildIndexSQL = (tableName, indexName, fieldName, action) => {
|
|
150
|
+
const parts = [];
|
|
151
|
+
if (action === 'create') {
|
|
152
|
+
parts.push(`ADD INDEX \`${indexName}\` (\`${fieldName}\`)`);
|
|
153
|
+
} else {
|
|
154
|
+
parts.push(`DROP INDEX \`${indexName}\``);
|
|
114
155
|
}
|
|
156
|
+
if (FLAGS.ONLINE_INDEX) {
|
|
157
|
+
parts.push('ALGORITHM=INPLACE');
|
|
158
|
+
parts.push('LOCK=NONE');
|
|
159
|
+
}
|
|
160
|
+
return `ALTER TABLE \`${tableName}\` ${parts.join(', ')}`;
|
|
115
161
|
};
|
|
116
162
|
|
|
117
163
|
// 创建表
|
|
@@ -148,11 +194,15 @@ const createTable = async (client, tableName, fields) => {
|
|
|
148
194
|
CREATE TABLE \`${tableName}\` (
|
|
149
195
|
${columns.join(',\n ')},
|
|
150
196
|
${indexes.join(',\n ')}
|
|
151
|
-
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=
|
|
197
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_as_cs
|
|
152
198
|
`;
|
|
153
199
|
|
|
154
|
-
|
|
155
|
-
|
|
200
|
+
if (FLAGS.DRY_RUN) {
|
|
201
|
+
Logger.info(`[计划] ${createTableSQL.replace(/\n+/g, ' ')}`);
|
|
202
|
+
} else {
|
|
203
|
+
await exec(client, createTableSQL);
|
|
204
|
+
Logger.info(`[新建表] ${tableName}`);
|
|
205
|
+
}
|
|
156
206
|
};
|
|
157
207
|
|
|
158
208
|
// 比较字段定义变化
|
|
@@ -192,14 +242,33 @@ const compareFieldDefinition = (existingColumn, newRule, fieldName) => {
|
|
|
192
242
|
changes.push({ type: 'datatype', current: existingColumn.type, new: expectedDbType });
|
|
193
243
|
}
|
|
194
244
|
|
|
245
|
+
// 检查默认值变化(按照生成规则推导期望默认值)
|
|
246
|
+
const expectedDefault = getExpectedDefault(fieldType, fieldDefaultValue);
|
|
247
|
+
const currDef = normalizeDefault(existingColumn.defaultValue);
|
|
248
|
+
const newDef = normalizeDefault(expectedDefault);
|
|
249
|
+
if (currDef !== newDef) {
|
|
250
|
+
changes.push({ type: 'default', current: existingColumn.defaultValue, new: expectedDefault });
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// 检查可空性变化(统一期望 NOT NULL)
|
|
254
|
+
const expectedNullable = false; // 期望 NOT NULL
|
|
255
|
+
if (existingColumn.nullable !== expectedNullable) {
|
|
256
|
+
// existingColumn.nullable 为 true 表示可空
|
|
257
|
+
changes.push({
|
|
258
|
+
type: 'nullability',
|
|
259
|
+
current: existingColumn.nullable ? 'NULL' : 'NOT NULL',
|
|
260
|
+
new: expectedNullable ? 'NULL' : 'NOT NULL'
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
195
264
|
return { hasChanges: changes.length > 0, changes };
|
|
196
265
|
};
|
|
197
266
|
|
|
198
|
-
//
|
|
199
|
-
const
|
|
267
|
+
// 生成字段 DDL 子句(不含 ALTER TABLE 前缀)
|
|
268
|
+
const generateDDLClause = (fieldName, rule, isAdd = false) => {
|
|
200
269
|
const columnDef = getColumnDefinition(fieldName, rule);
|
|
201
270
|
const operation = isAdd ? 'ADD COLUMN' : 'MODIFY COLUMN';
|
|
202
|
-
return
|
|
271
|
+
return `${operation} ${columnDef}`;
|
|
203
272
|
};
|
|
204
273
|
|
|
205
274
|
// 安全执行DDL语句
|
|
@@ -210,13 +279,16 @@ const executeDDLSafely = async (client, sql) => {
|
|
|
210
279
|
} catch (error) {
|
|
211
280
|
// INSTANT失败时尝试INPLACE
|
|
212
281
|
if (sql.includes('ALGORITHM=INSTANT')) {
|
|
213
|
-
const inplaceSql = sql.replace(
|
|
282
|
+
const inplaceSql = sql.replace(/ALGORITHM=INSTANT/g, 'ALGORITHM=INPLACE');
|
|
214
283
|
try {
|
|
215
284
|
await exec(client, inplaceSql);
|
|
216
285
|
return true;
|
|
217
286
|
} catch (inplaceError) {
|
|
218
|
-
// 最后尝试传统DDL
|
|
219
|
-
const traditionSql = sql
|
|
287
|
+
// 最后尝试传统DDL:移除 ALGORITHM/LOCK 附加子句后执行
|
|
288
|
+
const traditionSql = sql
|
|
289
|
+
.replace(/,\s*ALGORITHM=INPLACE/g, '')
|
|
290
|
+
.replace(/,\s*ALGORITHM=INSTANT/g, '')
|
|
291
|
+
.replace(/,\s*LOCK=(NONE|SHARED|EXCLUSIVE)/g, '');
|
|
220
292
|
await exec(client, traditionSql);
|
|
221
293
|
return true;
|
|
222
294
|
}
|
|
@@ -231,20 +303,97 @@ const syncTable = async (client, tableName, fields) => {
|
|
|
231
303
|
const existingColumns = await getTableColumns(client, tableName);
|
|
232
304
|
const existingIndexes = await getTableIndexes(client, tableName);
|
|
233
305
|
const systemFields = ['id', 'created_at', 'updated_at', 'deleted_at', 'state'];
|
|
306
|
+
let changed = false;
|
|
307
|
+
|
|
308
|
+
const addClauses = [];
|
|
309
|
+
const modifyClauses = [];
|
|
310
|
+
const defaultClauses = [];
|
|
311
|
+
const indexActions = [];
|
|
312
|
+
// 变更统计(按字段粒度)
|
|
313
|
+
const changeStats = {
|
|
314
|
+
addFields: 0,
|
|
315
|
+
datatype: 0,
|
|
316
|
+
length: 0,
|
|
317
|
+
default: 0,
|
|
318
|
+
comment: 0,
|
|
319
|
+
nullability: 0,
|
|
320
|
+
indexCreate: 0,
|
|
321
|
+
indexDrop: 0
|
|
322
|
+
};
|
|
234
323
|
|
|
235
324
|
// 同步字段
|
|
236
325
|
for (const [fieldName, rule] of Object.entries(fields)) {
|
|
237
326
|
if (existingColumns[fieldName]) {
|
|
238
327
|
const comparison = compareFieldDefinition(existingColumns[fieldName], rule, fieldName);
|
|
239
328
|
if (comparison.hasChanges) {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
329
|
+
// 打印具体变动项并统计
|
|
330
|
+
for (const c of comparison.changes) {
|
|
331
|
+
const label = { length: '长度', datatype: '类型', comment: '注释', default: '默认值' }[c.type] || c.type;
|
|
332
|
+
Logger.info(`[字段变更] ${tableName}.${fieldName} ${label}: ${c.current ?? 'NULL'} -> ${c.new ?? 'NULL'}`);
|
|
333
|
+
if (c.type in changeStats) changeStats[c.type]++;
|
|
334
|
+
}
|
|
335
|
+
// 风险护栏:长度收缩/类型变更
|
|
336
|
+
const ruleParts = parseFieldRule(rule);
|
|
337
|
+
const [, fType, , fMax, fDef] = ruleParts;
|
|
338
|
+
if ((fType === 'string' || fType === 'array') && existingColumns[fieldName].length && fMax !== 'null') {
|
|
339
|
+
const newLen = parseInt(fMax);
|
|
340
|
+
if (existingColumns[fieldName].length > newLen && FLAGS.DISALLOW_SHRINK) {
|
|
341
|
+
Logger.warn(`[跳过危险变更] ${tableName}.${fieldName} 长度收缩 ${existingColumns[fieldName].length} -> ${newLen} 已被跳过(设置 SYNC_DISALLOW_SHRINK=0 可放开)`);
|
|
342
|
+
// 如果仅有 shrink 一个变化,仍可能还有默认/注释变化要处理
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const expectedDbType = {
|
|
346
|
+
number: 'bigint',
|
|
347
|
+
string: 'varchar',
|
|
348
|
+
text: 'mediumtext',
|
|
349
|
+
array: 'varchar'
|
|
350
|
+
}[parseFieldRule(rule)[1]];
|
|
351
|
+
if (existingColumns[fieldName].type.toLowerCase() !== expectedDbType && !FLAGS.ALLOW_TYPE_CHANGE) {
|
|
352
|
+
Logger.warn(`[跳过危险变更] ${tableName}.${fieldName} 类型变更 ${existingColumns[fieldName].type} -> ${expectedDbType} 已被跳过(设置 SYNC_ALLOW_TYPE_CHANGE=1 可放开)`);
|
|
353
|
+
// 继续处理默认值/注释等非类型变更
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// 判断是否“仅默认值变化”
|
|
357
|
+
const onlyDefaultChanged = comparison.changes.every((c) => c.type === 'default');
|
|
358
|
+
if (onlyDefaultChanged) {
|
|
359
|
+
const expectedDefault = getExpectedDefault(parseFieldRule(rule)[1], parseFieldRule(rule)[4]);
|
|
360
|
+
if (expectedDefault === null) {
|
|
361
|
+
defaultClauses.push(`ALTER COLUMN \`${fieldName}\` DROP DEFAULT`);
|
|
362
|
+
} else {
|
|
363
|
+
const isNumber = parseFieldRule(rule)[1] === 'number';
|
|
364
|
+
const v = isNumber ? expectedDefault : `"${String(expectedDefault).replace(/"/g, '\\"')}"`;
|
|
365
|
+
defaultClauses.push(`ALTER COLUMN \`${fieldName}\` SET DEFAULT ${v}`);
|
|
366
|
+
}
|
|
367
|
+
} else {
|
|
368
|
+
// 判断是否需要跳过 MODIFY:包含收缩或类型变更时跳过
|
|
369
|
+
let skipModify = false;
|
|
370
|
+
const hasLengthChange = comparison.changes.some((c) => c.type === 'length');
|
|
371
|
+
if (hasLengthChange && (fType === 'string' || fType === 'array') && existingColumns[fieldName].length && fMax !== 'null') {
|
|
372
|
+
const newLen = parseInt(fMax);
|
|
373
|
+
if (existingColumns[fieldName].length > newLen && FLAGS.DISALLOW_SHRINK) {
|
|
374
|
+
skipModify = true;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
const hasTypeChange = comparison.changes.some((c) => c.type === 'datatype');
|
|
378
|
+
if (hasTypeChange && !FLAGS.ALLOW_TYPE_CHANGE) {
|
|
379
|
+
skipModify = true;
|
|
380
|
+
}
|
|
381
|
+
if (!skipModify) {
|
|
382
|
+
// 合并到 MODIFY COLUMN 子句
|
|
383
|
+
modifyClauses.push(generateDDLClause(fieldName, rule, false));
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
changed = true;
|
|
243
387
|
}
|
|
244
388
|
} else {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
389
|
+
// 新增字段日志
|
|
390
|
+
const [disp, fType, fMin, fMax, fDef, fIdx] = parseFieldRule(rule);
|
|
391
|
+
const lenPart = fType === 'string' || fType === 'array' ? ` 长度:${parseInt(fMax)}` : '';
|
|
392
|
+
const expectedDefault = getExpectedDefault(fType, fDef);
|
|
393
|
+
Logger.info(`[新增字段] ${tableName}.${fieldName} 类型:${fType}${lenPart} 默认:${expectedDefault ?? 'NULL'}`);
|
|
394
|
+
addClauses.push(generateDDLClause(fieldName, rule, true));
|
|
395
|
+
changed = true;
|
|
396
|
+
changeStats.addFields++;
|
|
248
397
|
}
|
|
249
398
|
}
|
|
250
399
|
|
|
@@ -255,11 +404,16 @@ const syncTable = async (client, tableName, fields) => {
|
|
|
255
404
|
const indexName = `idx_${fieldName}`;
|
|
256
405
|
|
|
257
406
|
if (fieldHasIndex === '1' && !existingIndexes[indexName]) {
|
|
258
|
-
|
|
407
|
+
indexActions.push({ action: 'create', indexName, fieldName });
|
|
408
|
+
changed = true;
|
|
409
|
+
changeStats.indexCreate++;
|
|
259
410
|
} else if (fieldHasIndex !== '1' && existingIndexes[indexName] && existingIndexes[indexName].length === 1) {
|
|
260
|
-
|
|
411
|
+
indexActions.push({ action: 'drop', indexName, fieldName });
|
|
412
|
+
changed = true;
|
|
413
|
+
changeStats.indexDrop++;
|
|
261
414
|
}
|
|
262
415
|
}
|
|
416
|
+
return { changed, addClauses, modifyClauses, defaultClauses, indexActions, metrics: changeStats };
|
|
263
417
|
};
|
|
264
418
|
|
|
265
419
|
// 主同步函数
|
|
@@ -297,6 +451,17 @@ const SyncDb = async () => {
|
|
|
297
451
|
let processedCount = 0;
|
|
298
452
|
let createdTables = 0;
|
|
299
453
|
let modifiedTables = 0;
|
|
454
|
+
// 全局统计
|
|
455
|
+
const overall = {
|
|
456
|
+
addFields: 0,
|
|
457
|
+
typeChanges: 0,
|
|
458
|
+
maxChanges: 0, // 映射为长度变化
|
|
459
|
+
minChanges: 0, // 最小值不参与 DDL,比对保留为0
|
|
460
|
+
defaultChanges: 0,
|
|
461
|
+
nameChanges: 0, // 字段显示名(注释)变更
|
|
462
|
+
indexCreate: 0,
|
|
463
|
+
indexDrop: 0
|
|
464
|
+
};
|
|
300
465
|
|
|
301
466
|
for (const dir of directories) {
|
|
302
467
|
try {
|
|
@@ -307,13 +472,80 @@ const SyncDb = async () => {
|
|
|
307
472
|
const exists = result[0].count > 0;
|
|
308
473
|
|
|
309
474
|
if (exists) {
|
|
310
|
-
await syncTable(client, tableName, tableDefinition);
|
|
475
|
+
const plan = await syncTable(client, tableName, tableDefinition);
|
|
476
|
+
if (plan.changed) {
|
|
477
|
+
// 汇总统计
|
|
478
|
+
if (plan.metrics) {
|
|
479
|
+
overall.addFields += plan.metrics.addFields;
|
|
480
|
+
overall.typeChanges += plan.metrics.datatype;
|
|
481
|
+
overall.maxChanges += plan.metrics.length;
|
|
482
|
+
overall.defaultChanges += plan.metrics.default;
|
|
483
|
+
overall.indexCreate += plan.metrics.indexCreate;
|
|
484
|
+
overall.indexDrop += plan.metrics.indexDrop;
|
|
485
|
+
overall.nameChanges += plan.metrics.comment;
|
|
486
|
+
}
|
|
487
|
+
// 合并执行 ALTER TABLE 子句
|
|
488
|
+
if (FLAGS.MERGE_ALTER) {
|
|
489
|
+
const clauses = [...plan.addClauses, ...plan.modifyClauses];
|
|
490
|
+
if (clauses.length > 0) {
|
|
491
|
+
const sql = `ALTER TABLE \`${tableName}\` ${clauses.join(', ')}, ALGORITHM=INSTANT, LOCK=NONE`;
|
|
492
|
+
if (FLAGS.DRY_RUN) {
|
|
493
|
+
Logger.info(`[计划] ${sql}`);
|
|
494
|
+
} else {
|
|
495
|
+
await executeDDLSafely(client, sql);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
} else {
|
|
499
|
+
// 分别执行
|
|
500
|
+
for (const c of plan.addClauses) {
|
|
501
|
+
const sql = `ALTER TABLE \`${tableName}\` ${c}, ALGORITHM=INSTANT, LOCK=NONE`;
|
|
502
|
+
if (FLAGS.DRY_RUN) Logger.info(`[计划] ${sql}`);
|
|
503
|
+
else await executeDDLSafely(client, sql);
|
|
504
|
+
}
|
|
505
|
+
for (const c of plan.modifyClauses) {
|
|
506
|
+
const sql = `ALTER TABLE \`${tableName}\` ${c}, ALGORITHM=INSTANT, LOCK=NONE`;
|
|
507
|
+
if (FLAGS.DRY_RUN) Logger.info(`[计划] ${sql}`);
|
|
508
|
+
else await executeDDLSafely(client, sql);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// 默认值专用 ALTER
|
|
513
|
+
if (plan.defaultClauses.length > 0) {
|
|
514
|
+
const sql = `ALTER TABLE \`${tableName}\` ${plan.defaultClauses.join(', ')}, ALGORITHM=INSTANT, LOCK=NONE`;
|
|
515
|
+
if (FLAGS.DRY_RUN) Logger.info(`[计划] ${sql}`);
|
|
516
|
+
else await executeDDLSafely(client, sql);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// 索引操作
|
|
520
|
+
for (const act of plan.indexActions) {
|
|
521
|
+
const sql = buildIndexSQL(tableName, act.indexName, act.fieldName, act.action);
|
|
522
|
+
if (FLAGS.DRY_RUN) {
|
|
523
|
+
Logger.info(`[计划] ${sql}`);
|
|
524
|
+
} else {
|
|
525
|
+
try {
|
|
526
|
+
await exec(client, sql);
|
|
527
|
+
if (act.action === 'create') {
|
|
528
|
+
Logger.info(`[索引变化] 新建索引 ${tableName}.${act.indexName} (${act.fieldName})`);
|
|
529
|
+
} else {
|
|
530
|
+
Logger.info(`[索引变化] 删除索引 ${tableName}.${act.indexName} (${act.fieldName})`);
|
|
531
|
+
}
|
|
532
|
+
} catch (error) {
|
|
533
|
+
Logger.error(`${act.action === 'create' ? '创建' : '删除'}索引失败: ${error.message}`);
|
|
534
|
+
throw error;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
modifiedTables++;
|
|
540
|
+
}
|
|
311
541
|
} else {
|
|
312
542
|
await createTable(client, tableName, tableDefinition);
|
|
543
|
+
createdTables++;
|
|
544
|
+
// 新建表已算作变更
|
|
545
|
+
modifiedTables += 0;
|
|
546
|
+
// 创建表统计:按需求仅汇总创建表数量
|
|
313
547
|
}
|
|
314
548
|
|
|
315
|
-
Logger.info(`表 ${tableName} 处理完成`);
|
|
316
|
-
exists ? modifiedTables++ : createdTables++;
|
|
317
549
|
processedCount++;
|
|
318
550
|
}
|
|
319
551
|
} catch (error) {
|
|
@@ -321,8 +553,17 @@ const SyncDb = async () => {
|
|
|
321
553
|
}
|
|
322
554
|
}
|
|
323
555
|
|
|
324
|
-
//
|
|
325
|
-
Logger.info(
|
|
556
|
+
// 显示统计信息(扩展维度)
|
|
557
|
+
Logger.info(`统计 - 创建表: ${createdTables}`);
|
|
558
|
+
Logger.info(`统计 - 字段新增: ${overall.addFields}`);
|
|
559
|
+
Logger.info(`统计 - 字段名称变更: ${overall.nameChanges}`);
|
|
560
|
+
Logger.info(`统计 - 字段类型变更: ${overall.typeChanges}`);
|
|
561
|
+
Logger.info(`统计 - 字段最小值变更: ${overall.minChanges}`);
|
|
562
|
+
Logger.info(`统计 - 字段最大值变更: ${overall.maxChanges}`);
|
|
563
|
+
Logger.info(`统计 - 字段默认值变更: ${overall.defaultChanges}`);
|
|
564
|
+
// 索引新增/删除分别打印
|
|
565
|
+
Logger.info(`统计 - 索引新增: ${overall.indexCreate}`);
|
|
566
|
+
Logger.info(`统计 - 索引删除: ${overall.indexDrop}`);
|
|
326
567
|
|
|
327
568
|
if (processedCount === 0) {
|
|
328
569
|
Logger.warn('没有找到任何表定义文件');
|
package/tables/tool.json
CHANGED