fdb2 1.0.36 → 1.0.38

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 (107) hide show
  1. package/README.md +41 -0
  2. package/dist/public/explorer.css +533 -485
  3. package/dist/public/explorer.js +1277 -868
  4. package/dist/public/index.css +172 -34
  5. package/dist/public/index.js +103 -49
  6. package/dist/public/layout.js +3 -3
  7. package/dist/public/vue.js +1 -1
  8. package/dist/server/core/schema/__tests__/canonical.test.ts +195 -0
  9. package/dist/server/core/schema/__tests__/ddl-emitter.test.ts +456 -0
  10. package/dist/server/core/schema/__tests__/diff.test.ts +242 -0
  11. package/dist/server/core/schema/__tests__/roundtrip.property.test.ts +403 -0
  12. package/dist/server/core/schema/canonical.d.ts +50 -0
  13. package/dist/server/core/schema/canonical.d.ts.map +1 -0
  14. package/dist/server/core/schema/canonical.js +212 -0
  15. package/dist/server/core/schema/canonical.js.map +1 -0
  16. package/dist/server/core/schema/canonical.ts +223 -0
  17. package/dist/server/core/schema/ddl-emitter.d.ts +68 -0
  18. package/dist/server/core/schema/ddl-emitter.d.ts.map +1 -0
  19. package/dist/server/core/schema/ddl-emitter.js +813 -0
  20. package/dist/server/core/schema/ddl-emitter.js.map +1 -0
  21. package/dist/server/core/schema/ddl-emitter.ts +925 -0
  22. package/dist/server/core/schema/diff.d.ts +16 -0
  23. package/dist/server/core/schema/diff.d.ts.map +1 -0
  24. package/dist/server/core/schema/diff.js +115 -0
  25. package/dist/server/core/schema/diff.js.map +1 -0
  26. package/dist/server/core/schema/diff.ts +134 -0
  27. package/dist/server/core/schema/type-defaults.d.ts +27 -0
  28. package/dist/server/core/schema/type-defaults.d.ts.map +1 -0
  29. package/dist/server/core/schema/type-defaults.js +61 -0
  30. package/dist/server/core/schema/type-defaults.js.map +1 -0
  31. package/dist/server/core/schema/type-defaults.ts +79 -0
  32. package/dist/server/index.d.ts +1 -1
  33. package/dist/server/index.d.ts.map +1 -1
  34. package/dist/server/index.js +347 -124
  35. package/dist/server/index.js.map +1 -1
  36. package/dist/server/index.ts +336 -161
  37. package/dist/server/model/ddl-spec.d.ts +163 -0
  38. package/dist/server/model/ddl-spec.d.ts.map +1 -0
  39. package/dist/server/model/ddl-spec.js +32 -0
  40. package/dist/server/model/ddl-spec.js.map +1 -0
  41. package/dist/server/model/ddl-spec.ts +175 -0
  42. package/dist/server/service/connection.service.d.ts +20 -0
  43. package/dist/server/service/connection.service.d.ts.map +1 -1
  44. package/dist/server/service/connection.service.js +99 -25
  45. package/dist/server/service/connection.service.js.map +1 -1
  46. package/dist/server/service/connection.service.ts +104 -25
  47. package/dist/server/service/database/base.service.d.ts +0 -4
  48. package/dist/server/service/database/base.service.d.ts.map +1 -1
  49. package/dist/server/service/database/base.service.js.map +1 -1
  50. package/dist/server/service/database/base.service.ts +0 -5
  51. package/dist/server/service/database/clickhouse.service.d.ts +0 -5
  52. package/dist/server/service/database/clickhouse.service.d.ts.map +1 -1
  53. package/dist/server/service/database/clickhouse.service.js +0 -49
  54. package/dist/server/service/database/clickhouse.service.js.map +1 -1
  55. package/dist/server/service/database/clickhouse.service.ts +0 -52
  56. package/dist/server/service/database/cockroachdb.service.d.ts +0 -5
  57. package/dist/server/service/database/cockroachdb.service.d.ts.map +1 -1
  58. package/dist/server/service/database/cockroachdb.service.js +11 -142
  59. package/dist/server/service/database/cockroachdb.service.js.map +1 -1
  60. package/dist/server/service/database/cockroachdb.service.ts +11 -161
  61. package/dist/server/service/database/database.service.d.ts +55 -2
  62. package/dist/server/service/database/database.service.d.ts.map +1 -1
  63. package/dist/server/service/database/database.service.js +246 -4
  64. package/dist/server/service/database/database.service.js.map +1 -1
  65. package/dist/server/service/database/database.service.ts +295 -7
  66. package/dist/server/service/database/mongodb.service.d.ts +0 -6
  67. package/dist/server/service/database/mongodb.service.d.ts.map +1 -1
  68. package/dist/server/service/database/mongodb.service.js +0 -8
  69. package/dist/server/service/database/mongodb.service.js.map +1 -1
  70. package/dist/server/service/database/mongodb.service.ts +0 -8
  71. package/dist/server/service/database/mssql.service.d.ts +8 -4
  72. package/dist/server/service/database/mssql.service.d.ts.map +1 -1
  73. package/dist/server/service/database/mssql.service.js +27 -132
  74. package/dist/server/service/database/mssql.service.js.map +1 -1
  75. package/dist/server/service/database/mssql.service.ts +34 -156
  76. package/dist/server/service/database/mysql.service.d.ts +0 -4
  77. package/dist/server/service/database/mysql.service.d.ts.map +1 -1
  78. package/dist/server/service/database/mysql.service.js +11 -155
  79. package/dist/server/service/database/mysql.service.js.map +1 -1
  80. package/dist/server/service/database/mysql.service.ts +17 -184
  81. package/dist/server/service/database/oracle.service.d.ts +0 -4
  82. package/dist/server/service/database/oracle.service.d.ts.map +1 -1
  83. package/dist/server/service/database/oracle.service.js +11 -141
  84. package/dist/server/service/database/oracle.service.js.map +1 -1
  85. package/dist/server/service/database/oracle.service.ts +17 -166
  86. package/dist/server/service/database/postgres.service.d.ts +0 -4
  87. package/dist/server/service/database/postgres.service.d.ts.map +1 -1
  88. package/dist/server/service/database/postgres.service.js +11 -150
  89. package/dist/server/service/database/postgres.service.js.map +1 -1
  90. package/dist/server/service/database/postgres.service.ts +17 -174
  91. package/dist/server/service/database/sap.service.d.ts +0 -4
  92. package/dist/server/service/database/sap.service.d.ts.map +1 -1
  93. package/dist/server/service/database/sap.service.js +11 -137
  94. package/dist/server/service/database/sap.service.js.map +1 -1
  95. package/dist/server/service/database/sap.service.ts +11 -158
  96. package/dist/server/service/database/sql-type.helper.d.ts +28 -0
  97. package/dist/server/service/database/sql-type.helper.d.ts.map +1 -0
  98. package/dist/server/service/database/sql-type.helper.js +113 -0
  99. package/dist/server/service/database/sql-type.helper.js.map +1 -0
  100. package/dist/server/service/database/sql-type.helper.ts +115 -0
  101. package/dist/server/service/database/sqlite.service.d.ts +0 -5
  102. package/dist/server/service/database/sqlite.service.d.ts.map +1 -1
  103. package/dist/server/service/database/sqlite.service.js +11 -156
  104. package/dist/server/service/database/sqlite.service.js.map +1 -1
  105. package/dist/server/service/database/sqlite.service.ts +17 -183
  106. package/package.json +1 -1
  107. package/server.js +1 -1
