befly 3.8.25 → 3.8.29

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 (62) hide show
  1. package/config.ts +8 -9
  2. package/hooks/{rateLimit.ts → _rateLimit.ts} +7 -13
  3. package/hooks/auth.ts +3 -11
  4. package/hooks/cors.ts +1 -4
  5. package/hooks/parser.ts +6 -8
  6. package/hooks/permission.ts +9 -12
  7. package/hooks/validator.ts +6 -9
  8. package/lib/cacheHelper.ts +0 -4
  9. package/lib/{database.ts → connect.ts} +65 -18
  10. package/lib/logger.ts +1 -17
  11. package/lib/redisHelper.ts +6 -5
  12. package/loader/loadApis.ts +3 -3
  13. package/loader/loadHooks.ts +15 -41
  14. package/loader/loadPlugins.ts +10 -16
  15. package/main.ts +25 -28
  16. package/package.json +4 -4
  17. package/plugins/cache.ts +2 -2
  18. package/plugins/cipher.ts +15 -0
  19. package/plugins/config.ts +16 -0
  20. package/plugins/db.ts +7 -17
  21. package/plugins/jwt.ts +15 -0
  22. package/plugins/logger.ts +1 -1
  23. package/plugins/redis.ts +4 -4
  24. package/plugins/tool.ts +50 -0
  25. package/router/api.ts +56 -42
  26. package/router/static.ts +12 -12
  27. package/sync/syncAll.ts +2 -20
  28. package/sync/syncApi.ts +7 -7
  29. package/sync/syncDb/apply.ts +10 -12
  30. package/sync/syncDb/constants.ts +64 -12
  31. package/sync/syncDb/ddl.ts +9 -8
  32. package/sync/syncDb/helpers.ts +7 -119
  33. package/sync/syncDb/schema.ts +16 -19
  34. package/sync/syncDb/sqlite.ts +1 -3
  35. package/sync/syncDb/table.ts +13 -146
  36. package/sync/syncDb/tableCreate.ts +28 -12
  37. package/sync/syncDb/types.ts +126 -0
  38. package/sync/syncDb/version.ts +4 -7
  39. package/sync/syncDb.ts +151 -6
  40. package/sync/syncDev.ts +19 -15
  41. package/sync/syncMenu.ts +87 -75
  42. package/tests/redisHelper.test.ts +15 -16
  43. package/tests/sync-connection.test.ts +189 -0
  44. package/tests/syncDb-apply.test.ts +288 -0
  45. package/tests/syncDb-constants.test.ts +151 -0
  46. package/tests/syncDb-ddl.test.ts +206 -0
  47. package/tests/syncDb-helpers.test.ts +113 -0
  48. package/tests/syncDb-schema.test.ts +178 -0
  49. package/tests/syncDb-types.test.ts +130 -0
  50. package/tsconfig.json +2 -2
  51. package/types/api.d.ts +1 -1
  52. package/types/befly.d.ts +23 -21
  53. package/types/common.d.ts +0 -29
  54. package/types/context.d.ts +8 -6
  55. package/types/hook.d.ts +3 -4
  56. package/types/plugin.d.ts +3 -0
  57. package/hooks/errorHandler.ts +0 -23
  58. package/hooks/requestId.ts +0 -24
  59. package/hooks/requestLogger.ts +0 -25
  60. package/hooks/responseFormatter.ts +0 -64
  61. package/router/root.ts +0 -56
  62. package/sync/syncDb/index.ts +0 -164
