befly 3.5.7 → 3.7.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.
Files changed (85) hide show
  1. package/{checks/table.ts → check.ts} +14 -17
  2. package/lib/addon.ts +77 -0
  3. package/lib/logger.ts +6 -15
  4. package/lifecycle/checker.ts +22 -48
  5. package/lifecycle/lifecycle.ts +7 -5
  6. package/lifecycle/loader.ts +5 -6
  7. package/main.ts +12 -1
  8. package/package.json +3 -14
  9. package/paths.ts +5 -54
  10. package/plugins/cache.ts +0 -1
  11. package/plugins/db.ts +0 -1
  12. package/plugins/logger.ts +0 -1
  13. package/plugins/redis.ts +0 -6
  14. package/util.ts +1 -83
  15. package/apis/admin/del.ts +0 -35
  16. package/apis/admin/info.ts +0 -50
  17. package/apis/admin/ins.ts +0 -61
  18. package/apis/admin/list.ts +0 -20
  19. package/apis/admin/roleDetail.ts +0 -35
  20. package/apis/admin/roleSave.ts +0 -40
  21. package/apis/admin/upd.ts +0 -51
  22. package/apis/api/all.ts +0 -37
  23. package/apis/auth/login.ts +0 -78
  24. package/apis/auth/logout.ts +0 -23
  25. package/apis/auth/register.ts +0 -50
  26. package/apis/auth/sendSmsCode.ts +0 -36
  27. package/apis/cache/refresh.ts +0 -34
  28. package/apis/dashboard/addonList.ts +0 -47
  29. package/apis/dashboard/changelog.ts +0 -37
  30. package/apis/dashboard/configStatus.ts +0 -54
  31. package/apis/dashboard/environmentInfo.ts +0 -46
  32. package/apis/dashboard/performanceMetrics.ts +0 -23
  33. package/apis/dashboard/permissionStats.ts +0 -31
  34. package/apis/dashboard/serviceStatus.ts +0 -82
  35. package/apis/dashboard/systemInfo.ts +0 -26
  36. package/apis/dashboard/systemOverview.ts +0 -32
  37. package/apis/dashboard/systemResources.ts +0 -119
  38. package/apis/dict/all.ts +0 -25
  39. package/apis/dict/del.ts +0 -19
  40. package/apis/dict/detail.ts +0 -21
  41. package/apis/dict/ins.ts +0 -27
  42. package/apis/dict/list.ts +0 -18
  43. package/apis/dict/upd.ts +0 -31
  44. package/apis/menu/all.ts +0 -68
  45. package/apis/menu/del.ts +0 -37
  46. package/apis/menu/ins.ts +0 -20
  47. package/apis/menu/list.ts +0 -21
  48. package/apis/menu/upd.ts +0 -29
  49. package/apis/role/apiDetail.ts +0 -30
  50. package/apis/role/apiSave.ts +0 -41
  51. package/apis/role/del.ts +0 -44
  52. package/apis/role/detail.ts +0 -24
  53. package/apis/role/ins.ts +0 -39
  54. package/apis/role/list.ts +0 -14
  55. package/apis/role/menuDetail.ts +0 -30
  56. package/apis/role/menuSave.ts +0 -38
  57. package/apis/role/save.ts +0 -44
  58. package/apis/role/upd.ts +0 -40
  59. package/bin/index.ts +0 -34
  60. package/checks/conflict.ts +0 -351
  61. package/commands/index.ts +0 -73
  62. package/commands/sync.ts +0 -88
  63. package/commands/syncApi.ts +0 -316
  64. package/commands/syncDb/apply.ts +0 -171
  65. package/commands/syncDb/constants.ts +0 -77
  66. package/commands/syncDb/ddl.ts +0 -191
  67. package/commands/syncDb/helpers.ts +0 -173
  68. package/commands/syncDb/index.ts +0 -217
  69. package/commands/syncDb/schema.ts +0 -199
  70. package/commands/syncDb/sqlite.ts +0 -50
  71. package/commands/syncDb/state.ts +0 -112
  72. package/commands/syncDb/table.ts +0 -214
  73. package/commands/syncDb/tableCreate.ts +0 -149
  74. package/commands/syncDb/types.ts +0 -92
  75. package/commands/syncDb/version.ts +0 -73
  76. package/commands/syncDb.ts +0 -34
  77. package/commands/syncDev.ts +0 -237
  78. package/commands/syncMenu.ts +0 -349
  79. package/commands/util.ts +0 -58
  80. package/entry.ts +0 -9
  81. package/tables/admin.json +0 -14
  82. package/tables/api.json +0 -8
  83. package/tables/dict.json +0 -8
  84. package/tables/menu.json +0 -8
  85. package/tables/role.json +0 -8