@@ -0,0 +1,456 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { canonicalizeTableEntity, canonicalizeTableSpec } from '../canonical';
3
+ import { ddlCapabilities, emitAlterDdl, emitCreateDdl, tableNameEquals } from '../ddl-emitter';
4
+ import type { TableSpec } from '../../../model/ddl-spec';
5
+
6
+ const baseEntity = () => ({
7
+ name: 'users',
8
+ comment: '',
9
+ columns: [
10
+ { name: 'id', type: 'int', nullable: false, isPrimary: true, isAutoIncrement: true },
11
+ { name: 'name', type: 'varchar(64)', nullable: false },
12
+ { name: 'age', type: 'int', nullable: true, defaultValue: '0' }
13
+ ]
14
+ });
15
+
16
+ function mirrorSpec(entity: ReturnType<typeof baseEntity>, overrides?: Partial<TableSpec>): TableSpec {
17
+ return {
18
+ name: entity.name,
19
+ comment: entity.comment,
20
+ columns: entity.columns.map((c) => ({
21
+ name: c.name,
22
+ type: c.type.replace(/\(\d+(,\d+)?\)/, ''),
23
+ length: c.type.includes('varchar') ? '64' : undefined,
24
+ nullable: c.nullable,
25
+ hasDefault: c.defaultValue !== undefined,
26
+ defaultValue: c.defaultValue !== undefined ? String(c.defaultValue) : undefined,
27
+ autoIncrement: c.isAutoIncrement
28
+ })),
29
+ primaryKey: entity.columns.filter((c) => c.isPrimary).map((c) => c.name),
30
+ ...overrides
31
+ };
32
+ }
33
+
34
+ describe('纯函数不变式', () => {
35
+ it.each(['mysql', 'postgres', 'sqlite', 'oracle', 'mssql'] as const)(
36
+ '%s:未修改的表不产生任何语句',
37
+ (kind) => {
38
+ const entity = baseEntity();
39
+ const out = emitAlterDdl(kind, canonicalizeTableEntity(entity), canonicalizeTableSpec(mirrorSpec(entity)));
40
+ expect(out).toEqual([]);
41
+ }
42
+ );
43
+
44
+ it('mongodb:unsupported 方言返回空数组', () => {
45
+ const entity = baseEntity();
46
+ const spec = mirrorSpec(entity, {
47
+ columns: [
48
+ { name: 'id', type: 'int', nullable: false, hasDefault: false },
49
+ { name: 'x', type: 'text', nullable: true, hasDefault: false }
50
+ ]
51
+ });
52
+ expect(emitAlterDdl('mongodb', canonicalizeTableEntity(entity), canonicalizeTableSpec(spec))).toEqual([]);
53
+ });
54
+ });
55
+
56
+ describe('MySQL', () => {
57
+ it('改列类型:MODIFY COLUMN 重建整列定义', () => {
58
+ const entity = baseEntity();
59
+ const spec = mirrorSpec(entity, {
60
+ columns: [
61
+ { name: 'id', type: 'int', nullable: false, hasDefault: false, autoIncrement: true },
62
+ { name: 'name', type: 'varchar', length: '128', nullable: false, hasDefault: false },
63
+ { name: 'age', type: 'int', nullable: true, hasDefault: true, defaultValue: '0' }
64
+ ],
65
+ primaryKey: ['id']
66
+ });
67
+ const out = emitAlterDdl('mysql', canonicalizeTableEntity(entity), canonicalizeTableSpec(spec));
68
+ expect(out).toEqual([
69
+ "ALTER TABLE `users` MODIFY COLUMN `name` VARCHAR(128) NOT NULL"
70
+ ]);
71
+ });
72
+
73
+ it('改名且改定义:合并为一条 CHANGE COLUMN(保留数据与列位置)', () => {
74
+ const entity = baseEntity();
75
+ const spec = mirrorSpec(entity, {
76
+ columns: [
77
+ { name: 'id', type: 'int', nullable: false, hasDefault: false, autoIncrement: true },
78
+ { name: 'name2', type: 'varchar', length: '128', nullable: true, hasDefault: false },
79
+ { name: 'age', type: 'int', nullable: true, hasDefault: true, defaultValue: '0' }
80
+ ],
81
+ primaryKey: ['id']
82
+ });
83
+ const out = emitAlterDdl('mysql', canonicalizeTableEntity(entity), canonicalizeTableSpec(spec));
84
+ expect(out).toEqual([
85
+ "ALTER TABLE `users` CHANGE COLUMN `name` `name2` VARCHAR(128)"
86
+ ]);
87
+ });
88
+
89
+ it('默认值三态:DEFAULT 0 不丢、DEFAULT \'\' 可表达、无默认不出子句', () => {
90
+ const entity = {
91
+ name: 't',
92
+ columns: [
93
+ { name: 'a', type: 'int', nullable: true },
94
+ { name: 'b', type: 'int', nullable: true, defaultValue: '5' },
95
+ { name: 'c', type: 'varchar(10)', nullable: true }
96
+ ]
97
+ };
98
+ const spec: TableSpec = {
99
+ name: 't',
100
+ columns: [
101
+ { name: 'a', type: 'int', nullable: true, hasDefault: true, defaultValue: '0' },
102
+ { name: 'b', type: 'int', nullable: true, hasDefault: false }, // 撤销默认值 → none
103
+ { name: 'c', type: 'varchar', length: '10', nullable: true, hasDefault: true, defaultValue: '' }
104
+ ],
105
+ primaryKey: []
106
+ };
107
+ const out = emitAlterDdl('mysql', canonicalizeTableEntity(entity), canonicalizeTableSpec(spec));
108
+ expect(out).toContain("ALTER TABLE `t` MODIFY COLUMN `a` INT DEFAULT 0");
109
+ expect(out).toContain("ALTER TABLE `t` MODIFY COLUMN `b` INT"); // 无 DEFAULT 子句(撤销默认值)
110
+ expect(out).toContain("ALTER TABLE `t` MODIFY COLUMN `c` VARCHAR(10) DEFAULT ''");
111
+ expect(out.join('\n')).not.toContain('DEFAULT NULL');
112
+ });
113
+
114
+ it('主键 A→B:去自增先于 DROP PRIMARY KEY,ADD PRIMARY KEY 先于加自增的 MODIFY', () => {
115
+ const entity = {
116
+ name: 't',
117
+ columns: [
118
+ { name: 'id', type: 'bigint', nullable: false, isPrimary: true, isAutoIncrement: true },
119
+ { name: 'uid', type: 'bigint', nullable: true }
120
+ ]
121
+ };
122
+ // 主键 id → uid,且 uid 要自增(effectiveAutoIncrement false→true)、id 去自增(true→false)
123
+ const spec: TableSpec = {
124
+ name: 't',
125
+ columns: [
126
+ { name: 'id', type: 'bigint', nullable: false, hasDefault: false },
127
+ { name: 'uid', type: 'bigint', nullable: false, hasDefault: false, autoIncrement: true }
128
+ ],
129
+ primaryKey: ['uid']
130
+ };
131
+ const out = emitAlterDdl('mysql', canonicalizeTableEntity(entity), canonicalizeTableSpec(spec));
132
+ expect(out).toEqual([
133
+ 'ALTER TABLE `t` MODIFY COLUMN `id` BIGINT NOT NULL', // 1) 先去 id 自增(去自增语句已含 id 的全部变化)
134
+ // 2) DROP 与 ADD 必须在同一条语句内:拆开会让表出现「无主键 + 仍有自增列」中间态(ER_WRONG_AUTO_KEY)
135
+ 'ALTER TABLE `t` DROP PRIMARY KEY, ADD PRIMARY KEY (`uid`)',
136
+ 'ALTER TABLE `t` MODIFY COLUMN `uid` BIGINT NOT NULL AUTO_INCREMENT' // 3) 主键就位后再加自增
137
+ ]);
138
+ });
139
+
140
+ it('主键扩为复合(自增列仍在主键内):同样合并为单条 DROP+ADD,且不产生去自增语句', () => {
141
+ const entity = {
142
+ name: 't',
143
+ columns: [
144
+ { name: 'id', type: 'bigint', nullable: false, isPrimary: true, isAutoIncrement: true },
145
+ { name: 'uid', type: 'bigint', nullable: false }
146
+ ]
147
+ };
148
+ const spec: TableSpec = {
149
+ name: 't',
150
+ columns: [
151
+ { name: 'id', type: 'bigint', nullable: false, hasDefault: false, autoIncrement: true },
152
+ { name: 'uid', type: 'bigint', nullable: false, hasDefault: false }
153
+ ],
154
+ primaryKey: ['id', 'uid'] // id 仍是主键且仍自增
155
+ };
156
+ const out = emitAlterDdl('mysql', canonicalizeTableEntity(entity), canonicalizeTableSpec(spec));
157
+ // 关键:不能出现单独的 DROP PRIMARY KEY(执行时 id 还是自增列 → 1075)
158
+ expect(out.some((s) => s === 'ALTER TABLE `t` DROP PRIMARY KEY')).toBe(false);
159
+ expect(out).toContain('ALTER TABLE `t` DROP PRIMARY KEY, ADD PRIMARY KEY (`id`, `uid`)');
160
+ });
161
+
162
+ it('新增自增列并设为主键:ADD COLUMN 不带 AUTO_INCREMENT,主键就位后再 MODIFY 加上', () => {
163
+ const entity = {
164
+ name: 't',
165
+ columns: [{ name: 'name', type: 'varchar(64)', nullable: true }]
166
+ };
167
+ const spec: TableSpec = {
168
+ name: 't',
169
+ columns: [
170
+ { name: 'id', type: 'bigint', nullable: false, hasDefault: false, autoIncrement: true },
171
+ { name: 'name', type: 'varchar', length: '64', nullable: true, hasDefault: false }
172
+ ],
173
+ primaryKey: ['id']
174
+ };
175
+ const out = emitAlterDdl('mysql', canonicalizeTableEntity(entity), canonicalizeTableSpec(spec));
176
+ expect(out).toEqual([
177
+ // 1) 先建列(不带 AUTO_INCREMENT——此刻还没有键)
178
+ 'ALTER TABLE `t` ADD COLUMN `id` BIGINT NOT NULL',
179
+ // 2) 建主键(原表无主键,故只有 ADD)
180
+ 'ALTER TABLE `t` ADD PRIMARY KEY (`id`)',
181
+ // 3) 再补 AUTO_INCREMENT
182
+ 'ALTER TABLE `t` MODIFY COLUMN `id` BIGINT NOT NULL AUTO_INCREMENT'
183
+ ]);
184
+ });
185
+
186
+ it('取消主键(新主键为空):只 DROP,绝不生成 ADD PRIMARY KEY ()', () => {
187
+ const entity = baseEntity();
188
+ const spec = mirrorSpec(entity, { primaryKey: [] });
189
+ const out = emitAlterDdl('mysql', canonicalizeTableEntity(entity), canonicalizeTableSpec(spec));
190
+ expect(out).toContain('ALTER TABLE `users` DROP PRIMARY KEY');
191
+ expect(out.some((s) => s.includes('ADD PRIMARY KEY'))).toBe(false);
192
+ });
193
+ });
194
+
195
+ describe('PostgreSQL', () => {
196
+ it('按 fields 精确输出:只改 nullable 时只有一条 SET/DROP NOT NULL', () => {
197
+ const entity = baseEntity();
198
+ const spec = mirrorSpec(entity, {
199
+ columns: [
200
+ { name: 'id', type: 'int', nullable: false, hasDefault: false, autoIncrement: true },
201
+ { name: 'name', type: 'varchar', length: '64', nullable: true, hasDefault: false },
202
+ { name: 'age', type: 'int', nullable: true, hasDefault: true, defaultValue: '0' }
203
+ ],
204
+ primaryKey: ['id']
205
+ });
206
+ const out = emitAlterDdl('postgres', canonicalizeTableEntity(entity), canonicalizeTableSpec(spec));
207
+ expect(out).toEqual(['ALTER TABLE "users" ALTER COLUMN "name" DROP NOT NULL']);
208
+ });
209
+
210
+ it('改默认值:SET DEFAULT / DROP DEFAULT 精确区分', () => {
211
+ const entity = {
212
+ name: 't',
213
+ columns: [
214
+ { name: 'a', type: 'int', nullable: true, defaultValue: '0' },
215
+ { name: 'b', type: 'int', nullable: true, defaultValue: '1' }
216
+ ]
217
+ };
218
+ const spec: TableSpec = {
219
+ name: 't',
220
+ columns: [
221
+ { name: 'a', type: 'int', nullable: true, hasDefault: false }, // 撤销默认值
222
+ { name: 'b', type: 'int', nullable: true, hasDefault: true, defaultValue: '2' } // 换默认值
223
+ ],
224
+ primaryKey: []
225
+ };
226
+ const out = emitAlterDdl('postgres', canonicalizeTableEntity(entity), canonicalizeTableSpec(spec));
227
+ expect(out).toEqual([
228
+ 'ALTER TABLE "t" ALTER COLUMN "a" DROP DEFAULT',
229
+ 'ALTER TABLE "t" ALTER COLUMN "b" SET DEFAULT 2'
230
+ ]);
231
+ });
232
+
233
+ it('改名后定义也变:RENAME COLUMN 仍最先执行', () => {
234
+ const entity = {
235
+ name: 't',
236
+ columns: [{ name: 'nm', type: 'varchar(10)', nullable: false }]
237
+ };
238
+ const spec: TableSpec = {
239
+ name: 't',
240
+ columns: [{ name: 'nm2', type: 'varchar', length: '20', nullable: false, hasDefault: false }],
241
+ primaryKey: []
242
+ };
243
+ const out = emitAlterDdl('postgres', canonicalizeTableEntity(entity), canonicalizeTableSpec(spec));
244
+ expect(out[0]).toBe('ALTER TABLE "t" RENAME COLUMN "nm" TO "nm2"');
245
+ expect(out).toContain('ALTER TABLE "t" ALTER COLUMN "nm2" SET DATA TYPE VARCHAR(20)');
246
+ });
247
+ });
248
+
249
+ describe('SQLite 重建表', () => {
250
+ it('输出确定性序列:固定临时表名、改名列从旧名取值、新列用默认值占位', () => {
251
+ const entity = {
252
+ name: 't',
253
+ columns: [
254
+ { name: 'id', type: 'integer', nullable: false, isPrimary: true, isAutoIncrement: true },
255
+ { name: 'nm', type: 'text', nullable: false },
256
+ { name: 'gone', type: 'text', nullable: true }
257
+ ]
258
+ };
259
+ const spec: TableSpec = {
260
+ name: 't',
261
+ columns: [
262
+ { name: 'id', type: 'integer', nullable: false, hasDefault: false, autoIncrement: true },
263
+ { name: 'nm2', type: 'text', nullable: true, hasDefault: false }, // nm 改名并放开非空
264
+ { name: 'created', type: 'text', nullable: true, hasDefault: true, defaultValue: "'x'" }
265
+ ],
266
+ primaryKey: ['id']
267
+ };
268
+ const out = emitAlterDdl('sqlite', canonicalizeTableEntity(entity), canonicalizeTableSpec(spec));
269
+ expect(out).toEqual([
270
+ '-- SQLite 不支持直接 ALTER 列/主键,以下为重建表序列(请置于事务中执行)',
271
+ 'PRAGMA foreign_keys = OFF',
272
+ 'DROP TABLE IF EXISTS "_rebuild_t"',
273
+ 'CREATE TABLE "_rebuild_t" (\n "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n "nm2" TEXT,\n "created" TEXT DEFAULT \'x\'\n)',
274
+ 'INSERT INTO "_rebuild_t" ("id", "nm2", "created") SELECT "id", "nm", \'x\' FROM "t"',
275
+ 'DROP TABLE "t"',
276
+ 'ALTER TABLE "_rebuild_t" RENAME TO "t"',
277
+ 'PRAGMA foreign_keys = ON'
278
+ ]);
279
+ });
280
+
281
+ it('纯函数:同一输入两次调用输出完全一致(可幂等重试)', () => {
282
+ const entity = baseEntity();
283
+ const spec = mirrorSpec(entity, {
284
+ columns: [
285
+ { name: 'id', type: 'int', nullable: false, hasDefault: false, autoIncrement: true },
286
+ { name: 'title', type: 'varchar', length: '64', nullable: false, hasDefault: false },
287
+ { name: 'age', type: 'int', nullable: true, hasDefault: true, defaultValue: '0' }
288
+ ],
289
+ primaryKey: ['id']
290
+ });
291
+ const a = emitAlterDdl('sqlite', canonicalizeTableEntity(entity), canonicalizeTableSpec(spec));
292
+ const b = emitAlterDdl('sqlite', canonicalizeTableEntity(entity), canonicalizeTableSpec(spec));
293
+ expect(a).toEqual(b);
294
+ });
295
+ });
296
+
297
+ describe('能力声明', () => {
298
+ it('sqlite 声明 rebuild、不支持 alterColumn/注释,但支持改主键(走整表重建)', () => {
299
+ const caps = ddlCapabilities('sqlite');
300
+ expect(caps.alterStrategy).toBe('rebuild');
301
+ expect(caps.alterColumn).toBe(false);
302
+ // 重建表可以正确落地新的主键并保留数据,故按真实能力声明为支持
303
+ expect(caps.alterPrimaryKey).toBe(true);
304
+ expect(caps.tableComment).toBe(false);
305
+ });
306
+
307
+ it('mssql 用 bracket 引用、mongodb 全不支持', () => {
308
+ expect(ddlCapabilities('mssql').quoteStyle).toBe('bracket');
309
+ expect(ddlCapabilities('mongodb').alterStrategy).toBe('unsupported');
310
+ expect(ddlCapabilities('mysql').quoteStyle).toBe('backtick');
311
+ });
312
+ });
313
+
314
+ describe('emitCreateDdl:建表单一出口', () => {
315
+ const spec: TableSpec = {
316
+ name: 'users',
317
+ comment: '用户表',
318
+ columns: [
319
+ { name: 'id', type: 'int', nullable: false, hasDefault: false, autoIncrement: true },
320
+ { name: 'name', type: 'varchar', length: '64', nullable: false, hasDefault: false, comment: '姓名' },
321
+ { name: 'age', type: 'int', nullable: true, hasDefault: true, defaultValue: '0' }
322
+ ],
323
+ primaryKey: ['id']
324
+ };
325
+ const canon = () => canonicalizeTableSpec(spec);
326
+
327
+ it('mysql:表级主键 + ENGINE=InnoDB utf8mb4 + 内联表/列注释 + DEFAULT 0 裸输出', () => {
328
+ const out = emitCreateDdl('mysql', canon());
329
+ expect(out).toEqual([
330
+ "CREATE TABLE `users` (\n" +
331
+ " `id` INT NOT NULL AUTO_INCREMENT,\n" +
332
+ " `name` VARCHAR(64) NOT NULL COMMENT '姓名',\n" +
333
+ " `age` INT DEFAULT 0,\n" +
334
+ " PRIMARY KEY (`id`)\n" +
335
+ ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'"
336
+ ]);
337
+ });
338
+
339
+ it('mysql:ifNotExists 选项输出 IF NOT EXISTS', () => {
340
+ const out = emitCreateDdl('mysql', canon(), { ifNotExists: true });
341
+ expect(out[0]).toContain('CREATE TABLE IF NOT EXISTS `users`');
342
+ });
343
+
344
+ it('postgres:表级 PRIMARY KEY + GENERATED ALWAYS AS IDENTITY + COMMENT ON 后置(修复旧版 int SERIAL 语法错误)', () => {
345
+ const out = emitCreateDdl('postgres', canon());
346
+ expect(out[0]).toBe(
347
+ "CREATE TABLE \"users\" (\n" +
348
+ ' "id" INT NOT NULL GENERATED ALWAYS AS IDENTITY,\n' +
349
+ ' "name" VARCHAR(64) NOT NULL,\n' +
350
+ ' "age" INT DEFAULT 0,\n' +
351
+ ' PRIMARY KEY ("id")\n' +
352
+ ')'
353
+ );
354
+ expect(out[1]).toBe("COMMENT ON TABLE \"users\" IS '用户表'");
355
+ expect(out[2]).toBe("COMMENT ON COLUMN \"users\".\"name\" IS '姓名'");
356
+ });
357
+
358
+ it('sqlite:单列自增主键内联 PRIMARY KEY AUTOINCREMENT、表级注释被忽略、多列主键用表级约束', () => {
359
+ const out = emitCreateDdl('sqlite', canon());
360
+ expect(out).toEqual([
361
+ "CREATE TABLE \"users\" (\n" +
362
+ ' "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n' + // 自增主键在 SQLite 必须落到字面 INTEGER
363
+ ' "name" VARCHAR(64) NOT NULL,\n' +
364
+ ' "age" INT DEFAULT 0\n' +
365
+ ')'
366
+ ]);
367
+ // 多列主键:列不内联,表级约束
368
+ const multi = emitCreateDdl('sqlite', canonicalizeTableSpec({
369
+ ...spec,
370
+ columns: spec.columns.map((c) => ({ ...c, autoIncrement: false })),
371
+ primaryKey: ['id', 'name']
372
+ }));
373
+ expect(multi[0]).toContain('"id" INT NOT NULL');
374
+ expect(multi[0]).toContain('PRIMARY KEY ("id", "name")');
375
+ expect(multi[0]).not.toContain('PRIMARY KEY AUTOINCREMENT');
376
+ });
377
+
378
+ it('mssql:IDENTITY(1,1) + 三段名前缀 + sp_addextendedproperty 注释后置', () => {
379
+ const out = emitCreateDdl('mssql', canon(), { mssqlDatabase: 'mydb' });
380
+ expect(out[0]).toBe(
381
+ "CREATE TABLE [mydb].[dbo].[users] (\n" +
382
+ ' [id] INT NOT NULL IDENTITY(1,1),\n' +
383
+ ' [name] VARCHAR(64) NOT NULL,\n' +
384
+ ' [age] INT DEFAULT 0,\n' +
385
+ ' PRIMARY KEY ([id])\n' +
386
+ ')'
387
+ );
388
+ expect(out[1]).toContain("sp_addextendedproperty @name=N'MS_Description', @value=N'用户表'");
389
+ expect(out[2]).toContain("@level2type=N'COLUMN', @level2name=N'name'");
390
+ });
391
+
392
+ it('oracle:标识符大写 + COMMENT ON 后置', () => {
393
+ const out = emitCreateDdl('oracle', canon());
394
+ expect(out[0]).toContain('CREATE TABLE "USERS" (');
395
+ expect(out[0]).toContain('"ID" INT NOT NULL');
396
+ expect(out[1]).toBe("COMMENT ON TABLE \"USERS\" IS '用户表'");
397
+ });
398
+
399
+ it('clickhouse:ENGINE 尾缀 + 主键落 ORDER BY + 无主键 ORDER BY tuple()', () => {
400
+ const out = emitCreateDdl('clickhouse', canon());
401
+ expect(out[0]).toContain(') ENGINE = MergeTree');
402
+ expect(out[0]).toContain(' ORDER BY (`id`)');
403
+ const noPk = emitCreateDdl('clickhouse', canonicalizeTableSpec({ ...spec, primaryKey: [] }));
404
+ expect(noPk[0]).toContain(' ORDER BY tuple()');
405
+ });
406
+
407
+ it('clickhouse:可空列包装 Nullable(...) 且不写 NOT NULL(裸类型即不可空)', () => {
408
+ const out = emitCreateDdl('clickhouse', canon());
409
+ // age 可空 → Nullable;id/name 不可空 → 裸类型
410
+ expect(out[0]).toContain('`age` Nullable(INT) DEFAULT 0');
411
+ expect(out[0]).toContain('`id` INT');
412
+ expect(out[0]).toContain('`name` VARCHAR(64)');
413
+ expect(out[0]).not.toContain('NOT NULL');
414
+ // 已是 Nullable 的类型不重复包装
415
+ const already = emitCreateDdl('clickhouse', canonicalizeTableSpec({
416
+ ...spec,
417
+ columns: [{ name: 'a', type: 'Nullable(String)', nullable: true, hasDefault: false }],
418
+ primaryKey: []
419
+ }));
420
+ expect(already[0]).toContain('`a` Nullable(String)');
421
+ expect(already[0]).not.toContain('Nullable(Nullable(');
422
+ });
423
+
424
+ it('tableNameEquals:按方言大小写语义判断重名', () => {
425
+ // PG/Oracle/SQLite/MSSQL 等标识符不敏感(PG 会把裸标识符落成小写)
426
+ expect(tableNameEquals('postgres', 'Users', 'users')).toBe(true);
427
+ expect(tableNameEquals('oracle', 'Users', 'USERS')).toBe(true);
428
+ expect(tableNameEquals('mysql', 'users', 'users')).toBe(true);
429
+ // ClickHouse / MongoDB 严格区分大小写
430
+ expect(tableNameEquals('clickhouse', 'Users', 'users')).toBe(false);
431
+ expect(tableNameEquals('mongodb', 'Users', 'users')).toBe(false);
432
+ });
433
+
434
+ it('sqlite:自增列类型强制 INTEGER(INT/BIGINT + AUTOINCREMENT 在 SQLite 是语法错误)', () => {
435
+ // 用户填 int / bigint 时 canonical 保留原类型名,SQLite 侧必须落到字面 INTEGER
436
+ for (const t of ['int', 'bigint']) {
437
+ const out = emitCreateDdl('sqlite', canonicalizeTableSpec({ ...spec, columns: [{ name: 'id', type: t, nullable: false, hasDefault: false, autoIncrement: true }] }));
438
+ expect(out[0]).toContain('"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT');
439
+ }
440
+ // 非自增列保持用户类型字面
441
+ const plain = emitCreateDdl('sqlite', canonicalizeTableSpec({ ...spec, columns: [{ name: 'id', type: 'int', nullable: false, hasDefault: false }], primaryKey: [] }));
442
+ expect(plain[0]).toContain('"id" INT NOT NULL');
443
+ });
444
+
445
+ it('sap:CREATE COLUMN TABLE + 表级 COMMENT 后缀(对齐旧方言 createTable,避免丢表注释)', () => {
446
+ const out = emitCreateDdl('sap', canon());
447
+ expect(out[0]).toContain('CREATE COLUMN TABLE "users" (');
448
+ expect(out[0]).toContain('"id" INT NOT NULL GENERATED BY DEFAULT AS IDENTITY');
449
+ expect(out[0]).toContain("COMMENT '姓名'"); // 列注释内联
450
+ expect(out[0]).toContain(") COMMENT '用户表'"); // 表注释后缀
451
+ });
452
+
453
+ it('非 SQL 方言返回空数组(调用方按 caps.createTable 拒绝)', () => {
454
+ expect(emitCreateDdl('mongodb', canon())).toEqual([]);
455
+ });
456
+ });