@@ -0,0 +1,288 @@
1
+ /**
2
+ * syncDb 变更应用模块测试
3
+ *
4
+ * 测试 apply.ts 中的函数:
5
+ * - compareFieldDefinition
6
+ */
7
+
8
+ import { describe, test, expect, beforeAll } from 'bun:test';
9
+ import { setDbType } from '../sync/syncDb/constants.js';
10
+
11
+ // 设置数据库类型为 MySQL
12
+ setDbType('mysql');
13
+
14
+ let compareFieldDefinition: any;
15
+
16
+ beforeAll(async () => {
17
+ const apply = await import('../sync/syncDb/apply.js');
18
+ compareFieldDefinition = apply.compareFieldDefinition;
19
+ });
20
+
21
+ describe('compareFieldDefinition', () => {
22
+ describe('长度变化检测', () => {
23
+ test('string 类型长度变化被检测到', () => {
24
+ const existingColumn = {
25
+ type: 'varchar',
26
+ max: 50,
27
+ nullable: false,
28
+ defaultValue: '',
29
+ comment: '用户名'
30
+ };
31
+ const fieldDef = {
32
+ name: '用户名',
33
+ type: 'string',
34
+ max: 100,
35
+ nullable: false,
36
+ default: null
37
+ };
38
+
39
+ const changes = compareFieldDefinition(existingColumn, fieldDef);
40
+ const lengthChange = changes.find((c: any) => c.type === 'length');
41
+
42
+ expect(lengthChange).toBeDefined();
43
+ expect(lengthChange.current).toBe(50);
44
+ expect(lengthChange.expected).toBe(100);
45
+ });
46
+
47
+ test('长度相同无变化', () => {
48
+ const existingColumn = {
49
+ type: 'varchar',
50
+ max: 100,
51
+ nullable: false,
52
+ defaultValue: '',
53
+ comment: '用户名'
54
+ };
55
+ const fieldDef = {
56
+ name: '用户名',
57
+ type: 'string',
58
+ max: 100,
59
+ nullable: false,
60
+ default: null
61
+ };
62
+
63
+ const changes = compareFieldDefinition(existingColumn, fieldDef);
64
+ const lengthChange = changes.find((c: any) => c.type === 'length');
65
+
66
+ expect(lengthChange).toBeUndefined();
67
+ });
68
+ });
69
+
70
+ describe('注释变化检测', () => {
71
+ test('注释变化被检测到', () => {
72
+ const existingColumn = {
73
+ type: 'varchar',
74
+ max: 100,
75
+ nullable: false,
76
+ defaultValue: '',
77
+ comment: '旧注释'
78
+ };
79
+ const fieldDef = {
80
+ name: '新注释',
81
+ type: 'string',
82
+ max: 100,
83
+ nullable: false,
84
+ default: null
85
+ };
86
+
87
+ const changes = compareFieldDefinition(existingColumn, fieldDef);
88
+ const commentChange = changes.find((c: any) => c.type === 'comment');
89
+
90
+ expect(commentChange).toBeDefined();
91
+ expect(commentChange.current).toBe('旧注释');
92
+ expect(commentChange.expected).toBe('新注释');
93
+ });
94
+
95
+ test('注释相同无变化', () => {
96
+ const existingColumn = {
97
+ type: 'varchar',
98
+ max: 100,
99
+ nullable: false,
100
+ defaultValue: '',
101
+ comment: '用户名'
102
+ };
103
+ const fieldDef = {
104
+ name: '用户名',
105
+ type: 'string',
106
+ max: 100,
107
+ nullable: false,
108
+ default: null
109
+ };
110
+
111
+ const changes = compareFieldDefinition(existingColumn, fieldDef);
112
+ const commentChange = changes.find((c: any) => c.type === 'comment');
113
+
114
+ expect(commentChange).toBeUndefined();
115
+ });
116
+ });
117
+
118
+ describe('数据类型变化检测', () => {
119
+ test('类型变化被检测到', () => {
120
+ const existingColumn = {
121
+ type: 'bigint',
122
+ max: null,
123
+ nullable: false,
124
+ defaultValue: 0,
125
+ comment: '数量'
126
+ };
127
+ const fieldDef = {
128
+ name: '数量',
129
+ type: 'string',
130
+ max: 100,
131
+ nullable: false,
132
+ default: null
133
+ };
134
+
135
+ const changes = compareFieldDefinition(existingColumn, fieldDef);
136
+ const typeChange = changes.find((c: any) => c.type === 'datatype');
137
+
138
+ expect(typeChange).toBeDefined();
139
+ expect(typeChange.current).toBe('bigint');
140
+ expect(typeChange.expected).toBe('varchar');
141
+ });
142
+
143
+ test('类型相同无变化', () => {
144
+ const existingColumn = {
145
+ type: 'bigint',
146
+ max: null,
147
+ nullable: false,
148
+ defaultValue: 0,
149
+ comment: '数量'
150
+ };
151
+ const fieldDef = {
152
+ name: '数量',
153
+ type: 'number',
154
+ max: null,
155
+ nullable: false,
156
+ default: 0
157
+ };
158
+
159
+ const changes = compareFieldDefinition(existingColumn, fieldDef);
160
+ const typeChange = changes.find((c: any) => c.type === 'datatype');
161
+
162
+ expect(typeChange).toBeUndefined();
163
+ });
164
+ });
165
+
166
+ describe('可空性变化检测', () => {
167
+ test('nullable 变化被检测到', () => {
168
+ const existingColumn = {
169
+ type: 'varchar',
170
+ max: 100,
171
+ nullable: false,
172
+ defaultValue: '',
173
+ comment: '用户名'
174
+ };
175
+ const fieldDef = {
176
+ name: '用户名',
177
+ type: 'string',
178
+ max: 100,
179
+ nullable: true,
180
+ default: null
181
+ };
182
+
183
+ const changes = compareFieldDefinition(existingColumn, fieldDef);
184
+ const nullableChange = changes.find((c: any) => c.type === 'nullable');
185
+
186
+ expect(nullableChange).toBeDefined();
187
+ expect(nullableChange.current).toBe(false);
188
+ expect(nullableChange.expected).toBe(true);
189
+ });
190
+ });
191
+
192
+ describe('默认值变化检测', () => {
193
+ test('默认值变化被检测到', () => {
194
+ const existingColumn = {
195
+ type: 'varchar',
196
+ max: 100,
197
+ nullable: false,
198
+ defaultValue: 'old',
199
+ comment: '用户名'
200
+ };
201
+ const fieldDef = {
202
+ name: '用户名',
203
+ type: 'string',
204
+ max: 100,
205
+ nullable: false,
206
+ default: 'new'
207
+ };
208
+
209
+ const changes = compareFieldDefinition(existingColumn, fieldDef);
210
+ const defaultChange = changes.find((c: any) => c.type === 'default');
211
+
212
+ expect(defaultChange).toBeDefined();
213
+ expect(defaultChange.current).toBe('old');
214
+ expect(defaultChange.expected).toBe('new');
215
+ });
216
+
217
+ test('null 默认值被正确处理', () => {
218
+ const existingColumn = {
219
+ type: 'varchar',
220
+ max: 100,
221
+ nullable: false,
222
+ defaultValue: '',
223
+ comment: '用户名'
224
+ };
225
+ const fieldDef = {
226
+ name: '用户名',
227
+ type: 'string',
228
+ max: 100,
229
+ nullable: false,
230
+ default: null // null 会被解析为空字符串
231
+ };
232
+
233
+ const changes = compareFieldDefinition(existingColumn, fieldDef);
234
+ const defaultChange = changes.find((c: any) => c.type === 'default');
235
+
236
+ // null -> '' (空字符串),与现有值相同,无变化
237
+ expect(defaultChange).toBeUndefined();
238
+ });
239
+ });
240
+
241
+ describe('多变化组合', () => {
242
+ test('多个变化同时被检测', () => {
243
+ const existingColumn = {
244
+ type: 'varchar',
245
+ max: 50,
246
+ nullable: false,
247
+ defaultValue: 'old',
248
+ comment: '旧注释'
249
+ };
250
+ const fieldDef = {
251
+ name: '新注释',
252
+ type: 'string',
253
+ max: 100,
254
+ nullable: true,
255
+ default: 'new'
256
+ };
257
+
258
+ const changes = compareFieldDefinition(existingColumn, fieldDef);
259
+
260
+ expect(changes.length).toBe(4); // length, comment, nullable, default
261
+ expect(changes.some((c: any) => c.type === 'length')).toBe(true);
262
+ expect(changes.some((c: any) => c.type === 'comment')).toBe(true);
263
+ expect(changes.some((c: any) => c.type === 'nullable')).toBe(true);
264
+ expect(changes.some((c: any) => c.type === 'default')).toBe(true);
265
+ });
266
+
267
+ test('无变化返回空数组', () => {
268
+ const existingColumn = {
269
+ type: 'varchar',
270
+ max: 100,
271
+ nullable: false,
272
+ defaultValue: '',
273
+ comment: '用户名'
274
+ };
275
+ const fieldDef = {
276
+ name: '用户名',
277
+ type: 'string',
278
+ max: 100,
279
+ nullable: false,
280
+ default: null
281
+ };
282
+
283
+ const changes = compareFieldDefinition(existingColumn, fieldDef);
284
+
285
+ expect(changes.length).toBe(0);
286
+ });
287
+ });
288
+ });
@@ -0,0 +1,151 @@
1
+ /**
2
+ * syncDb 常量模块测试
3
+ *
4
+ * 测试 constants.ts 中的常量:
5
+ * - DB_VERSION_REQUIREMENTS
6
+ * - SYSTEM_FIELDS
7
+ * - SYSTEM_INDEX_FIELDS
8
+ * - CHANGE_TYPE_LABELS
9
+ * - MYSQL_TABLE_CONFIG
10
+ * - typeMapping
11
+ */
12
+
13
+ import { describe, test, expect, beforeAll } from 'bun:test';
14
+ import { setDbType } from '../sync/syncDb/constants.js';
15
+
16
+ // 设置数据库类型为 MySQL
17
+ setDbType('mysql');
18
+
19
+ let constants: any;
20
+
21
+ beforeAll(async () => {
22
+ constants = await import('../sync/syncDb/constants.js');
23
+ });
24
+
25
+ describe('DB_VERSION_REQUIREMENTS', () => {
26
+ test('MySQL 最低版本为 8', () => {
27
+ expect(constants.DB_VERSION_REQUIREMENTS.MYSQL_MIN_MAJOR).toBe(8);
28
+ });
29
+
30
+ test('PostgreSQL 最低版本为 17', () => {
31
+ expect(constants.DB_VERSION_REQUIREMENTS.POSTGRES_MIN_MAJOR).toBe(17);
32
+ });
33
+
34
+ test('SQLite 最低版本为 3.50.0', () => {
35
+ expect(constants.DB_VERSION_REQUIREMENTS.SQLITE_MIN_VERSION).toBe('3.50.0');
36
+ });
37
+ });
38
+
39
+ describe('SYSTEM_FIELDS', () => {
40
+ test('包含 id 字段', () => {
41
+ expect(constants.SYSTEM_FIELDS.ID.name).toBe('id');
42
+ });
43
+
44
+ test('包含 created_at 字段', () => {
45
+ expect(constants.SYSTEM_FIELDS.CREATED_AT.name).toBe('created_at');
46
+ });
47
+
48
+ test('包含 updated_at 字段', () => {
49
+ expect(constants.SYSTEM_FIELDS.UPDATED_AT.name).toBe('updated_at');
50
+ });
51
+
52
+ test('包含 deleted_at 字段', () => {
53
+ expect(constants.SYSTEM_FIELDS.DELETED_AT.name).toBe('deleted_at');
54
+ });
55
+
56
+ test('包含 state 字段', () => {
57
+ expect(constants.SYSTEM_FIELDS.STATE.name).toBe('state');
58
+ });
59
+ });
60
+
61
+ describe('SYSTEM_INDEX_FIELDS', () => {
62
+ test('包含 created_at', () => {
63
+ expect(constants.SYSTEM_INDEX_FIELDS).toContain('created_at');
64
+ });
65
+
66
+ test('包含 updated_at', () => {
67
+ expect(constants.SYSTEM_INDEX_FIELDS).toContain('updated_at');
68
+ });
69
+
70
+ test('包含 state', () => {
71
+ expect(constants.SYSTEM_INDEX_FIELDS).toContain('state');
72
+ });
73
+
74
+ test('共 3 个系统索引字段', () => {
75
+ expect(constants.SYSTEM_INDEX_FIELDS.length).toBe(3);
76
+ });
77
+ });
78
+
79
+ describe('CHANGE_TYPE_LABELS', () => {
80
+ test('length 对应 "长度"', () => {
81
+ expect(constants.CHANGE_TYPE_LABELS.length).toBe('长度');
82
+ });
83
+
84
+ test('datatype 对应 "类型"', () => {
85
+ expect(constants.CHANGE_TYPE_LABELS.datatype).toBe('类型');
86
+ });
87
+
88
+ test('comment 对应 "注释"', () => {
89
+ expect(constants.CHANGE_TYPE_LABELS.comment).toBe('注释');
90
+ });
91
+
92
+ test('default 对应 "默认值"', () => {
93
+ expect(constants.CHANGE_TYPE_LABELS.default).toBe('默认值');
94
+ });
95
+ });
96
+
97
+ describe('MYSQL_TABLE_CONFIG', () => {
98
+ test('ENGINE 为 InnoDB', () => {
99
+ expect(constants.MYSQL_TABLE_CONFIG.ENGINE).toBe('InnoDB');
100
+ });
101
+
102
+ test('CHARSET 为 utf8mb4', () => {
103
+ expect(constants.MYSQL_TABLE_CONFIG.CHARSET).toBe('utf8mb4');
104
+ });
105
+
106
+ test('COLLATE 为 utf8mb4_0900_ai_ci', () => {
107
+ expect(constants.MYSQL_TABLE_CONFIG.COLLATE).toBe('utf8mb4_0900_ai_ci');
108
+ });
109
+ });
110
+
111
+ describe('typeMapping (MySQL)', () => {
112
+ test('number 映射为 BIGINT', () => {
113
+ expect(constants.typeMapping.number).toBe('BIGINT');
114
+ });
115
+
116
+ test('string 映射为 VARCHAR', () => {
117
+ expect(constants.typeMapping.string).toBe('VARCHAR');
118
+ });
119
+
120
+ test('text 映射为 MEDIUMTEXT', () => {
121
+ expect(constants.typeMapping.text).toBe('MEDIUMTEXT');
122
+ });
123
+
124
+ test('array_string 映射为 VARCHAR', () => {
125
+ expect(constants.typeMapping.array_string).toBe('VARCHAR');
126
+ });
127
+
128
+ test('array_text 映射为 MEDIUMTEXT', () => {
129
+ expect(constants.typeMapping.array_text).toBe('MEDIUMTEXT');
130
+ });
131
+ });
132
+
133
+ describe('IS_PLAN', () => {
134
+ test('IS_PLAN 为 boolean 类型', () => {
135
+ expect(typeof constants.IS_PLAN).toBe('boolean');
136
+ });
137
+ });
138
+
139
+ describe('数据库类型判断 (MySQL)', () => {
140
+ test('IS_MYSQL 为 true', () => {
141
+ expect(constants.IS_MYSQL).toBe(true);
142
+ });
143
+
144
+ test('IS_PG 为 false', () => {
145
+ expect(constants.IS_PG).toBe(false);
146
+ });
147
+
148
+ test('IS_SQLITE 为 false', () => {
149
+ expect(constants.IS_SQLITE).toBe(false);
150
+ });
151
+ });
@@ -0,0 +1,206 @@
1
+ /**
2
+ * syncDb DDL 构建模块测试
3
+ *
4
+ * 测试 ddl.ts 中的函数:
5
+ * - buildIndexSQL
6
+ * - buildSystemColumnDefs
7
+ * - buildBusinessColumnDefs
8
+ * - generateDDLClause
9
+ * - isPgCompatibleTypeChange
10
+ */
11
+
12
+ import { describe, test, expect, beforeAll } from 'bun:test';
13
+ import { setDbType } from '../sync/syncDb/constants.js';
14
+
15
+ // 设置数据库类型为 MySQL
16
+ setDbType('mysql');
17
+
18
+ let buildIndexSQL: any;
19
+ let buildSystemColumnDefs: any;
20
+ let buildBusinessColumnDefs: any;
21
+ let generateDDLClause: any;
22
+ let isPgCompatibleTypeChange: any;
23
+
24
+ beforeAll(async () => {
25
+ const ddl = await import('../sync/syncDb/ddl.js');
26
+ buildIndexSQL = ddl.buildIndexSQL;
27
+ buildSystemColumnDefs = ddl.buildSystemColumnDefs;
28
+ buildBusinessColumnDefs = ddl.buildBusinessColumnDefs;
29
+ generateDDLClause = ddl.generateDDLClause;
30
+ isPgCompatibleTypeChange = ddl.isPgCompatibleTypeChange;
31
+ });
32
+
33
+ describe('buildIndexSQL (MySQL)', () => {
34
+ test('创建索引 SQL', () => {
35
+ const sql = buildIndexSQL('user', 'idx_created_at', 'created_at', 'create');
36
+ expect(sql).toContain('ALTER TABLE `user`');
37
+ expect(sql).toContain('ADD INDEX `idx_created_at`');
38
+ expect(sql).toContain('(`created_at`)');
39
+ expect(sql).toContain('ALGORITHM=INPLACE');
40
+ expect(sql).toContain('LOCK=NONE');
41
+ });
42
+
43
+ test('删除索引 SQL', () => {
44
+ const sql = buildIndexSQL('user', 'idx_created_at', 'created_at', 'drop');
45
+ expect(sql).toContain('ALTER TABLE `user`');
46
+ expect(sql).toContain('DROP INDEX `idx_created_at`');
47
+ });
48
+ });
49
+
50
+ describe('buildSystemColumnDefs (MySQL)', () => {
51
+ test('返回 5 个系统字段定义', () => {
52
+ const defs = buildSystemColumnDefs();
53
+ expect(defs.length).toBe(5);
54
+ });
55
+
56
+ test('包含 id 主键', () => {
57
+ const defs = buildSystemColumnDefs();
58
+ const idDef = defs.find((d: string) => d.includes('`id`'));
59
+ expect(idDef).toContain('PRIMARY KEY');
60
+ expect(idDef).toContain('AUTO_INCREMENT');
61
+ expect(idDef).toContain('BIGINT UNSIGNED');
62
+ });
63
+
64
+ test('包含 created_at 字段', () => {
65
+ const defs = buildSystemColumnDefs();
66
+ const def = defs.find((d: string) => d.includes('`created_at`'));
67
+ expect(def).toContain('BIGINT UNSIGNED');
68
+ expect(def).toContain('NOT NULL');
69
+ expect(def).toContain('DEFAULT 0');
70
+ });
71
+
72
+ test('包含 state 字段', () => {
73
+ const defs = buildSystemColumnDefs();
74
+ const def = defs.find((d: string) => d.includes('`state`'));
75
+ expect(def).toContain('BIGINT UNSIGNED');
76
+ expect(def).toContain('NOT NULL');
77
+ expect(def).toContain('DEFAULT 0');
78
+ });
79
+ });
80
+
81
+ describe('buildBusinessColumnDefs (MySQL)', () => {
82
+ test('生成 string 类型字段', () => {
83
+ const fields = {
84
+ userName: {
85
+ name: '用户名',
86
+ type: 'string',
87
+ max: 50,
88
+ default: null,
89
+ unique: false,
90
+ nullable: false,
91
+ unsigned: true
92
+ }
93
+ };
94
+ const defs = buildBusinessColumnDefs(fields);
95
+ expect(defs.length).toBe(1);
96
+ expect(defs[0]).toContain('`user_name`');
97
+ expect(defs[0]).toContain('VARCHAR(50)');
98
+ expect(defs[0]).toContain('NOT NULL');
99
+ expect(defs[0]).toContain("DEFAULT ''");
100
+ expect(defs[0]).toContain('COMMENT "用户名"');
101
+ });
102
+
103
+ test('生成 number 类型字段', () => {
104
+ const fields = {
105
+ age: {
106
+ name: '年龄',
107
+ type: 'number',
108
+ max: null,
109
+ default: 0,
110
+ unique: false,
111
+ nullable: false,
112
+ unsigned: true
113
+ }
114
+ };
115
+ const defs = buildBusinessColumnDefs(fields);
116
+ expect(defs[0]).toContain('`age`');
117
+ expect(defs[0]).toContain('BIGINT UNSIGNED');
118
+ expect(defs[0]).toContain('DEFAULT 0');
119
+ });
120
+
121
+ test('生成 unique 字段', () => {
122
+ const fields = {
123
+ email: {
124
+ name: '邮箱',
125
+ type: 'string',
126
+ max: 100,
127
+ default: null,
128
+ unique: true,
129
+ nullable: false,
130
+ unsigned: true
131
+ }
132
+ };
133
+ const defs = buildBusinessColumnDefs(fields);
134
+ expect(defs[0]).toContain('UNIQUE');
135
+ });
136
+
137
+ test('生成 nullable 字段', () => {
138
+ const fields = {
139
+ remark: {
140
+ name: '备注',
141
+ type: 'string',
142
+ max: 200,
143
+ default: null,
144
+ unique: false,
145
+ nullable: true,
146
+ unsigned: true
147
+ }
148
+ };
149
+ const defs = buildBusinessColumnDefs(fields);
150
+ expect(defs[0]).toContain('NULL');
151
+ expect(defs[0]).not.toContain('NOT NULL');
152
+ });
153
+ });
154
+
155
+ describe('generateDDLClause (MySQL)', () => {
156
+ test('生成 ADD COLUMN 子句', () => {
157
+ const fieldDef = {
158
+ name: '用户名',
159
+ type: 'string',
160
+ max: 50,
161
+ default: null,
162
+ unique: false,
163
+ nullable: false,
164
+ unsigned: true
165
+ };
166
+ const clause = generateDDLClause('userName', fieldDef, true);
167
+ expect(clause).toContain('ADD COLUMN');
168
+ expect(clause).toContain('`user_name`');
169
+ expect(clause).toContain('VARCHAR(50)');
170
+ });
171
+
172
+ test('生成 MODIFY COLUMN 子句', () => {
173
+ const fieldDef = {
174
+ name: '用户名',
175
+ type: 'string',
176
+ max: 100,
177
+ default: null,
178
+ unique: false,
179
+ nullable: false,
180
+ unsigned: true
181
+ };
182
+ const clause = generateDDLClause('userName', fieldDef, false);
183
+ expect(clause).toContain('MODIFY COLUMN');
184
+ expect(clause).toContain('`user_name`');
185
+ expect(clause).toContain('VARCHAR(100)');
186
+ });
187
+ });
188
+
189
+ describe('isPgCompatibleTypeChange', () => {
190
+ test('varchar -> text 是兼容变更', () => {
191
+ expect(isPgCompatibleTypeChange('character varying', 'text')).toBe(true);
192
+ });
193
+
194
+ test('text -> varchar 不是兼容变更', () => {
195
+ expect(isPgCompatibleTypeChange('text', 'character varying')).toBe(false);
196
+ });
197
+
198
+ test('相同类型不是变更', () => {
199
+ expect(isPgCompatibleTypeChange('text', 'text')).toBe(false);
200
+ });
201
+
202
+ test('空值处理', () => {
203
+ expect(isPgCompatibleTypeChange(null, 'text')).toBe(false);
204
+ expect(isPgCompatibleTypeChange('text', null)).toBe(false);
205
+ });
206
+ });