befly 2.2.0 → 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 CHANGED
@@ -110,14 +110,41 @@ export const checkTable = async () => {
110
110
  continue;
111
111
  }
112
112
 
113
- // 第4个值:当类型为 string/array 时,最大长度必须为数字且不可为 null;其他类型允许为 null 或数字
114
- if (type === 'string' || type === 'array') {
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}" 格式错误,string/array 类型必须为具体数字`);
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个值:默认值必须为null、字符串或数字
129
- if (!validateDefaultValue(defaultValue)) {
130
- Logger.error(`${fileType}表 ${fileName} 文件 ${fieldName} 默认值 "${defaultValue}" 格式错误,必须为null、字符串或数字`);
131
- fileValid = false;
132
- continue;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "befly",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "Buma - 为 Bun 专属打造的 API 接口框架核心引擎",
5
5
  "type": "module",
6
6
  "private": false,
@@ -13,9 +13,9 @@
13
13
  ".": "./main.js"
14
14
  },
15
15
  "scripts": {
16
- "ra": "bun run release.js -a",
17
- "rb": "bun run release.js -b",
18
- "rc": "bun run release.js -c"
16
+ "ra": "bun run ../release.js --major",
17
+ "rb": "bun run ../release.js --minor",
18
+ "rc": "bun run ../release.js --patch"
19
19
  },
20
20
  "keywords": [
21
21
  "bun",
@@ -59,4 +59,4 @@
59
59
  "*.{js,css,scss,less,ts,jsx,vue,html,json,md,yaml}": "bunx --bun prettier --write --cache --ignore-unknown"
60
60
  },
61
61
  "gitHead": "1dc5f118a723969456559e758e2ba889f4601224"
62
- }
62
+ }
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
- // 设置默认值:如果第5个属性为null或字段类型为text,则不设置默认值
35
- if (fieldDefaultValue && fieldDefaultValue !== 'null' && fieldType !== 'text') {
81
+ // 设置默认值:类型非 text 时总是设置(显式默认或内置默认)
82
+ const expectedDefault = getExpectedDefault(fieldType, fieldDefaultValue);
83
+ if (fieldType !== 'text' && expectedDefault !== null) {
36
84
  if (fieldType === 'number') {
37
- columnDef += ` DEFAULT ${fieldDefaultValue}`;
85
+ columnDef += ` DEFAULT ${expectedDefault}`;
38
86
  } else {
39
- columnDef += ` DEFAULT "${fieldDefaultValue.replace(/"/g, '\\"')}"`;
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 manageIndex = async (client, tableName, indexName, fieldName, action) => {
106
- const sql = action === 'create' ? `CREATE INDEX \`${indexName}\` ON \`${tableName}\` (\`${fieldName}\`)` : `DROP INDEX \`${indexName}\` ON \`${tableName}\``;
107
-
108
- try {
109
- await exec(client, sql);
110
- Logger.info(`表 ${tableName} 索引 ${indexName} ${action === 'create' ? '创建' : '删除'}成功`);
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=utf8mb4_unicode_ci
197
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_as_cs
152
198
  `;
153
199
 
154
- await exec(client, createTableSQL);
155
- Logger.info(`表 ${tableName} 创建成功`);
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
- // 生成DDL语句
199
- const generateDDL = (tableName, fieldName, rule, isAdd = false) => {
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 `ALTER TABLE \`${tableName}\` ${operation} ${columnDef}, ALGORITHM=INSTANT, LOCK=NONE`;
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('ALGORITHM=INSTANT', 'ALGORITHM=INPLACE');
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.split(',')[0]; // 移除ALGORITHM和LOCK参数
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
- const sql = generateDDL(tableName, fieldName, rule);
241
- await executeDDLSafely(client, sql);
242
- Logger.info(`字段 ${tableName}.${fieldName} 更新成功`);
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
- const sql = generateDDL(tableName, fieldName, rule, true);
246
- await executeDDLSafely(client, sql);
247
- Logger.info(`字段 ${tableName}.${fieldName} 添加成功`);
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
- await manageIndex(client, tableName, indexName, fieldName, 'create');
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
- await manageIndex(client, tableName, indexName, fieldName, 'drop');
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(`同步完成 - 总计: ${processedCount}, 新建: ${createdTables}, 修改: ${modifiedTables}`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "filename": "文件名⚡string⚡1⚡255⚡null⚡1⚡null",
3
- "content": "内容⚡string⚡0⚡1000000⚡null⚡0⚡null",
3
+ "content": "内容⚡string⚡0⚡100⚡null⚡0⚡null",
4
4
  "type": "类型⚡string⚡0⚡50⚡file⚡1⚡^(file|folder|link)$",
5
5
  "path": "路径⚡string⚡1⚡500⚡null⚡0⚡null"
6
6
  }