@@ -1,199 +0,0 @@
1
- /**
2
- * syncDb 表结构查询模块
3
- *
4
- * 包含:
5
- * - 判断表是否存在
6
- * - 获取表的列信息
7
- * - 获取表的索引信息
8
- */
9
-
10
- import { Env } from '../../env.js';
11
- import { IS_MYSQL, IS_PG, IS_SQLITE } from './constants.js';
12
- import type { ColumnInfo, IndexInfo } from './types.js';
13
- import type { SQL } from 'bun';
14
-
15
- // 重新导出类型供其他模块使用
16
- export type { ColumnInfo, IndexInfo };
17
-
18
- /**
19
- * 判断表是否存在(返回布尔值)
20
- *
21
- * @param sql - SQL 客户端实例
22
- * @param tableName - 表名
23
- * @returns 表是否存在
24
- */
25
- export async function tableExists(sql: SQL, tableName: string): Promise<boolean> {
26
- if (!sql) throw new Error('SQL 客户端未初始化');
27
-
28
- try {
29
- if (IS_MYSQL) {
30
- const res = await sql`SELECT COUNT(*) AS count FROM information_schema.TABLES WHERE TABLE_SCHEMA = ${Env.DB_NAME} AND TABLE_NAME = ${tableName}`;
31
- return (res[0]?.count || 0) > 0;
32
- }
33
-
34
- if (IS_PG) {
35
- const res = await sql`SELECT COUNT(*)::int AS count FROM information_schema.tables WHERE table_schema = 'public' AND table_name = ${tableName}`;
36
- return (res[0]?.count || 0) > 0;
37
- }
38
-
39
- if (IS_SQLITE) {
40
- const res = await sql`SELECT name FROM sqlite_master WHERE type='table' AND name = ${tableName}`;
41
- return res.length > 0;
42
- }
43
-
44
- return false;
45
- } catch (error: any) {
46
- throw new Error(`查询表是否存在失败 [${tableName}]: ${error.message}`);
47
- }
48
- }
49
-
50
- /**
51
- * 获取表的现有列信息(按方言)
52
- *
53
- * 查询数据库元数据,获取表的所有列信息,包括:
54
- * - 列名
55
- * - 数据类型
56
- * - 字符最大长度
57
- * - 是否可为空
58
- * - 默认值
59
- * - 列注释(MySQL/PG)
60
- *
61
- * @param sql - SQL 客户端实例
62
- * @param tableName - 表名
63
- * @returns 列信息对象,键为列名,值为列详情
64
- */
65
- export async function getTableColumns(sql: SQL, tableName: string): Promise<{ [key: string]: ColumnInfo }> {
66
- const columns: { [key: string]: ColumnInfo } = {};
67
-
68
- try {
69
- if (IS_MYSQL) {
70
- const result = await sql`
71
- SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, IS_NULLABLE, COLUMN_DEFAULT, COLUMN_COMMENT, COLUMN_TYPE
72
- FROM information_schema.COLUMNS
73
- WHERE TABLE_SCHEMA = ${Env.DB_NAME} AND TABLE_NAME = ${tableName}
74
- ORDER BY ORDINAL_POSITION
75
- `;
76
- for (const row of result) {
77
- // MySQL 的 COLUMN_DEFAULT 已经是解析后的实际值,无需处理:
78
- // - 空字符串 DEFAULT '': 返回 '' (空字符串)
79
- // - 字符串 DEFAULT 'admin': 返回 admin (无引号)
80
- // - 单引号 DEFAULT '''': 返回 ' (单引号字符)
81
- // - 数字 DEFAULT 0: 返回 0
82
- // - NULL: 返回 null
83
- const defaultValue = row.COLUMN_DEFAULT;
84
-
85
- columns[row.COLUMN_NAME] = {
86
- type: row.DATA_TYPE,
87
- columnType: row.COLUMN_TYPE,
88
- length: row.CHARACTER_MAXIMUM_LENGTH,
89
- nullable: row.IS_NULLABLE === 'YES',
90
- defaultValue: defaultValue,
91
- comment: row.COLUMN_COMMENT
92
- };
93
- }
94
- } else if (IS_PG) {
95
- const result = await sql`
96
- SELECT column_name, data_type, character_maximum_length, is_nullable, column_default
97
- FROM information_schema.columns
98
- WHERE table_schema = 'public' AND table_name = ${tableName}
99
- ORDER BY ordinal_position
100
- `;
101
- // 获取列注释
102
- const comments = await sql`
103
- SELECT a.attname AS column_name, col_description(c.oid, a.attnum) AS column_comment
104
- FROM pg_class c
105
- JOIN pg_attribute a ON a.attrelid = c.oid
106
- JOIN pg_namespace n ON n.oid = c.relnamespace
107
- WHERE c.relkind = 'r' AND n.nspname = 'public' AND c.relname = ${tableName} AND a.attnum > 0
108
- `;
109
- const commentMap: { [key: string]: string } = {};
110
- for (const r of comments) commentMap[r.column_name] = r.column_comment;
111
-
112
- for (const row of result) {
113
- columns[row.column_name] = {
114
- type: row.data_type,
115
- columnType: row.data_type,
116
- length: row.character_maximum_length,
117
- nullable: String(row.is_nullable).toUpperCase() === 'YES',
118
- defaultValue: row.column_default,
119
- comment: commentMap[row.column_name] ?? null
120
- };
121
- }
122
- } else if (IS_SQLITE) {
123
- const result = await sql.unsafe(`PRAGMA table_info(${tableName})`);
124
- for (const row of result) {
125
- let baseType = String(row.type || '').toUpperCase();
126
- let length = null;
127
- const m = /^(\w+)\s*\((\d+)\)/.exec(baseType);
128
- if (m) {
129
- baseType = m[1];
130
- length = Number(m[2]);
131
- }
132
- columns[row.name] = {
133
- type: baseType.toLowerCase(),
134
- columnType: baseType.toLowerCase(),
135
- length: length,
136
- nullable: row.notnull === 0,
137
- defaultValue: row.dflt_value,
138
- comment: null
139
- };
140
- }
141
- }
142
-
143
- return columns;
144
- } catch (error: any) {
145
- throw new Error(`获取表列信息失败 [${tableName}]: ${error.message}`);
146
- }
147
- }
148
-
149
- /**
150
- * 获取表的现有索引信息(单列索引)
151
- *
152
- * @param sql - SQL 客户端实例
153
- * @param tableName - 表名
154
- * @returns 索引信息对象,键为索引名,值为列名数组
155
- */
156
- export async function getTableIndexes(sql: SQL, tableName: string): Promise<IndexInfo> {
157
- const indexes: IndexInfo = {};
158
-
159
- try {
160
- if (IS_MYSQL) {
161
- const result = await sql`
162
- SELECT INDEX_NAME, COLUMN_NAME
163
- FROM information_schema.STATISTICS
164
- WHERE TABLE_SCHEMA = ${Env.DB_NAME}
165
- AND TABLE_NAME = ${tableName}
166
- AND INDEX_NAME != 'PRIMARY'
167
- ORDER BY INDEX_NAME
168
- `;
169
- for (const row of result) {
170
- if (!indexes[row.INDEX_NAME]) indexes[row.INDEX_NAME] = [];
171
- indexes[row.INDEX_NAME].push(row.COLUMN_NAME);
172
- }
173
- } else if (IS_PG) {
174
- const result = await sql`
175
- SELECT indexname, indexdef
176
- FROM pg_indexes
177
- WHERE schemaname = 'public' AND tablename = ${tableName}
178
- `;
179
- for (const row of result) {
180
- const m = /\(([^)]+)\)/.exec(row.indexdef);
181
- if (m) {
182
- const col = m[1].replace(/\"/g, '').replace(/"/g, '').trim();
183
- indexes[row.indexname] = [col];
184
- }
185
- }
186
- } else if (IS_SQLITE) {
187
- const list = await sql.unsafe(`PRAGMA index_list(${tableName})`);
188
- for (const idx of list) {
189
- const info = await sql.unsafe(`PRAGMA index_info(${idx.name})`);
190
- const cols = info.map((r) => r.name);
191
- if (cols.length === 1) indexes[idx.name] = cols;
192
- }
193
- }
194
-
195
- return indexes;
196
- } catch (error: any) {
197
- throw new Error(`获取表索引信息失败 [${tableName}]: ${error.message}`);
198
- }
199
- }
@@ -1,50 +0,0 @@
1
- /**
2
- * syncDb SQLite 特殊处理模块
3
- *
4
- * 包含:
5
- * - SQLite 重建表迁移(处理列修改等不支持的操作)
6
- */
7
-
8
- import { Logger } from '../../lib/logger.js';
9
- import { createTable } from './tableCreate.js';
10
- import type { SQL } from 'bun';
11
-
12
- // 是否为计划模式(从环境变量读取)
13
- const IS_PLAN = process.argv.includes('--plan');
14
-
15
- /**
16
- * SQLite 重建表迁移(简化版)
17
- *
18
- * SQLite 不支持修改列类型等操作,需要通过重建表实现:
19
- * 1. 创建临时表(新结构)
20
- * 2. 拷贝数据(仅公共列)
21
- * 3. 删除旧表
22
- * 4. 重命名临时表
23
- *
24
- * 注意:仅处理新增/修改字段,不处理复杂约束与复合索引
25
- *
26
- * @param sql - SQL 客户端实例
27
- * @param tableName - 表名
28
- * @param fields - 字段定义对象
29
- */
30
- export async function rebuildSqliteTable(sql: SQL, tableName: string, fields: Record<string, string>): Promise<void> {
31
- // 1. 读取现有列顺序
32
- const info = await sql.unsafe(`PRAGMA table_info(${tableName})`);
33
- const existingCols = info.map((r) => r.name);
34
- const targetCols = ['id', 'created_at', 'updated_at', 'deleted_at', 'state', ...Object.keys(fields)];
35
- const tmpTable = `${tableName}__tmp__${Date.now()}`;
36
-
37
- // 2. 创建新表(使用当前定义)
38
- await createTable(sql, tmpTable, fields);
39
-
40
- // 3. 拷贝数据(按交集列)
41
- const commonCols = targetCols.filter((c) => existingCols.includes(c));
42
- if (commonCols.length > 0) {
43
- const colsSql = commonCols.map((c) => `"${c}"`).join(', ');
44
- await sql.unsafe(`INSERT INTO "${tmpTable}" (${colsSql}) SELECT ${colsSql} FROM "${tableName}"`);
45
- }
46
-
47
- // 4. 删除旧表并重命名
48
- await sql.unsafe(`DROP TABLE "${tableName}"`);
49
- await sql.unsafe(`ALTER TABLE "${tmpTable}" RENAME TO "${tableName}"`);
50
- }
@@ -1,112 +0,0 @@
1
- /**
2
- * syncDb 状态管理模块
3
- *
4
- * 包含:
5
- * - 性能统计(阶段耗时、总体计时)
6
- * - 进度信息记录
7
- */
8
-
9
- import type { SQL } from 'bun';
10
- import { Logger } from '../../lib/logger.js';
11
-
12
- /**
13
- * 阶段统计信息
14
- */
15
- export interface PhaseStats {
16
- startTime: number;
17
- endTime?: number;
18
- duration?: number;
19
- }
20
-
21
- /**
22
- * 性能统计器
23
- */
24
- export class PerformanceTracker {
25
- private phases = new Map<string, PhaseStats>();
26
- private globalStart: number;
27
-
28
- constructor() {
29
- this.globalStart = Date.now();
30
- }
31
-
32
- /**
33
- * 标记阶段开始
34
- */
35
- markPhase(phase: string): void {
36
- this.phases.set(phase, { startTime: Date.now() });
37
- }
38
-
39
- /**
40
- * 获取阶段耗时
41
- */
42
- getPhaseTime(phase: string): string {
43
- const stats = this.phases.get(phase);
44
- if (!stats) return '0ms';
45
-
46
- const duration = stats.duration || Date.now() - stats.startTime;
47
- return duration > 1000 ? `${(duration / 1000).toFixed(2)}s` : `${duration}ms`;
48
- }
49
-
50
- /**
51
- * 完成阶段并记录耗时
52
- */
53
- finishPhase(phase: string): void {
54
- const stats = this.phases.get(phase);
55
- if (stats && !stats.endTime) {
56
- stats.endTime = Date.now();
57
- stats.duration = stats.endTime - stats.startTime;
58
- }
59
- }
60
-
61
- /**
62
- * 获取总耗时
63
- */
64
- getTotalTime(): string {
65
- const duration = Date.now() - this.globalStart;
66
- return duration > 1000 ? `${(duration / 1000).toFixed(2)}s` : `${duration}ms`;
67
- }
68
-
69
- /**
70
- * 输出所有阶段统计
71
- */
72
- logStats(): void {
73
- Logger.info('\n⏱️ 性能统计:');
74
- for (const [phase, stats] of this.phases) {
75
- const duration = stats.duration || Date.now() - stats.startTime;
76
- const timeStr = duration > 1000 ? `${(duration / 1000).toFixed(2)}s` : `${duration}ms`;
77
- Logger.info(` ${phase}: ${timeStr}`);
78
- }
79
- Logger.info(` 总耗时: ${this.getTotalTime()}`);
80
- }
81
- }
82
-
83
- /**
84
- * 进度记录器
85
- */
86
- export class ProgressLogger {
87
- /**
88
- * 记录表处理进度(紧凑格式)
89
- * @param current 当前进度
90
- * @param total 总数
91
- * @param tableName 表名
92
- * @param dirType 目录类型(核心/项目/组件名)
93
- */
94
- logTableProgress(current: number, total: number, tableName: string, dirType?: string): void {
95
- const typeInfo = dirType ? ` (${dirType})` : '';
96
- Logger.info(`[${current}/${total}] ${tableName}${typeInfo}`);
97
- }
98
-
99
- /**
100
- * 记录字段变更进度
101
- */
102
- logFieldChangeProgress(current: number, total: number, fieldName: string, changeType: string): void {
103
- Logger.info(` [${current}/${total}] 修改字段 ${fieldName} (${changeType})`);
104
- }
105
-
106
- /**
107
- * 记录索引创建进度
108
- */
109
- logIndexProgress(current: number, total: number, indexName: string): void {
110
- Logger.info(` [${current}/${total}] 创建索引: ${indexName}`);
111
- }
112
- }
@@ -1,214 +0,0 @@
1
- /**
2
- * syncDb 表操作模块
3
- *
4
- * 包含:
5
- * - 修改表结构
6
- * - 对比字段变化
7
- * - 应用变更计划
8
- */
9
-
10
- import { snakeCase } from 'es-toolkit/string';
11
- import { parseRule } from '../../util.js';
12
- import { Logger } from '../../lib/logger.js';
13
- import { IS_MYSQL, IS_PG, IS_SQLITE, SYSTEM_INDEX_FIELDS, CHANGE_TYPE_LABELS, typeMapping } from './constants.js';
14
- import { quoteIdentifier, logFieldChange, resolveDefaultValue, generateDefaultSql, isStringOrArrayType, getSqlType } from './helpers.js';
15
- import { buildIndexSQL, generateDDLClause, isPgCompatibleTypeChange } from './ddl.js';
16
- import { getTableColumns, getTableIndexes, type ColumnInfo } from './schema.js';
17
- import { compareFieldDefinition, applyTablePlan } from './apply.js';
18
- import { createTable } from './tableCreate.js';
19
- import type { TablePlan } from './types.js';
20
- import type { SQL } from 'bun';
21
-
22
- // 是否为计划模式(从环境变量读取)
23
- const IS_PLAN = process.argv.includes('--plan');
24
-
25
- // 导出 createTable 供其他模块使用
26
- export { createTable };
27
-
28
- /**
29
- * 同步表结构(对比和应用变更)
30
- *
31
- * 主要逻辑:
32
- * 1. 获取表的现有列和索引信息
33
- * 2. 对比每个字段的定义变化
34
- * 3. 生成 DDL 变更计划
35
- * 4. 处理索引的增删
36
- * 5. 应用变更计划
37
- *
38
- * 安全策略:
39
- * - 禁止字段类型变更(除 string<->array)
40
- * - 跳过危险的长度收缩
41
- * - 使用在线 DDL(MySQL/PG)
42
- *
43
- * @param sql - SQL 客户端实例
44
- * @param tableName - 表名
45
- * @param fields - 字段定义对象
46
- * @param globalCount - 全局统计对象(用于计数)
47
- * @returns 表结构变更计划
48
- */
49
- export async function modifyTable(sql: SQL, tableName: string, fields: Record<string, string>, globalCount: Record<string, number>): Promise<TablePlan> {
50
- const existingColumns = await getTableColumns(sql, tableName);
51
- const existingIndexes = await getTableIndexes(sql, tableName);
52
- let changed = false;
53
-
54
- const addClauses = [];
55
- const modifyClauses = [];
56
- const defaultClauses = [];
57
- const indexActions = [];
58
-
59
- for (const [fieldKey, fieldRule] of Object.entries(fields)) {
60
- // 转换字段名为下划线格式(用于与数据库字段对比)
61
- const dbFieldName = snakeCase(fieldKey);
62
-
63
- if (existingColumns[dbFieldName]) {
64
- const comparison = compareFieldDefinition(existingColumns[dbFieldName], fieldRule, dbFieldName);
65
- if (comparison.length > 0) {
66
- for (const c of comparison) {
67
- // 使用统一的日志格式函数和常量标签
68
- const changeLabel = CHANGE_TYPE_LABELS[c.type] || '未知';
69
- logFieldChange(tableName, dbFieldName, c.type, c.current, c.expected, changeLabel);
70
-
71
- // 全量计数:全局累加
72
- if (c.type === 'datatype') globalCount.typeChanges++;
73
- else if (c.type === 'length') globalCount.maxChanges++;
74
- else if (c.type === 'default') globalCount.defaultChanges++;
75
- else if (c.type === 'comment') globalCount.nameChanges++;
76
- }
77
-
78
- const parsed = parseRule(fieldRule);
79
- const { name: fieldName, type: fieldType, max: fieldMax, default: fieldDefault } = parsed;
80
-
81
- if (isStringOrArrayType(fieldType) && existingColumns[dbFieldName].length) {
82
- if (existingColumns[dbFieldName].length! > fieldMax) {
83
- Logger.warn(`[跳过危险变更] ${tableName}.${dbFieldName} 长度收缩 ${existingColumns[dbFieldName].length} -> ${fieldMax} 已被跳过`);
84
- }
85
- }
86
-
87
- const hasTypeChange = comparison.some((c) => c.type === 'datatype');
88
- const hasLengthChange = comparison.some((c) => c.type === 'length');
89
- const onlyDefaultChanged = comparison.every((c) => c.type === 'default');
90
- const defaultChanged = comparison.some((c) => c.type === 'default');
91
-
92
- // 严格限制:除 string/array 互转外,禁止任何字段类型变更;一旦发现,立即终止同步
93
- if (hasTypeChange) {
94
- const currentSqlType = String(existingColumns[dbFieldName].type || '').toLowerCase();
95
- const newSqlType = String(typeMapping[fieldType] || '').toLowerCase();
96
- const errorMsg = [`禁止字段类型变更: ${tableName}.${dbFieldName}`, `当前类型: ${currentSqlType}`, `目标类型: ${newSqlType}`, `说明: 仅允许 string<->array 互相切换,其他类型变更需要手动处理`].join('\n');
97
- throw new Error(errorMsg);
98
- }
99
-
100
- // 默认值变化处理
101
- if (defaultChanged) {
102
- // 使用公共函数处理默认值
103
- const actualDefault = resolveDefaultValue(fieldDefault, fieldType);
104
-
105
- // 生成 SQL DEFAULT 值(不包含前导空格,因为要用于 ALTER COLUMN)
106
- let v: string | null = null;
107
- if (actualDefault !== 'null') {
108
- const defaultSql = generateDefaultSql(actualDefault, fieldType);
109
- // 移除前导空格 ' DEFAULT ' -> 'DEFAULT '
110
- v = defaultSql.trim().replace(/^DEFAULT\s+/, '');
111
- }
112
-
113
- if (v !== null && v !== '') {
114
- if (IS_PG) {
115
- defaultClauses.push(`ALTER COLUMN "${dbFieldName}" SET DEFAULT ${v}`);
116
- } else if (IS_MYSQL && onlyDefaultChanged) {
117
- // MySQL 的 TEXT/BLOB 不允许 DEFAULT,跳过 text 类型
118
- if (fieldType !== 'text') {
119
- defaultClauses.push(`ALTER COLUMN \`${dbFieldName}\` SET DEFAULT ${v}`);
120
- }
121
- }
122
- }
123
- }
124
-
125
- // 若不仅仅是默认值变化,继续生成修改子句
126
- if (!onlyDefaultChanged) {
127
- let skipModify = false;
128
- if (hasLengthChange && isStringOrArrayType(fieldType) && existingColumns[dbFieldName].length) {
129
- const oldLen = existingColumns[dbFieldName].length!;
130
- const isShrink = oldLen > fieldMax;
131
- if (isShrink) skipModify = true;
132
- }
133
-
134
- if (hasTypeChange) {
135
- if (IS_PG && isPgCompatibleTypeChange(existingColumns[dbFieldName].type, typeMapping[fieldType].toLowerCase())) {
136
- Logger.info(`[PG兼容类型变更] ${tableName}.${dbFieldName} ${existingColumns[dbFieldName].type} -> ${typeMapping[fieldType].toLowerCase()} 允许执行`);
137
- }
138
- }
139
-
140
- if (!skipModify) modifyClauses.push(generateDDLClause(fieldKey, fieldRule, false));
141
- }
142
- changed = true;
143
- }
144
- } else {
145
- const parsed = parseRule(fieldRule);
146
- const { name: fieldName, type: fieldType, max: fieldMax, default: fieldDefault } = parsed;
147
- const lenPart = isStringOrArrayType(fieldType) ? ` 长度:${parseInt(String(fieldMax))}` : '';
148
- Logger.info(` + 新增字段 ${dbFieldName} (${fieldType}${lenPart})`);
149
- addClauses.push(generateDDLClause(fieldKey, fieldRule, true));
150
- changed = true;
151
- globalCount.addFields++;
152
- }
153
- }
154
-
155
- // 检查系统字段索引
156
- for (const sysField of ['created_at', 'updated_at', 'state']) {
157
- const idxName = `idx_${sysField}`;
158
- if (!existingIndexes[idxName]) {
159
- indexActions.push({ action: 'create', indexName: idxName, fieldName: sysField });
160
- changed = true;
161
- globalCount.indexCreate++;
162
- }
163
- }
164
-
165
- // 检查业务字段索引
166
- for (const [fieldKey, fieldRule] of Object.entries(fields)) {
167
- // 转换字段名为下划线格式
168
- const dbFieldName = snakeCase(fieldKey);
169
-
170
- const parsed = parseRule(fieldRule);
171
- const indexName = `idx_${dbFieldName}`;
172
- if (parsed.index === 1 && !existingIndexes[indexName]) {
173
- indexActions.push({ action: 'create', indexName, fieldName: dbFieldName });
174
- changed = true;
175
- globalCount.indexCreate++;
176
- } else if (!(parsed.index === 1) && existingIndexes[indexName] && existingIndexes[indexName].length === 1) {
177
- indexActions.push({ action: 'drop', indexName, fieldName: dbFieldName });
178
- changed = true;
179
- globalCount.indexDrop++;
180
- }
181
- }
182
-
183
- // PG 列注释处理
184
- const commentActions = [];
185
- if (IS_PG) {
186
- for (const [fieldKey, fieldRule] of Object.entries(fields)) {
187
- // 转换字段名为下划线格式
188
- const dbFieldName = snakeCase(fieldKey);
189
-
190
- if (existingColumns[dbFieldName]) {
191
- const parsed = parseRule(fieldRule);
192
- const { name: fieldName } = parsed;
193
- const curr = existingColumns[dbFieldName].comment || '';
194
- const want = fieldName && fieldName !== 'null' ? String(fieldName) : '';
195
- if (want !== curr) {
196
- commentActions.push(`COMMENT ON COLUMN "${tableName}"."${dbFieldName}" IS ${want ? `'${want}'` : 'NULL'}`);
197
- changed = true;
198
- }
199
- }
200
- }
201
- }
202
-
203
- // 仅当存在实际动作时才认为有变更(避免仅日志的收缩跳过被计为修改)
204
- changed = addClauses.length > 0 || modifyClauses.length > 0 || defaultClauses.length > 0 || indexActions.length > 0 || commentActions.length > 0;
205
-
206
- const plan = { changed, addClauses, modifyClauses, defaultClauses, indexActions, commentActions };
207
-
208
- // 将计划应用(包含 --plan 情况下仅输出)
209
- if (plan.changed) {
210
- await applyTablePlan(sql, tableName, fields, plan, globalCount);
211
- }
212
-
213
- return plan;
214
- }